From 52e6af04a7b1953dc18a0b34faacf81c403bf86a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 03:53:34 +0900 Subject: [PATCH 1/4] fix(semgrep): make the pinned image digest authoritative Replay unique SEMGREP_IMAGE single-source onto current main without rewriting hashed opencode-review-dispatch.yml (83f6830d). One job-level digest is logged, inspected, and passed to docker run. NVIDIA_NIM_API_KEY remains the model credential. Do not rewrite hashed dispatch. --- .github/workflows/sast-semgrep.yml | 17 +++++++++++-- AGENTS.md | 1 + ARCHITECTURE.md | 5 ++++ CHANGELOG.md | 12 ++++++--- CLAUDE.md | 3 ++- .../semgrep-image-digest-single-source.md | 25 +++++++++++++++++++ ...central_required_workflow_ruleset_audit.py | 12 +++++++++ 7 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 docs/doctoring/semgrep-image-digest-single-source.md diff --git a/.github/workflows/sast-semgrep.yml b/.github/workflows/sast-semgrep.yml index 0f31c3c43..e4f117805 100644 --- a/.github/workflows/sast-semgrep.yml +++ b/.github/workflows/sast-semgrep.yml @@ -56,6 +56,9 @@ jobs: # Deterministic, no telemetry: registry rules are fetched but no scan data # is sent back. SEMGREP_SEND_METRICS: "off" + # Semgrep OSS 1.169.0. Keep the immutable manifest reference in one + # place so hosted scans and local reproduction cannot drift. + SEMGREP_IMAGE: "semgrep/semgrep@sha256:2b33f46ba66cf8cc2ad59ccfa7d22951fd00c632c38f1339e84ec8e6e641a942" steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 @@ -65,17 +68,27 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false + - name: Verify pinned Semgrep manifest + run: | + set -euo pipefail + if [[ "${SEMGREP_IMAGE}" =~ ^semgrep/semgrep@sha256:[0-9a-f]{64}$ ]]; then + docker manifest inspect "${SEMGREP_IMAGE}" >/dev/null + echo "Verified immutable Semgrep manifest: ${SEMGREP_IMAGE}" + else + echo "::error::SEMGREP_IMAGE must be a complete semgrep/semgrep@sha256:<64-hex-digest> reference." + exit 1 + fi - name: Run Semgrep (SARIF) id: semgrep run: | set +e - echo "Using semgrep/semgrep:1.169.0@sha256:2b33f46ba66cf8cc2ad59ccfa7d22951fd00c632c38f1339e84ec8e6e641a942" + echo "Using ${SEMGREP_IMAGE}" docker run --rm \ -v "${GITHUB_WORKSPACE}:/src" \ -w /src \ -e SEMGREP_SEND_METRICS=off \ --entrypoint semgrep \ - semgrep/semgrep@sha256:2b33f46ba66cf8cc2ad59ccfa7d22951fd00c632c38f1339e84ec8e6e641a942 \ + "${SEMGREP_IMAGE}" \ scan \ --config=p/default \ --severity=WARNING \ diff --git a/AGENTS.md b/AGENTS.md index bd6a96a11..a85d8f72e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,3 +7,4 @@ Materialize accepts only exact SHA-256 pins or a bounded relative `-r` include ( Conflict-scope roots fail closed when the immediate parent directory is a symbolic link. OriginWeave hourly NVIDIA NIM repair is a thin caller at minute 10. See [`docs/doctoring/originweave-hourly-review-caller.md`](docs/doctoring/originweave-hourly-review-caller.md). nonnest2 hourly NVIDIA NIM repair is a thin caller at minute 16. See [`docs/doctoring/nonnest2-hourly-review-caller.md`](docs/doctoring/nonnest2-hourly-review-caller.md). +Semgrep hosted scans bind one job-level `SEMGREP_IMAGE` digest for log evidence and `docker run`. See [`docs/doctoring/semgrep-image-digest-single-source.md`](docs/doctoring/semgrep-image-digest-single-source.md). diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3e2e70b58..ca85fe6da 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -93,6 +93,9 @@ sequenceDiagram - Required review workflows execute **base-branch** scripts. A PR that edits those workflows cannot widen its own `pull_request_target` token. - Reviewer agents stay `edit: deny`. They judge; they do not implement. +- Central Semgrep binds one job-level `SEMGREP_IMAGE` digest for log + evidence, manifest inspect, and `docker run` so buyers can reconstruct + the exact scanner that produced SARIF. - Sandbox helpers copy the workspace, drop secret environment values unless explicitly allowlisted by **name**, and run subprocesses with `shell=False`. - Logs and review receipts redact credential shapes (tokens, bearer values, @@ -122,5 +125,7 @@ trusted `uv` exporter is downloaded from the literal GitHub Releases URL for contract. - [`docs/doctoring/hourly-nvidia-nim-autofix.md`](docs/doctoring/hourly-nvidia-nim-autofix.md) — current increment's repair-worker decision and APA 7th citations. +- [`docs/doctoring/semgrep-image-digest-single-source.md`](docs/doctoring/semgrep-image-digest-single-source.md) + — single-source Semgrep digest for log evidence and `docker run`. - [`docs/doctoring/fast-mlsirm-hourly-review-caller.md`](docs/doctoring/fast-mlsirm-hourly-review-caller.md) — product-specific psychometric repair heartbeat and scientific gates. \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index fd1aebf43..1519ca0d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,10 +35,14 @@ Semantic Versioning where the repository publishes a release. ### Fixed -- Parsed `opencode.jsonc` as JSONC (stripping `//` and `/* */` comments outside string literals) in the reasoning-effort guard and its contract tests, instead of raw `json.loads`, which rejected the file the moment it carried its first explanatory comment (added for the `contextual-orchestrator` provider block) with `Expecting property name enclosed in double quotes`. Comment markers inside string values, such as the `$schema` URL, are left untouched. -- Download the pinned `uv` 0.12.1 exporter from the official GitHub Releases URL instead of `releases.astral.sh`, which now returns HTTP 403 and blocks org-wide OpenCode `coverage-evidence`. The SHA-256 pin is unchanged. The opener may follow one hop onto `release-assets.githubusercontent.com` or `objects.githubusercontent.com` and still rejects every other host, userinfo, non-HTTPS scheme, and nondefault port (ContextualWisdomLab/.github#1109). -- Compared the trusted `uv` executable's post-install `--version` output against the real GitHub Releases build's full string, `uv 0.12.1 (x86_64-unknown-linux-gnu)`, instead of the bare `uv 0.12.1` the prior check required; the genuine release binary always prints the target triple, so every installation was failing the pin check immediately after the archive download itself was fixed (ContextualWisdomLab/.github#1109). -- Excluded relative `-r` and `--requirement` referrers from generated flat base-lock publication while retaining bounded include syntax diagnostics and discovering independently complete direct `.txt` children of `requirements` directories. + - Parsed `opencode.jsonc` as JSONC (stripping `//` and `/* */` comments outside string literals) in the reasoning-effort guard and its contract tests, instead of raw `json.loads`, which rejected the file the moment it carried its first explanatory comment (added for the `contextual-orchestrator` provider block) with `Expecting property name enclosed in double quotes`. Comment markers inside string values, such as the `$schema` URL, are left untouched. + - Download the pinned `uv` 0.12.1 exporter from the official GitHub Releases URL instead of `releases.astral.sh`, which now returns HTTP 403 and blocks org-wide OpenCode `coverage-evidence`. The SHA-256 pin is unchanged. The opener may follow one hop onto `release-assets.githubusercontent.com` or `objects.githubusercontent.com` and still rejects every other host, userinfo, non-HTTPS scheme, and nondefault port (ContextualWisdomLab/.github#1109). + - Compared the trusted `uv` executable's post-install `--version` output against the real GitHub Releases build's full string, `uv 0.12.1 (x86_64-unknown-linux-gnu)`, instead of the bare `uv 0.12.1` the prior check required; the genuine release binary always prints the target triple, so every installation was failing the pin check immediately after the archive download itself was fixed (ContextualWisdomLab/.github#1109). + - Excluded relative `-r` and `--requirement` referrers from generated flat base-lock publication while retaining bounded include syntax diagnostics and discovering independently complete direct `.txt` children of `requirements` directories. + - Bound the central Semgrep job to one `SEMGREP_IMAGE` digest for log evidence, manifest inspection, and `docker run`, so a buyer reconstructing the scan can prove the logged scanner is the scanner that ran. + - Materialized base Python locks only when every package line is an exact SHA-256 pin or a bounded relative `-r`/`--requirement` include. A lone `--require-hashes` directive, a dotted include such as `./lock.txt`, or `-r other-hashes.txt` no longer enters the trusted build context. + - Bound the central Semgrep job to one `SEMGREP_IMAGE` digest for log evidence, manifest inspection, and `docker run`, so a buyer reconstructing the scan can prove the logged scanner is the scanner that ran. + - Materialized base Python locks only when every package line is an exact SHA-256 pin or a bounded relative `-r`/`--requirement` include. A lone `--require-hashes` directive, a dotted include such as `./lock.txt`, or `-r other-hashes.txt` no longer enters the trusted build context. - Refused a conflict-scope repository root whose immediate parent is a symbolic link, so a swapped parent cannot redirect the canonical worktree after the last-component check (CWE-367). - Bounded the Strix quality self-test's deterministic timeout fixtures to 3-second process and 5-second fake-sleep budgets so exact-head policy evidence completes inside the existing job limit without changing production Strix scanner timeouts, providers, credentials, or review semantics. - Allowed commas and ASCII parentheses in the bounded Strix changed-file path policy so legal tracked Packrat fixtures can receive exact-head security analysis, while rejecting raw `..` components before normalization and keeping controls, backslashes, whitespace ambiguity, and shell punctuation fail-closed. diff --git a/CLAUDE.md b/CLAUDE.md index d73a5c169..f14b5df75 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,7 +25,8 @@ This is the ContextualWisdomLab **organization-wide `.github` special repository An organization required-workflow ruleset (`CWL Central required workflows`, id `18156473`) runs Strix, OpenCode Review, and the PR Review Merge Scheduler from this repo in each target repository's context. Repository-local copies of these workflows are drift sources, not - repo-specific contracts. See `README.md` (policy summary) and `PR_GOVERNANCE_AUDIT.md` + repo-specific contracts. Central Semgrep binds one job-level `SEMGREP_IMAGE` + digest for log evidence and `docker run`. See `README.md` (policy summary) and `PR_GOVERNANCE_AUDIT.md` (live audit + per-repo DX/UX transfer decisions). 3. **Infrastructure as code** — `infra/cloudflare/` manages the org's DNS zones and Cloudflare Pages hosting declaratively (`zones.json` + `reconcile.sh`, curl + jq only; dry-run by default, writes diff --git a/docs/doctoring/semgrep-image-digest-single-source.md b/docs/doctoring/semgrep-image-digest-single-source.md new file mode 100644 index 000000000..24b5e7f81 --- /dev/null +++ b/docs/doctoring/semgrep-image-digest-single-source.md @@ -0,0 +1,25 @@ +# Semgrep image digest single source + +## Incident and buyer impact + +The central Semgrep job logged one image tag/digest pair and executed +another. A buyer or auditor reconstructing the scan could not prove the +logged scanner was the scanner that ran. A partial local digest then +surfaced as an ambiguous image-manifest failure instead of a fail-closed +pin error. + +## Decision + +Keep Semgrep OSS 1.169.0 at one job-level `SEMGREP_IMAGE` value. Validate +the complete `semgrep/semgrep@sha256:<64-hex>` form, inspect that exact +manifest, and pass the same value to `docker run`. Scan policy, SARIF +handling, metrics-off, and `--error` remain unchanged. + +## References + +National Institute of Standards and Technology. (2017). *Application +container security guide* (NIST Special Publication 800-190). +https://doi.org/10.6028/NIST.SP.800-190 + +The Linux Foundation. (2023). *SLSA: Supply-chain levels for software +artifacts* (Version 1.0). https://slsa.dev/spec/v1.0/ diff --git a/tests/test_central_required_workflow_ruleset_audit.py b/tests/test_central_required_workflow_ruleset_audit.py index ca7dd4234..8bcbdf854 100644 --- a/tests/test_central_required_workflow_ruleset_audit.py +++ b/tests/test_central_required_workflow_ruleset_audit.py @@ -293,6 +293,18 @@ def test_central_semgrep_filters_source_suppressions_and_gates_on_sarif_results( ) assert "--output=semgrep-results.raw.sarif" in workflow + assert ( + 'SEMGREP_IMAGE: "semgrep/semgrep@sha256:' + "2b33f46ba66cf8cc2ad59ccfa7d22951fd00c632c38f1339e84ec8e6e641a942\"" + ) in workflow + assert ( + workflow.count( + "2b33f46ba66cf8cc2ad59ccfa7d22951fd00c632c38f1339e84ec8e6e641a942" + ) + == 1 + ) + assert '"${SEMGREP_IMAGE}"' in workflow + assert "Verify pinned Semgrep manifest" in workflow assert "Remove explicitly suppressed findings from Semgrep SARIF" in workflow assert ".suppressions // []" in workflow assert "SEMGREP_SUPPRESSED_COUNT" in workflow From 25a8872ade964e5df4fac3b1cf560eb232a3c930 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 18:08:37 +0900 Subject: [PATCH 2/4] fix(semgrep): verify image binding and changelog --- CHANGELOG.md | 2 -- tests/test_central_required_workflow_ruleset_audit.py | 7 ++++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40a3cfd69..4ed2c974f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,9 +40,7 @@ Semantic Versioning where the repository publishes a release. - Compared the trusted `uv` executable's post-install `--version` output against the real GitHub Releases build's full string, `uv 0.12.1 (x86_64-unknown-linux-gnu)`, instead of the bare `uv 0.12.1` the prior check required; the genuine release binary always prints the target triple, so every installation was failing the pin check immediately after the archive download itself was fixed (ContextualWisdomLab/.github#1109). - Excluded relative `-r` and `--requirement` referrers from generated flat base-lock publication while retaining bounded include syntax diagnostics and discovering independently complete direct `.txt` children of `requirements` directories. - Bound the central Semgrep job to one `SEMGREP_IMAGE` digest for log evidence, manifest inspection, and `docker run`, so a buyer reconstructing the scan can prove the logged scanner is the scanner that ran. - - Materialized base Python locks only when every package line is an exact SHA-256 pin or a bounded relative `-r`/`--requirement` include. A lone `--require-hashes` directive, a dotted include such as `./lock.txt`, or `-r other-hashes.txt` no longer enters the trusted build context. - Bound the central Semgrep job to one `SEMGREP_IMAGE` digest for log evidence, manifest inspection, and `docker run`, so a buyer reconstructing the scan can prove the logged scanner is the scanner that ran. - - Materialized base Python locks only when every package line is an exact SHA-256 pin or a bounded relative `-r`/`--requirement` include. A lone `--require-hashes` directive, a dotted include such as `./lock.txt`, or `-r other-hashes.txt` no longer enters the trusted build context. - Refused a conflict-scope repository root whose immediate parent is a symbolic link, so a swapped parent cannot redirect the canonical worktree after the last-component check (CWE-367). - Materialized base Python locks only when every package line is an exact SHA-256 pin or a bounded relative `-r`/`--requirement` include. A lone `--require-hashes` directive, a dotted include such as `./lock.txt`, or `-r other-hashes.txt` no longer enters the trusted build context. - Bounded the Strix quality self-test's deterministic timeout fixtures to 3-second process and 5-second fake-sleep budgets so exact-head policy evidence completes inside the existing job limit without changing production Strix scanner timeouts, providers, credentials, or review semantics. diff --git a/tests/test_central_required_workflow_ruleset_audit.py b/tests/test_central_required_workflow_ruleset_audit.py index 8bcbdf854..413b97dd6 100644 --- a/tests/test_central_required_workflow_ruleset_audit.py +++ b/tests/test_central_required_workflow_ruleset_audit.py @@ -303,7 +303,12 @@ def test_central_semgrep_filters_source_suppressions_and_gates_on_sarif_results( ) == 1 ) - assert '"${SEMGREP_IMAGE}"' in workflow + semgrep_job = workflow.split("\n semgrep:\n", 1)[1] + job_header, steps = semgrep_job.split("\n steps:\n", 1) + assert 'SEMGREP_IMAGE: "semgrep/semgrep@sha256:' in job_header + assert 'echo "Using ${SEMGREP_IMAGE}"' in steps + assert 'docker manifest inspect "${SEMGREP_IMAGE}"' in steps + assert '--entrypoint semgrep \\\n "${SEMGREP_IMAGE}" \\\n' in steps assert "Verify pinned Semgrep manifest" in workflow assert "Remove explicitly suppressed findings from Semgrep SARIF" in workflow assert ".suppressions // []" in workflow From 4c1654e78095acbb70aa4469179346b5d537d8d3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 18:11:00 +0900 Subject: [PATCH 3/4] docs(semgrep): document manifest binding --- AGENTS.md | 2 +- CLAUDE.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 37e370d14..b07efee2b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,6 @@ Conflict-scope roots fail closed when the immediate parent directory is a symbol OriginWeave hourly NVIDIA NIM repair is a thin caller at minute 10. See [`docs/doctoring/originweave-hourly-review-caller.md`](docs/doctoring/originweave-hourly-review-caller.md). nonnest2 hourly NVIDIA NIM repair is a thin caller at minute 16. See [`docs/doctoring/nonnest2-hourly-review-caller.md`](docs/doctoring/nonnest2-hourly-review-caller.md). -Semgrep hosted scans bind one job-level `SEMGREP_IMAGE` digest for log evidence and `docker run`. See [`docs/doctoring/semgrep-image-digest-single-source.md`](docs/doctoring/semgrep-image-digest-single-source.md). +Semgrep hosted scans bind one job-level `SEMGREP_IMAGE` digest for log evidence, manifest inspection, and `docker run`. See [`docs/doctoring/semgrep-image-digest-single-source.md`](docs/doctoring/semgrep-image-digest-single-source.md). The materialization contract is also covered by [`docs/doctoring/exact-artifact-sbom-attestation.md`](docs/doctoring/exact-artifact-sbom-attestation.md). diff --git a/CLAUDE.md b/CLAUDE.md index 79a29e280..5c8703a00 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,10 +26,10 @@ This is the ContextualWisdomLab **organization-wide `.github` special repository Strix, OpenCode Review, and the PR Review Merge Scheduler from this repo in each target repository's context. Repository-local copies of these workflows are drift sources, not repo-specific contracts. Central Semgrep binds one job-level `SEMGREP_IMAGE` - digest for log evidence and `docker run`. See `README.md` (policy summary) and `PR_GOVERNANCE_AUDIT.md` - (live audit + per-repo DX/UX transfer decisions). - `docs/pr-review-and-merge-procedure.md` (bot/agent procedure), and - `PR_GOVERNANCE_AUDIT.md` (live audit + per-repo DX/UX transfer decisions). + digest for log evidence, `docker manifest inspect`, and `docker run`. See + `README.md` (policy summary), `docs/pr-review-and-merge-procedure.md` + (bot/agent procedure), and `PR_GOVERNANCE_AUDIT.md` (live audit + per-repo + DX/UX transfer decisions). 3. **Infrastructure as code** — `infra/cloudflare/` manages the org's DNS zones and Cloudflare Pages hosting declaratively (`zones.json` + `reconcile.sh`, curl + jq only; dry-run by default, writes only on explicit manual `mode = apply`). From adf70162ed6cae853f77a721bab6180066225f89 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 04:28:49 +0900 Subject: [PATCH 4/4] ci: refresh pip audit runtime --- requirements-pip-audit-ci-hashes.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-pip-audit-ci-hashes.txt b/requirements-pip-audit-ci-hashes.txt index ade197a49..0ae099d8f 100644 --- a/requirements-pip-audit-ci-hashes.txt +++ b/requirements-pip-audit-ci-hashes.txt @@ -213,9 +213,9 @@ packaging==26.2 \ # via # pip-audit # pip-requirements-parser -pip==26.1.2 \ - --hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \ - --hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605 +pip==26.2.1 \ + --hash=sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e \ + --hash=sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f # via pip-api pip-api==0.0.34 \ --hash=sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb \