Skip to content
Open
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
2 changes: 2 additions & 0 deletions .github/trivy-no-exceptions.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# No accepted image vulnerability exceptions.
# Bare advisory IDs are intentionally absent.
62 changes: 61 additions & 1 deletion .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,14 @@ jobs:
snapshot-ref: ${{ github.event_name == 'pull_request' && format('refs/heads/{0}', github.event.pull_request.head.ref) || github.ref }}

- name: Dependency Review
id: review
if: >-
${{ github.event_name == 'pull_request' &&
steps.dependency-changes.outputs.changed == 'true' }}
uses: actions/dependency-review-action@v5
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
base-ref: ${{ github.event.pull_request.base.sha }}
head-ref: ${{ github.event.pull_request.head.sha }}
fail-on-severity: moderate
# The dependency graph API can briefly report "no snapshots found for the
# head SHA" when a PR's dependency snapshot is submitted asynchronously and
Expand Down Expand Up @@ -191,6 +194,63 @@ jobs:
pkg:npm/@img/sharp-win32-x64
comment-summary-in-pr: always

# Preserve the graph comparison even when a composite license is rejected.
# An empty/missing action output stays empty; it is not a clean result.
- name: Record dependency review evidence
if: >-
${{ always() && github.event_name == 'pull_request' &&
steps.dependency-changes.outputs.changed == 'true' }}
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
REVIEW_OUTCOME: ${{ steps.review.outcome }}
DEPENDENCY_CHANGES: ${{ steps.review.outputs.dependency-changes }}
INVALID_LICENSE_CHANGES: ${{ steps.review.outputs.invalid-license-changes }}
VULNERABLE_CHANGES: ${{ steps.review.outputs.vulnerable-changes }}
DENIED_CHANGES: ${{ steps.review.outputs.denied-changes }}
run: |
python3 - <<'PY'
import json
import os
from pathlib import Path

evidence = {
"schema_version": 1,
"base_sha": os.environ["BASE_SHA"],
"head_sha": os.environ["HEAD_SHA"],
"run_id": os.environ["GITHUB_RUN_ID"],
"run_attempt": os.environ["GITHUB_RUN_ATTEMPT"],
"review_outcome": os.environ["REVIEW_OUTCOME"],
# Retain the exact raw JSON strings, including duplicate entries,
# full SPDX expressions, and empty outputs from incomplete reviews.
"raw_outputs": {
name: os.environ[variable]
for name, variable in {
"dependency-changes": "DEPENDENCY_CHANGES",
"invalid-license-changes": "INVALID_LICENSE_CHANGES",
"vulnerable-changes": "VULNERABLE_CHANGES",
"denied-changes": "DENIED_CHANGES",
}.items()
},
}
destination = Path("dependency-review-evidence")
destination.mkdir(exist_ok=True)
(destination / "review.json").write_text(
json.dumps(evidence, indent=2) + "\n", encoding="utf-8"
)
PY

- name: Upload dependency review evidence
if: >-
${{ always() && github.event_name == 'pull_request' &&
steps.dependency-changes.outputs.changed == 'true' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dependency-review-${{ github.event.pull_request.head.sha }}-${{ github.run_attempt }}
path: dependency-review-evidence/review.json
if-no-files-found: error
retention-days: 30

- name: Record no dependency delta
if: >-
${{ github.event_name == 'pull_request' &&
Expand Down
35 changes: 27 additions & 8 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ jobs:
timeout-minutes: 10 # Prevent long-running jobs
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Cache Trivy DB
uses: actions/cache@v6
with:
Expand All @@ -104,31 +106,48 @@ jobs:
trivy-db-
continue-on-error: true
- name: Build image for scanning
run: docker build -t eventrelay:test -f Dockerfile .
- name: Run Trivy vulnerability scanner
run: |
docker build -t eventrelay:test -f Dockerfile .
docker image inspect eventrelay:test > image-identity.json
git rev-parse HEAD > image-source-commit.txt
# Bootstrap #2425 ships dependency repairs separately from the final
# zero-HIGH/CRITICAL gate. Findings remain unresolved in #2396; success
# here proves scan completion, not an accepted baseline or a clean image.
- name: Report Trivy vulnerabilities (enforcement deferred)
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: 'eventrelay:test'
format: 'sarif'
output: 'trivy-results.sarif'
exit-code: '0'
ignore-unfixed: true
ignore-unfixed: false
vuln-type: 'os,library'
severity: 'CRITICAL,HIGH'
trivyignores: '.trivyignore'
limit-severities-for-sarif: true
trivyignores: '.github/trivy-no-exceptions.txt'
- name: Upload Trivy results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v4
if: hashFiles('trivy-results.sarif') != ''
if: always() && hashFiles('trivy-results.sarif') != ''
with:
sarif_file: 'trivy-results.sarif'
- name: Generate human-readable report
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
if: success()
if: always()
with:
image-ref: 'eventrelay:test'
format: 'table'
exit-code: '0'
ignore-unfixed: true
ignore-unfixed: false
vuln-type: 'os,library'
severity: 'CRITICAL,HIGH'
trivyignores: '.trivyignore'
trivyignores: '.github/trivy-no-exceptions.txt'
- name: Retain exact image scan evidence
uses: actions/upload-artifact@v7
if: always()
with:
name: trivy-image-evidence
path: |
trivy-results.sarif
image-identity.json
image-source-commit.txt
if-no-files-found: error
15 changes: 12 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/*

# Copy dependency manifests
COPY pyproject.toml requirements.txt* ./
COPY pyproject.toml requirements.txt* constraints.txt ./
COPY package.json package-lock.json ./
COPY apps/web/package.json ./apps/web/

Expand All @@ -32,8 +32,12 @@ COPY apps/web/package.json ./apps/web/
COPY apps/web/scripts ./apps/web/scripts

# Install Python dependencies
RUN pip install --no-cache-dir --upgrade pip && \
(pip install --no-cache-dir -r requirements.txt || pip install --no-cache-dir -e .)
RUN pip install --no-cache-dir --upgrade \
pip \
"setuptools>=80.10.2" \
"wheel>=0.46.2" && \
(pip install --no-cache-dir -c constraints.txt -r requirements.txt || \
pip install --no-cache-dir -c constraints.txt -e .)

# Install Node.js dependencies for the web app
# Using workspace to ensure proper hoisting and dependency resolution
Expand All @@ -53,13 +57,18 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y nodejs \
&& npm install --global npm@12.2.0 \
&& rm -rf /var/lib/apt/lists/*

# Create non-root user
RUN groupadd --gid 1000 appuser && \
useradd --uid 1000 --gid appuser --shell /bin/bash --create-home appuser

# Copy installed Python packages from builder
# The base image ships packaging metadata of its own. Clear it before copying
# the builder environment so stale vulnerable dist-info/vendor records cannot
# survive the directory overlay into the runtime image.
RUN rm -rf /usr/local/lib/python3.11/site-packages/*
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin

Expand Down
66 changes: 35 additions & 31 deletions MERGE_POLICY.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,27 +113,34 @@ strand a change permanently pending. v1 listed `Security` among its required
six, and dropping every security scan out of the required set would be a
weakening introduced by the rewrite that set out to make this gate precise.

> **What requiring these does and does not buy you.** Four of the six cannot
> fail on findings, by construction, in `.github/workflows/security.yml`:
>
> | Job | Why it always reports `success` |
> | --- | --- |
> | `npm-audit` | `continue-on-error: true` |
> | `python-safety` | `\|\| true` **and** `continue-on-error: true` |
> | `bandit` | `\|\| true` |
> | `trivy` | `exit-code: '0'` on both invocations |
>
> `Security Scan - python` and `Security Scan - javascript` are CodeQL matrix
> jobs; they report that analysis completed and upload alerts to the Security
> tab. Whether an alert blocks a merge is a code-scanning check-failure setting,
> not something the workflow decides.
>
> So gate 2 requires these security scan jobs (check-runs) to **run and complete** —
> which catches a scan that crashed, timed out, or was silently dropped from the
> pipeline. It is not a findings gate, and this policy should not be read as
> claiming vulnerability enforcement. Making findings actually block is a change to
> `security.yml`, with its own diff and its own blast radius; see *What is
> deliberately not here*.
`trivy` is temporarily a required **scan-completion gate** for the dependency
repair bootstrap (#2425, PR #2409). Image build or scanner execution errors
still fail the job, but reported vulnerabilities do not (`exit-code: '0'`).
The HIGH/CRITICAL SARIF includes findings without a fix; SARIF, exact source
commit and image identity are retained even when the job fails. A green `trivy`
or uppercase `Trivy` SARIF-ingestion check does not establish a clean image.

The dependency repairs are separated from final strict enforcement. This is
partial remediation, with no accepted baseline and no risk acceptance for the
remaining findings. Parent #2396 stays open until a re-fetched exact image or
separately reviewed applicability/VEX evidence accounts for every finding.
See [the bootstrap evidence and remaining gates](docs/security/image-remediation-bootstrap.md).
Enable the final `exit-code: '1'` HIGH/CRITICAL gate in a separately reviewed
change after that accounting, or introduce an explicitly reviewed delta policy
with package/version/path identity and regression tests. Keep
`limit-severities-for-sarif: true` so the pinned action honors the threshold.

No exceptions are accepted by the current image scan. Existing bare IDs in
`.trivyignore` are not evidence of approved risk acceptance and are not applied.
Any future exception must be introduced in a separately reviewed change with
an exact advisory/package/version/image scope, applicability evidence,
compensating controls, an accountable approver, linked issue, expiry, and an
enforced expiry check. A missing advisory or unproven exposure remains unresolved.

The other security jobs also remain completion gates: `npm-audit`, `python-safety`,
and `bandit` still tolerate findings. CodeQL check failure also depends on the
repository's code-scanning settings. Their green conclusions do not establish
absence of vulnerabilities.

> **`trivy` is lowercase.** Two distinct check-runs exist on the same head —
> `trivy` reports `success`, `Trivy` reports `neutral`, confirmed on both heads
Expand Down Expand Up @@ -252,16 +259,13 @@ laundering itself as enforcement.
- **No evidence ledger requirement.** v1 mandated a receipt comment on #898 for
every gate evaluation. Nothing consumed those receipts. Git history and
check runs are the ledger.
- **No change to what the security scans enforce.** Gate 2 requires the six
security checks to run and complete; four of them cannot fail on findings, as
documented there. Turning `npm-audit`, `python-safety`, `bandit`, and `trivy`
into blocking gates means removing their `continue-on-error` / `|| true` /
`exit-code: '0'` escapes in `security.yml`, which will fail pull requests on
the existing backlog of findings the moment it lands. That is a deliberate
decision about tolerance for known vulnerabilities, not a documentation fix,
so it is left to its own pull request. Requiring the checks first is still
worth doing: it makes a scan that crashes or quietly disappears from the
pipeline visible, which is the failure this policy can actually close.
- **Strict image enforcement is deferred explicitly.** The #2425 bootstrap
keeps `trivy` required for scan completion and evidence retention while #2396
owns the unresolved findings and final enforcement. This is not a clean-image
claim or an approved exception list. Do not automatically demote `trivy` or
suppress findings for a high failure rate; the 50% demotion rule applies to
broken execution, not to valid security findings. Final enforcement requires
the separate review and finding accounting described in gate 2.

## Review clock

Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"flags": "^4.3.1",
"jpeg-js": "^0.4.4",
"lucide-react": "^1.47.0",
"next": "^16.3.3",
"next": "16.3.8",
"next-auth": "^4.24.15",
"openai": "^7.23.0",
"react": "^19",
Expand Down
2 changes: 2 additions & 0 deletions constraints.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Security floors for transitive Python packages copied into the runtime image.
urllib3>=2.8.0
102 changes: 102 additions & 0 deletions docs/security/image-remediation-bootstrap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Image dependency remediation bootstrap

PR #2409 implements the bounded child [#2425](https://github.com/groupthinking/EventRelay/issues/2425).
The parent [#2396](https://github.com/groupthinking/EventRelay/issues/2396) remains
the unresolved image-security backlog. This is partial remediation; neither
scanner success nor a smaller report establishes a clean image or production
remediation.

## Bootstrap decision

Ship the already-verified application dependency repairs separately from final
strict Trivy enforcement. The required lowercase `trivy` job builds and scans
the exact PR source and retains image identity, source SHA and HIGH/CRITICAL
SARIF. Findings, including unfixed findings, are reported with `exit-code: '0'`.
Build and scanner execution failures still fail the job. No `continue-on-error`
is added to either scan, and the empty `.github/trivy-no-exceptions.txt` remains
the only ignore input. Existing `.trivyignore` IDs are not applied.

This is an explicit split, not a baseline-delta gate or accepted risk. It does
not block newly introduced findings. Review each new exact-image report before
merge; do not infer clearance from the scan-completion check. A future delta
gate would need separate review of advisory/package/version/path identity,
severity and occurrence regressions, missing/malformed evidence, and baseline
changes. A total-count cap alone is insufficient.

`limit-severities-for-sarif: true` fixes the review finding that the pinned action
otherwise clears `TRIVY_SEVERITY` for SARIF output. The historical SARIF files
below retain all originally generated results; future reports deliberately
honor the configured HIGH/CRITICAL threshold. See the
[pinned action implementation](https://github.com/aquasecurity/trivy-action/blob/ed142fd0673e97e23eac54620cfb913e5ce36c25/entrypoint.sh#L69-L76).

## Retained image evidence, inspected 2026-10-16

These are two historical snapshots. A new commit requires new hosted evidence.
Both images are linux/amd64; neither has a published `RepoDigest` in the retained
Docker inspect output. The following SHA256 values identify local image
contents, not registry publication.

| Field | Original retained scan | Latest inspected PR head |
| --- | --- | --- |
| Source commit | `1d9f72d7c40bff34aa43b8107cb817ad01e8efd5` | `64d2562dce7f2d870b32e3982ffe8888bbdcd808` |
| Run / job | [38258616455 / 114843053097](https://github.com/groupthinking/EventRelay/actions/runs/38258616455/job/114843053097) | [38706968204 / 116328658621](https://github.com/groupthinking/EventRelay/actions/runs/38706968204/job/116328658621) |
| Artifact ID | `11751896119` | `11958184738` |
| Image ID | `sha256:3e479db974698dc0bf11eec7fbce12acee901a33fe8179570fe4418a6b2ec34e` | `sha256:58c3934b3b50d5e9086a822bfd87f185839ff61f8302b0d165f1960acf839201` |
| HIGH / CRITICAL occurrences | **259** (258 / 1) | **252** (251 / 1) |
| OS / global npm / Python occurrences | 251 / 4 / 4 | 244 / 4 / 4 |
| Unique HIGH/CRITICAL advisories | 68 | 63 |
| All original SARIF results | 905 | 934 |

Artifact ZIP SHA256:

- Original: `545f1565a67c41fe5a9d641854e74f28bbb7050aa3247e537ca27d4fcb976459`.
- Latest inspected: `1ff1b9ca9a058b14f536a585bbe9e29e8dc0dbd3d079940878b0bd6422275217`.

Original `trivy-results.sarif` SHA256:

- Original: `3b32c283e60868c70b2929bc936d38c08e5f4f410d3e21ecad76ec10c95c9c37`.
- Latest inspected: `ebc2fa5c7bb192d8d523ed1395944cb2d2c3a780e905d9baf61763bfd67ed6f6`.

Both downloaded ZIPs were checked against provider artifact digests. Each
contains `image-source-commit.txt`, `image-identity.json` and the original
`trivy-results.sarif`; all original package paths and finding occurrences remain
in those artifacts. Local retained copies and occurrence-by-occurrence
reconciliation are under `work/pr2409-evidence/`, including
`scan-comparison.json`. Do not rewrite the 259-occurrence snapshot as a 252-row
baseline.

The source changes between these two heads are a merge from `main`; the image
Dockerfile, dependency inputs and scan workflow are unchanged. Seven
advisory/package/path occurrences disappeared from the later report:
`CVE-2026-8286` and `CVE-2026-8927` in curl/libcurl4t64,
`CVE-2026-88647` and `CVE-2026-88648` in libgnutls30t64, and
`CVE-2026-96889` in librsvg2-2. Curl/libcurl moved from
`8.14.1-2+deb13u5` to `8.14.1-2+deb13u7`; four continuing findings therefore
have changed version-bearing keys. Different image builds and scanner databases
can change reports. These disappearances are observations, not independently
accounted remediations.

## Remaining gates and ownership

All 244 OS occurrences in the latest inspected report have no fixed version
populated. The npm and pip vendor findings remain unresolved. Keep package
paths and runtime reachability in the #2396 ledger: application constraints
cannot replace private npm/pip vendor bundles, and removal of runtime tools is
a compatibility change requiring its own review.

Before closing #2425, obtain CI, Security Scan, Dependency Review, E2E, Coverage,
CodeQL, secret scan and applicable reviews for one exact implementation head.
Keep the PR draft while intentional required checks fail. The inspected
`64d2562…` head was open and **not** draft despite the stale description; it is
not evidence that these new changes passed hosted checks.

Before final strict enforcement, a re-fetched exact image or separately reviewed
applicability/VEX evidence must account for every remaining finding in #2396.
Enable `exit-code: '1'` with the same unfixed-finding and severity settings in a
separately reviewed change. Any exception needs the exact scope, applicability
evidence, owner, linked issue, compensating controls, expiry and enforced expiry
validation required by `MERGE_POLICY.md`. No merge, deployment, clean-image
claim or closure of #2396 follows from this bootstrap.

See [NumPy license investigation](numpy-license-review.md) for the independent
license gate. The license allowlist is unchanged.
Loading
Loading