Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/ACTION_PINS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Reviewed action pins

Resolved release tags through each authoritative repository Git ref (including
annotated-tag dereferencing) on 2026-09-26 UTC. These are source pins, not claims
that runtime tool downloads are made immutable by pinning an action.

| Repository/action | Release | Commit |
| --- | --- | --- |
| `actions/checkout` | [v7.0.1](https://github.com/actions/checkout/releases/tag/v7.0.1) | `3d3c42e5aac5ba805825da76410c181273ba90b1` |
| `actions/setup-java` | [v6.0.1](https://github.com/actions/setup-java/releases/tag/v6.0.1) | `de7274f081f381c8f8158605e0321c36c376e2e6` |
| `actions/upload-artifact` | [v4.6.2](https://github.com/actions/upload-artifact/releases/tag/v4.6.2) | `ea165f8d65b6e75b540449e92b4886f43607fa02` |
| `actions/download-artifact` | [v4.3.0](https://github.com/actions/download-artifact/releases/tag/v4.3.0) | `d3f86a106a0bac45b974a628896c90dbdf5c8093` |
| `github/codeql-action` | [v4.38.2](https://github.com/github/codeql-action/releases/tag/v4.38.2) | `2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2` |
| `advanced-security/maven-dependency-submission-action` | [v6.0.1](https://github.com/advanced-security/maven-dependency-submission-action/releases/tag/v6.0.1) | `a64327a7329c9939cf675e458452febe1894a70c` |

CodeQL uses the `init` and `analyze` subactions at the same commit. No other
external actions or reusable workflows are referenced. Upload/download stay on
the existing v4 major versions; upgrading their packaging protocol is separate
work. All actions are JavaScript actions, not composite actions with hidden
`uses` references. Their metadata, entrypoints and relevant credential/download
paths were reviewed, along with the release changes.

CodeQL's release tag is annotated: the Git ref points to tag object
`88585263c0627ee42c0e1c5143a112c8d6f4aa18`, which must be dereferenced to the
commit in the table. Do not copy an annotated tag object's SHA into a commit
pin. Verify the object's type and follow tag targets until it is a commit
(equivalently, inspect the peeled `refs/tags/<version>^{}` Git ref).

- Checkout uses Git and the supplied token for retrieval; persistence is disabled.
- Setup Java runs its bundled JavaScript, downloads Temurin from the upstream
service when absent from the hosted toolcache and verifies the distribution.
Dependency caching is disabled. JDK version lines are intentionally updated
within their supported major versions; a commit pin does not pin the JDK bytes.
- Artifact actions use bundled clients and GitHub's artifact service. Download
is restricted to the same workflow run; no privileged cross-run reuse occurs.
- CodeQL runs bundled JavaScript and obtains its corresponding CodeQL tools/query
bundle. No repository token with content-write permission or custom scanner
secret is available to the analysis jobs.
- Maven submission v6.0.1 runs the checkout's Maven command with the pinned
`com.github.ferstl:depgraph-maven-plugin:4.0.3`, then submits with GitHub's bundled
dependency toolkit. It has no composite action references. Its source logs
the snapshot. Maven plugins and their resolved transitives remain a separate
supply-chain boundary, covered by build graph submission.

Working allowlist (GitHub-owned/verified blanket allowances are disabled):

```text
actions/checkout@*
actions/setup-java@*
actions/upload-artifact@*
actions/download-artifact@*
github/codeql-action/init@*
github/codeql-action/analyze@*
advanced-security/maven-dependency-submission-action@*
```

GitHub's full-SHA policy separately enforces the immutable action reference.
Dependabot proposes SHA updates weekly; reviewers must repeat source/release and
transitive behavior review, run actionlint, and wait for the full packaging gates.
Future release actions in #95 must satisfy the same inventory and review policy.
121 changes: 121 additions & 0 deletions .github/CI_SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# CI and security operations

## Required coverage and trust boundaries

`Java CI gate` requires the clean JDK 17 reactor build, JSP/Jakarta source parity,
CI policy tests, the Docker/Selenium browser test, exact reactor JAR inclusion in
the Jakarta WAR, and every ESAPI version from 2.5.1.0 through 2.7.0.0.
`Packaged consumer gate` requires JDK 17 artifact preparation/API and Java 8
signature checks, package guard tests, the original packaged bytes on Java
8/11/17/21/25, and core/JSP/ESAPI unit tests with the Java 8 JVM and coverage.
JDK 21/25 build probes remain advisory. Require **both** gates: neither observes
the other workflow. The gates run with `always()` and accept only `success` for
every expected dependency; missing, failed, skipped and cancelled jobs fail.

Both workflows run on pushes to main, pull requests, weekly schedules and manual
dispatch, with bounded timeouts and per-workflow/ref concurrency cancellation.
There are no path filters that could silently omit a required check. Changes to
workflow topology must update the gate dependencies and required check names
only after the replacement checks have succeeded.

Every checkout disables credential persistence. PRs use `pull_request`, never
`pull_request_target` or privileged `workflow_run` execution. Fork tokens are
read-only and receive no repository secrets; all external contributors require
run approval. A fork's artifacts are consumed only within that same unprivileged
run. Artifact download has no cross-run/repository/token input, preserving the
original packaged bytes. No privileged job restores PR artifacts or caches.

All Maven invocations use a fresh runner-temporary repository. Shared Maven
caching is deliberately disabled, including Java 8's necessary `install`, all
scanner builds, and intentional release commits. Setup Java's pinned v6.0.1
does expose cache controls, but this configuration does not rely on any of them.
See [local quarantine guidance](../RELEASING.md#maven-storage-and-repository-controls).

Baseline main runs [Java CI 36220505798](https://github.com/OWASP/owasp-java-encoder/actions/runs/36220505798)
and [consumers 36220505783](https://github.com/OWASP/owasp-java-encoder/actions/runs/36220505783)
had 14 Maven cache misses and one hit (Java 8 install job). Build took 3m14s;
ESAPI jobs 65–97s; preparation 85s; Java 8 tests 87s; runtimes 12–16s.
The separate clean test-compilation and install lifecycles are now one clean
verify lifecycle. Further core sharing between ESAPI versions is deferred: it
would complicate reactor resolution and package checks for little measured gain.

## Scanning and dependency updates

Advanced CodeQL in `codeql.yaml` is the single analysis owner; leave default
setup unconfigured. Java uses a manual JDK 17 build of all four libraries and
the optional `testJakarta` application; Actions and Python tooling use extraction
without a build. It runs on PRs, main, a weekly schedule and manual dispatch.
Only analysis jobs request `security-events: write`; fork PRs use GitHub's
restricted PR SARIF upload path, not a general write token. Reports are retained
as artifacts and in Code scanning. No scanner secrets are required.

Dependency submission runs only for the trusted default branch, including manual
dispatch. Its only elevated permission is job-level `contents: write` for the
snapshot API. It builds its own checkout without caches or imported artifacts.
Separate correlators submit the normal reactor and the optional Jakarta profile.
The pinned Maven submission action includes all resolved project scopes,
including runtime, test and provided dependencies. Maven dependency plugin
3.9.0 `resolve-plugins` separately resolves build/report plugins and their
transitives; `scripts/build-dependency-snapshot.py` submits those edges as
development dependencies. Graph reports and submission JSON are retained for
inspection. Inspect representative ESAPI/AntiSamy HTTP transitives and Jakarta
Spring/Tomcat dependencies in the resulting graph; alert counts are not gates.

Dependabot checks all library POMs, the parent and optional app weekly, with
separate Maven and SHA-pinned Actions groups and grouped Maven security updates.
Normal review and complete CI apply to automated PRs; no automatic merging is
configured. Review new action source and transitive downloads as well as pins.
The nonstandard XML files under `compatibility/dependencies` remain explicit
manual compatibility fixtures. In particular Felix 5.6.12 is an intentional
OSGi R6/Java 8 baseline, not a production dependency; the Maven ignore prevents
an automatic baseline replacement. Inspect any advisory against its actual
local bundle-loading test scope, and record a specific disposition. Do not
suppress advisories across all Felix versions or application deployments.

ESAPI advisory triage lives in [the adapter guide](../esapi/README.md#dependency-security-triage).
Upstream fixes are preferred; tested mitigations remain possible. No transitive
finding is dismissed merely because another library introduces it. Optional
Scorecard publication, best-practices registration and another scheduled scanner
are follow-ups, not prerequisites for these operating controls.

## Repository controls and recovery

Before changes, snapshot repository/ruleset, Actions, Pages, collaborator/team
and webhook settings. Store webhook configuration privately; do not publish
endpoint tokens. Record changes and negative tests in issue #169.

Use read-only default Actions tokens, no bot PR approvals, supported secret
scanning/push protection, all-external-contributor run approval, and an allowlist
of the exact action repositories/paths in [the action inventory](ACTION_PINS.md).
Enable SHA enforcement after pins and the allowlist are ready. Re-read both
Actions endpoints afterward: updating the general permissions endpoint may reset
`github_owned_allowed` to true; reapply the exact allowlist and test rejection of
an unlisted GitHub-owned action as well as a mutable action tag. When adding a
reviewed action, update the allowlist before merging the workflow. Old open PRs
must refresh their workflows before rerunning under the SHA policy.

Main requires both complete CI gates and successful CodeQL language jobs in a
separate ruleset with no bypass. The review rule keeps one independent approval,
dismisses stale approvals, requires approval of the latest push, and requires
resolution of review threads. The
verified maintainers Jim Manico and Jeremy Long have only
PR-based, audit-visible emergency **review** bypass; it cannot bypass required
checks or directly push main. An emergency bypass is not independent approval.
CODEOWNERS enforcement remains with #127's ownership validation; do not name
unverified owners or treat review rules as CODEOWNERS validation.

All tags prohibit update/deletion with no bypass; new signed release tags still
follow `RELEASING.md`. GitHub `required_signatures` checks commit signatures,
not annotated tag signatures. Legacy Codacy and Travis webhooks have no current
workflow/required-check owner and return 404/502; disable them while retaining
their private configuration for recovery. Site retirement remains with #96.

For recovery, an administrator uses Settings or the REST API, records the actor,
reason and affected PR in #169, makes the narrowest temporary change, then
restores and verifies the controls. Do not introduce privileged PR workflows to
repair policy failures. Check names can be repaired without removing review
requirements; action patterns can be added without disabling SHA enforcement.
There is no standing direct-push bypass. Never move a published tag or blanket
disable tag protection. A disposable unpublished policy-test tag can be removed
using an exclusion for that exact tag only, then immediately removing the
exclusion. Review branch cleanup against current main and open PRs individually.
38 changes: 38 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
version: 2
updates:
- package-ecosystem: maven
directories:
- /
- /core
- /jsp
- /jakarta
- /esapi
- /jakarta-test
schedule:
interval: weekly
day: monday
time: '09:00'
timezone: Etc/UTC
open-pull-requests-limit: 5
groups:
maven-dependencies:
applies-to: version-updates
patterns: ['*']
maven-security:
applies-to: security-updates
patterns: ['*']
# The Felix 5.6.12 fixture is the intentional OSGi R6/Java 8 baseline.
# Review alerts by execution scope; never dismiss or upgrade it blindly.
ignore:
- dependency-name: org.apache.felix:org.apache.felix.framework
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: monday
time: '09:00'
timezone: Etc/UTC
open-pull-requests-limit: 3
groups:
actions:
patterns: ['*']
90 changes: 69 additions & 21 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,71 @@ on:
branches:
- main
pull_request:
workflow_dispatch:
schedule:
- cron: '23 6 * * 1'

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

defaults:
run:
shell: bash

jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check versions and isolate Maven storage
run: |
python3 scripts/check-ci-version.py --ref "$GITHUB_REF"
echo "MAVEN_OPTS=-Dmaven.repo.local=$RUNNER_TEMP/m2" >> "$GITHUB_ENV"
- name: Check JSP and Jakarta source parity
run: python3 scripts/check-taglib-parity.py
- name: Set up JDK 17
uses: actions/setup-java@v6
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Check jakarta-test uses this build's encoder version
- name: Test CI policy boundaries
run: python3 -m unittest discover -s scripts/tests
- name: Verify clean reactor including the required Docker/browser test
run: mvn -B -ntp clean verify -PtestJakarta 2>&1 | tee build.log
- name: Confirm the Jakarta application contains this reactor's exact JAR
run: |
evaluate() {
mvn -B -ntp -q "$@" org.apache.maven.plugins:maven-help-plugin:3.5.2:evaluate -DforceStdout
}
reactor=$(evaluate -N -Dexpression=project.version) || { echo "$reactor"; exit 1; }
tested=$(evaluate -f jakarta-test/pom.xml -Dexpression=encoder.version) || { echo "$tested"; exit 1; }
if [ "$reactor" != "$tested" ]; then
echo "::error file=jakarta-test/pom.xml::encoder.version is '$tested' but the build is '$reactor'. Update jakarta-test/pom.xml when bumping the version."
exit 1
fi
echo "jakarta-test tests encoder-jakarta-jsp $tested from this build"
- name: Test clean reactor compilation
run: mvn -B -ntp clean test-compile
- name: Run build
run: mvn -B -ntp install -PtestJakarta
python3 - <<'PY'
from pathlib import Path
from zipfile import ZipFile
jars = [p for p in Path('jakarta/target').glob('encoder-jakarta-jsp-*.jar')
if not p.name.endswith(('-sources.jar', '-javadoc.jar'))]
assert len(jars) == 1, jars
jar = jars[0]
with ZipFile('jakarta-test/target/jakarta-test.war') as war:
assert war.read('WEB-INF/lib/' + jar.name) == jar.read_bytes()
print('Jakarta WAR contains the exact reactor JAR:', jar.name)
PY
- name: Preserve build and browser diagnostics
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: reactor-browser-diagnostics
path: |
build.log
**/target/surefire-reports/
**/target/failsafe-reports/

esapi-compatibility:
name: ESAPI ${{ matrix.esapi-version }}
runs-on: ubuntu-latest
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
Expand All @@ -57,12 +85,32 @@ jobs:
- '2.6.2.0'
- '2.7.0.0'
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check versions and isolate Maven storage
run: |
python3 scripts/check-ci-version.py --ref "$GITHUB_REF"
echo "MAVEN_OPTS=-Dmaven.repo.local=$RUNNER_TEMP/m2" >> "$GITHUB_ENV"
- name: Set up JDK 17
uses: actions/setup-java@v6
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Test ESAPI compatibility
run: mvn -B -ntp -pl esapi -am verify -Desapi.version=${{ matrix.esapi-version }}

gate:
name: Java CI gate
if: ${{ always() }}
needs: [build, esapi-compatibility]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Require every build, browser and ESAPI matrix result
env:
NEEDS: ${{ toJSON(needs) }}
run: python3 scripts/check-ci-gate.py build esapi-compatibility
Loading
Loading