Skip to content

fix(supply-chain): pin scanner images and stop Semgrep scanning the engine - #191

Merged
bogdaniel merged 12 commits into
masterfrom
fix/scanner-image-pinning-and-semgrep-scope
Aug 3, 2026
Merged

bogdaniel merged 12 commits into
masterfrom
fix/scanner-image-pinning-and-semgrep-scope

Conversation

@bogdaniel

@bogdaniel bogdaniel commented Jul 26, 2026 •

Copy link
Copy Markdown
Owner

Closes #85
Closes #86

Stacked on #143 (fix/release-status-source-of-truth) — both PRs touch the same workflow templates, so this branch is based on #143 to keep the diffs conflict-free. Merge #143 first; this PR's own diff is the second commit.

Verified root cause

Two shipped-default defects with the same shape: what a fresh install executes and scans was not what the repository claimed.

#86 — moving scanner image. templates/workflows/sentinel-shield-scheduled.yml and templates/workflows/sentinel-shield-dependency-check.yml both shipped SENTINEL_SHIELD_DEPENDENCY_CHECK_IMAGE: owasp/dependency-check:latest as the default, with a "pin before production" comment. docs/supply-chain-reproducibility.md recorded this as "the one allowed exception … a not-yet-validated placeholder", but docs/scanner-image-digest-pinning.md had already recorded and re-verified a digest for it (sha256:ad169904…cc77b9, resolved 2026-06-10, re-verified 2026-06-15) and the consumer-evidence runs had used it. So a validated digest existed while every adopter kept executing whatever the moving tag pointed at that day.

#85 — Semgrep self-scan. The managed workflows check the engine out at SENTINEL_SHIELD_PATH (tools/sentinel-shield) inside the consumer repository and run Semgrep from the repository root. templates/.semgrepignore excluded that path; profiles/laravel/.semgrepignore and profiles/react/.semgrepignore did not. Worse, and not in the issue text: install-baseline.sh --profile node and --profile hardened-enterprise installed no .semgrepignore at all while both profiles select semgrep as required — verified by installing every profile into a temp dir. So the engine's own scripts, examples and intentionally insecure evidence fixtures were analysed as consumer application code.

Implementation

Approved-image contract (#86)

  • config/scanner-images.json + schemas/scanner-images.schema.json — for every SENTINEL_SHIELD_*_IMAGE: repository, the tag the digest was resolved from, the immutable digest, and whether the shipped default is digest or a readable immutable tag.
  • scripts/validate-scanner-images.sh — contract | templates | registry | show | all. Statically rejects moving tags, unapproved images, drift from the approved reference, and a digest-pinned image downgraded to a tag. registry (opt-in, network) re-resolves each recorded tag and reports drift — it never auto-bumps the contract.
  • Both Dependency-Check templates now ship the digest. The other four images (Semgrep, Gitleaks, Grype, Dockle) keep their immutable version tags as documented, with their digests recorded for the production override.
  • scripts/audits/tool-provenance-audit.sh gained image-mutable-tag and image-digest-drift against the contract, under --require-image-digest — matching that audit's documented contract that a normal run records provenance rather than failing.

Semgrep scope (#85)

  • tools/sentinel-shield/ added to profiles/laravel/.semgrepignore and profiles/react/.semgrepignore; new profiles/node/.semgrepignore; node and hardened-enterprise manifests now install one.
  • doctor.sh reads SENTINEL_SHIELD_PATH from the installed workflows and warns when the ignore file does not exclude it, naming the exact rule to add — so a non-default path gets actionable guidance instead of silence.

Affected contracts and files

Contract Files
New: approved scanner-image contract config/scanner-images.json, schemas/scanner-images.schema.json
New: validator CLI (0 ok / 1 violated / 2 malformed / 3 tool missing) scripts/validate-scanner-images.sh
Shipped scanner image defaults templates/workflows/sentinel-shield-scheduled.yml, templates/workflows/sentinel-shield-dependency-check.yml
Runtime provenance violations (image-mutable-tag, image-digest-drift) scripts/audits/tool-provenance-audit.sh
Installed Semgrep scope profiles/laravel/.semgrepignore, profiles/react/.semgrepignore, new profiles/node/.semgrepignore, profiles/node/profile.manifest.json, profiles/hardened-enterprise/profile.manifest.json
Preflight scripts/doctor.sh
Docs scanner-image-digest-pinning.md, supply-chain-reproducibility.md, dependency-check-runbook.md, semgrep-scoping.md, managed-file-inventory.md
Tests new tests/prod/282-scanner-image-pinning.sh, new tests/prod/283-semgrep-scope.sh, tests/prod/221-scanner-provenance-health.sh, scripts/self-test.sh (syntax tier)

No stable CLI, exit code, environment variable, schema or profile-resolution contract changed. SENTINEL_SHIELD_DEPENDENCY_CHECK_IMAGE remains an override — only its shipped default changed.

Why this stays fail-closed

  • Missing/malformed contract → exit 2; missing jq/curl → exit 3. Never a silent pass.
  • The template sweep fails when it inspects nothing (SWEEP_EMPTY), so coverage cannot be disabled by narrowing it.
  • registry mode treats an unreachable registry as a failure, not a skip — an unverifiable digest is never reported as verified.
  • The Semgrep fix could have been "faked" by excluding everything; 283 asserts consumer application source (app/, src/, resources/js, lib/) is still scanned for every profile.
  • Nothing was converted from failure to success; no || true, no continue-on-error.

Compatibility and migration

  • Adopters who already override the image are unaffected; only the shipped default changed. A readable tag remains valid as a local development override and is documented as such.
  • .semgrepignore is project-owned (create-if-missing) — sync-baseline.sh deliberately does not rewrite an existing consumer file. An existing consumer therefore keeps its old scope until they act; doctor.sh reports it with the one-line fix. This behaviour is asserted by a test, not assumed.
  • New installs of node / hardened-enterprise now receive a .semgrepignore they previously did not (create-if-missing, so an existing file is never touched).

Validation executed

Command Result
make validate PASS
sh scripts/self-test.sh syntax PASS (extended: every shipped ignore template must exclude the checkout)
sh scripts/self-test.sh workflow-sanity PASS
sh scripts/self-test.sh fixtures PASS
sh scripts/self-test.sh install-sync PASS
sh tests/prod/282-scanner-image-pinning.sh PASS (all checks)
sh tests/prod/283-semgrep-scope.sh PASS (all checks)
sh tests/prod/221-scanner-provenance-health.sh PASS (incl. new B3c/B3d/negative-control cases)
sh tests/prod/110-workflows.sh, 120-installer-tx.sh, 150-doctor.sh, 190-hygiene.sh, 220-workflow-runtime-audit.sh, 261-production-security.sh, 271-doc-reference-integrity.sh, 272-profile-tool-integrity.sh PASS
sh scripts/validate-scanner-images.sh all PASS
sh scripts/validate-scanner-images.sh registry PASS — all five recorded digests re-resolved live (owasp/dependency-check:latest, semgrep/semgrep:1.165.0, anchore/grype:v0.114.0, goodwithtech/dockle:v0.4.15, zricethezav/gitleaks:v8.18.4)
sh scripts/validate-release-status.sh all PASS (unchanged by this PR)

Test coverage added — real-repository positives plus isolated fixture negatives: moving tag shipped, drift from the approved reference, digest-pin downgraded to a tag, image absent from the contract, empty sweep, mutable tag approved in the contract, abbreviated digest, tag/resolved_from mismatch, cross-repository reference, missing declared template, malformed/missing contract, unknown mode; and for Semgrep: per-profile install across all nine profiles, a nested insecure engine fixture vs clean consumer source, consumer-source-still-scanned, sync idempotency, a legacy consumer file (negative control) and doctor's detection of it, and a non-default SENTINEL_SHIELD_PATH.

Skipped tests / environmental limitations

  • The live Semgrep scan assertion did not run — semgrep is not installed in this environment, and 283 prints SKIP: and falls back to structural path-matching assertions against the actual installed ignore files. The suite runs the real scan automatically wherever the binary exists.
  • The Dependency-Check engine_version is recorded as null, deliberately. The Docker daemon was unavailable here, so the scanner version behind the digest could not be read from a real run — and it is not guessed. The schema documents it, and the update procedure requires it to be filled from a real run; when non-null, a report whose engine version disagrees is visible drift.
  • Digest resolution used the Docker Hub registry API directly (registry-1.docker.io manifest requests), not docker buildx imagetools, for the same reason. Digests match the previously recorded values exactly.
  • docs/managed-file-inventory.md has no hardened-enterprise section (pre-existing); the node row was updated.

Security considerations

  • Removes a moving upstream dependency from a production-facing execution path: a compromised or incompatible :latest push no longer reaches every new adopter.
  • Evidence becomes reproducible from the workflow source alone — the executed scanner is identified by digest.
  • Two enforcement layers, static (templates) and runtime (what actually executed), so a local override cannot silently become release evidence.
  • The Semgrep fix removes engineered-insecure fixtures from consumer findings, which is a correctness and a signal-integrity issue: real findings were competing with noise from tool source.
  • No new network dependency in any default path — registry mode is opt-in.

Acceptance criteria

#86 — Dependency-Check container immutability

  • No active production/adoption workflow template executes owasp/dependency-check:latest by default — both templates ship the digest; asserted directly against the files in 282, independently of the validator
  • The default image is pinned to a full verified digest — sha256:ad169904…cc77b9, re-resolved live by validate-scanner-images.sh registry
  • Scheduled and dedicated workflows use the same approved image contract — both read from config/scanner-images.json; 282 asserts both files pin the same approved reference
  • A blocking test rejects :latest, unpinned production scanner images, and digest drift — 282 (static, in the blocking prod suite) + 221 (runtime image-mutable-tag / image-digest-drift)
  • The update process verifies the new digest, scanner version, report shape, collector compatibility, cold/warm cache behaviour, and NVD-key secrecy — documented as a six-step reviewed procedure in scanner-image-digest-pinning.md, each step naming the command that proves it
  • Documentation still permits a readable development override while clearly preventing it in production/release evidence — dev override documented; production prevented statically (templates) and at release-authoritative runtime (--require-image-digest)

#85 — Semgrep must not scan the embedded checkout

  • Laravel, Symfony, Node, React, PHP-library and combination installs exclude the embedded checkout — verified by installing all nine profiles and matching the installed ignore file (283); docker is exempt because the resolver shows it does not select Semgrep
  • Consumer application source remains scanned — asserted per profile against app/, src/, resources/js, lib/
  • The behaviour is tested after install-baseline.sh and after sync-baseline.sh — both paths asserted, including that sync does not regress the exclusion
  • A nested Sentinel Shield test/evidence fixture cannot affect the consumer Semgrep count — fixture with an intentionally detectable finding under the checkout plus clean consumer source; structural assertion always, live semgrep scan when the binary exists (skipped here, disclosed above)
  • Changing the configured checkout path either updates the exclusion safely or fails with actionable guidance — doctor.sh reads the path from the installed workflows and names the exact rule to add; asserted in 283
  • Other scanners keep their documented full-tree/dependency behaviour — this change touches only .semgrepignore files and the doctor check; the scope note is restated in semgrep-scoping.md

Summary by CodeRabbit

  • New Features
    • Released engine-only version v2.2.0 with updated governance and quality gate capabilities.
    • Added a recovery/backfill path for publishing when a valid signed tag exists but the GitHub Release was not created.
  • Security
    • Updated CI scanners to use immutable, digest-pinned container images.
    • Strengthened release publishing to require verified tag signatures and fail closed when release notes are missing.
  • Documentation
    • Refreshed release-status, upgrade, support, and operational runbooks to reflect v2.2.0.
  • Governance & Scanning
    • Improved Semgrep scoping/ignores to exclude embedded engine checkouts and merge required ignore rules idempotently without losing project edits.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds v2.2.0 release-status governance, immutable scanner-image contracts, stricter publication checks, Semgrep checkout exclusions, updated workflow references, synchronized documentation, and production validation coverage.

Changes

Release and publication governance

Layer / File(s) Summary
Release-status contract and publication flow
.github/workflows/release-publish.yml, config/release-status.json, schemas/release-status.schema.json, scripts/validate-release-status.sh, scripts/check-release-readiness.sh, tests/prod/281-release-status-contract.sh, tests/prod/297-release-signing-identity.sh
Adds release metadata validation, signed-tag checks, dispatch-based backfill, fail-closed release-note and GitHub Release verification, and regression tests.
Release references and guidance
README.md, CHANGELOG.md, docs/*, templates/workflows/*, examples/laravel-react-docker/*
Updates v2.2.0 release claims, workflow pins, support guidance, rollout documentation, and upgrade instructions.

Scanner reproducibility

Layer / File(s) Summary
Digest-pinned scanner contract
config/scanner-images.json, schemas/scanner-images.schema.json, scripts/validate-scanner-images.sh, scripts/audits/tool-provenance-audit.sh
Defines approved image digests, validates template and registry resolution, and reports mutable-tag or digest-drift violations.
Scanner workflow defaults and tests
.github/workflows/*, templates/workflows/*, examples/laravel-react-docker/.github/workflows/*, tests/prod/221-scanner-provenance-health.sh, tests/prod/282-scanner-image-pinning.sh
Replaces mutable scanner defaults with digest references and adds positive and negative contract coverage.

Semgrep scope

Layer / File(s) Summary
Embedded checkout exclusions
profiles/*/.semgrepignore, profiles/*/profile.manifest.json, profiles/profile.manifest.schema.json, schemas/installation-plan.schema.json, scripts/install-baseline.sh, scripts/sync-baseline.sh, scripts/doctor.sh, scripts/self-test.sh, tests/prod/283-semgrep-scope.sh
Adds managed ignore rules and merge-required-lines support, validates configured paths, and tests installation, synchronization, migration, and custom paths.
Scope and inventory documentation
docs/semgrep-scoping.md, docs/managed-file-inventory.md, tests/prod/268-documentation-accuracy.sh
Documents exclusion behavior and checks profile inventories, release wording, and gate-enforcement claims.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.28% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two primary changes: pinning scanner images and excluding the embedded engine from Semgrep scans.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@bogdaniel

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 26, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 25 minutes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
.github/workflows/ci-security.yml (1)

196-203: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Same stale "pin to a digest ... before production" comment here.

Line 203 already resolves SENTINEL_SHIELD_SEMGREP_IMAGE to a digest by default; the comment above it still reads as if pinning is a future TODO.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci-security.yml around lines 196 - 203, Update the
comments above the Semgrep docker invocation to reflect that the default
SENTINEL_SHIELD_SEMGREP_IMAGE is already pinned to a digest, removing the stale
“pin to a digest before production” TODO while preserving the existing command
and override behavior.
.github/workflows/ci-pipeline.yml (1)

241-246: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale "pin to a digest before production" comment — already digest-pinned.

Line 246 already resolves SENTINEL_SHIELD_SEMGREP_IMAGE to a @sha256:... default; the comment above still frames pinning as a pending production step.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci-pipeline.yml around lines 241 - 246, Update the comment
above the Semgrep docker invocation to reflect that the default
SENTINEL_SHIELD_SEMGREP_IMAGE is already pinned by digest, removing the stale
“before production” wording while preserving the existing -w /src explanation.
🧹 Nitpick comments (9)
scripts/check-release-readiness.sh (1)

300-338: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Avoid mutating positional parameters before the evidence validator.

The release-status block replaces $1/$2/$@ with published --repo-root ..., and the next block then resets them for validate-release-evidence.sh. Use local argument variables (_rs_args=...) or expand the sh argument list directly so each gate's invocation does not depend on the previous block’s $@ state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check-release-readiness.sh` around lines 300 - 338, Keep the
release-status validator invocation in the release-status contract gate from
mutating positional parameters. Replace the set -- "$@" construction around the
published check with a dedicated _rs_args argument list or a direct sh
invocation, including --verify-github when VERIFY_MODE is verify-github, so the
subsequent validate-release-evidence.sh gate retains its original positional
arguments.
scripts/validate-release-status.sh (2)

218-222: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unconditional pass after a possible DECLARED_FILE_MISSING.

Line 222 prints "declared status surfaces and workflow templates exist" even when the loop just recorded failures. Exit code stays correct, but the report contradicts itself.

♻️ Gate the pass line
+	_missing=0
 	for _f in $(qa '.status_surfaces') $(qa '.active_workflow_templates'); do
-		[ -f "$REPO_ROOT/$_f" ] || fail "DECLARED_FILE_MISSING — $_f is declared in the contract but does not exist"
+		[ -f "$REPO_ROOT/$_f" ] || { fail "DECLARED_FILE_MISSING — $_f is declared in the contract but does not exist"; _missing=1; }
 	done
-	pass "declared status surfaces and workflow templates exist"
+	[ "$_missing" -eq 0 ] && pass "declared status surfaces and workflow templates exist"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/validate-release-status.sh` around lines 218 - 222, Gate the success
message after the declared-file validation loop so pass "declared status
surfaces and workflow templates exist" is emitted only when no missing files
were recorded. Preserve the existing fail behavior and exit status for
DECLARED_FILE_MISSING cases.

192-197: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Version ordering ignores pre-release suffixes.

sort -t. -k1,1n -k2,2n -k3,3n compares 2.2.0 and 2.2.0-rc.1 as equal (third field parses numerically as 0), so a superseded pre-release of the current version silently passes the "newer than current" check even though current.stage explicitly allows alpha|beta|rc. Fine for GA-only contracts today; worth tightening if pre-releases are ever recorded in superseded.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/validate-release-status.sh` around lines 192 - 197, Update the
version comparison in the superseded-release validation around CUR_VERSION and
_newest so pre-release suffixes such as alpha, beta, and rc are ordered
distinctly from the corresponding GA version. Preserve the existing failure when
a superseded version is newer than the current release, while ensuring an equal
numeric core with a pre-release suffix does not compare as equal to the GA
release.
tests/prod/281-release-status-contract.sh (1)

278-293: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prose-literal greps make these assertions brittle.

skipping auto-publish and this workflow never creates tags are free-text strings in the workflow; a harmless rewording flips the verdict in both directions (a reworded skip message would silently "pass"). Consider asserting on structural markers (job/step ids, if: conditions) instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/prod/281-release-status-contract.sh` around lines 278 - 293, Replace
the prose-literal grep checks in the release-publish contract test with
structural assertions against stable workflow markers, such as relevant job or
step identifiers and their if: conditions. Ensure the checks reliably verify
publication is not skipped when notes are absent and that the workflow never
creates tags, regardless of message wording; keep the workflow_dispatch
assertion unchanged.
docs/semgrep-scoping.md (1)

26-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Two doc-lint nits in the new section.

Static analysis flags: the fenced block at Line 51 has no language hint (MD040), and "analyses" (Line 36) mixes with "analyze" used elsewhere in the file.

📝 Proposed fix

WARN .semgrepignore does not exclude the configured Sentinel Shield checkout path(s): tools/sentinel-shield

Add a text language hint to the fence at Line 51, and standardize on one spelling variant for "analy{s,z}e(s)" in this file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/semgrep-scoping.md` around lines 26 - 61, Update the fenced warning
block in the “Existing consumers” section to declare the text language for MD040
compliance. In the same section, standardize “analyses” with the
analyze/analyzing spelling used elsewhere in docs/semgrep-scoping.md, without
changing the surrounding guidance.

Source: Linters/SAST tools

.github/workflows/ci-security.yml (2)

367-369: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Same hardcoded-Gitleaks-image inconsistency in security-acceptance-live.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci-security.yml around lines 367 - 369, Update the
Gitleaks Docker image reference in the security-acceptance-live workflow step to
use the repository’s established pinned image consistently, replacing the
differing hardcoded digest in the command under “Gitleaks (JSON →
reports/raw/gitleaks.json)”.

236-238: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Gitleaks image hardcoded with no override variable, unlike the Semgrep step above.

The Semgrep step uses "${SENTINEL_SHIELD_SEMGREP_IMAGE:-semgrep/semgrep@sha256:...}", but Gitleaks is hardcoded directly to zricethezav/gitleaks@sha256:... with no SENTINEL_SHIELD_GITLEAKS_IMAGE fallback, even though that variable is a governed key in config/scanner-images.json.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci-security.yml around lines 236 - 238, Update the
Gitleaks step to use the governed SENTINEL_SHIELD_GITLEAKS_IMAGE environment
variable with the pinned zricethezav/gitleaks image as its fallback, matching
the override pattern used by the Semgrep step and the key defined in
scanner-images.json.
.github/workflows/ci-pipeline.yml (1)

275-277: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Gitleaks image hardcoded, no override variable (unlike the Semgrep step above).

Same inconsistency as ci-security.yml: Semgrep uses ${SENTINEL_SHIELD_SEMGREP_IMAGE:-...}, Gitleaks is a bare hardcoded digest with no SENTINEL_SHIELD_GITLEAKS_IMAGE fallback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci-pipeline.yml around lines 275 - 277, Update the
Gitleaks step in the CI workflow to use a SENTINEL_SHIELD_GITLEAKS_IMAGE
environment override with the existing pinned Gitleaks image as its default,
matching the Semgrep configuration pattern. Keep the Docker invocation and
pinned fallback unchanged.
examples/laravel-react-docker/.github/workflows/sentinel-shield.yml (1)

293-298: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale comment: digest is already pinned.

Line 293's # Pin the image to a digest before production. now contradicts line 295, which already pins Semgrep by digest. Remove or update this leftover comment.

🧹 Proposed fix
-        # Pin the image to a digest before production.
         run: |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/laravel-react-docker/.github/workflows/sentinel-shield.yml` around
lines 293 - 298, Update the comment immediately before the Semgrep docker run
command to reflect that the image is already pinned by digest, or remove the
stale comment; leave the existing digest-pinned image invocation unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci-security.yml:
- Around line 71-74: Update the comment immediately above the container image in
the workflow to state that the Semgrep image is already pinned by digest,
removing the outdated instruction to pin it before production. Leave the
existing digest-pinned image value unchanged.

In `@docs/profile-tool-policy.md`:
- Around line 246-247: Update the capability notes and section headings in
docs/profile-tool-policy.md, including the repeated note near the later section,
to reflect the corrected v2.2 framing: these engine capabilities are additive,
but changed_lines_coverage_violations and focused_test_violations enforce from
baseline in every mode, while architecture_violations blocks from baseline.
Remove the claim that they are off by default or wholly inert after upgrade, and
change the stale v2.1 headings to v2.2.

In `@docs/scanner-image-digest-pinning.md`:
- Around line 126-178: Update the “Context” table in the approved-image contract
section so every shipped scanner default is documented as a digest, including
Semgrep, Gitleaks, Grype, and Dockle. Remove the contradictory
immutable-version-tag wording and describe version tags only as the recorded
source for digest resolution where applicable; keep local tag overrides and
production digest requirements unchanged.

In `@docs/supply-chain-reproducibility.md`:
- Around line 96-101: Update the superseded note in the supply-chain
reproducibility documentation and the §5 wrapper example so they consistently
describe digest-pinned shipped defaults. Replace the literal tag fallback with
the validated digest-pinned reference, while retaining the readable tag only as
metadata or documentation context.

In `@docs/upgrading.md`:
- Around line 59-64: The upgrade documentation must not claim the new gate
families are off by default: update the text around the v2.2.0 compatibility
statement to describe them as additive, including that applicable gate keys
enforce from baseline once evidence exists. In
tests/prod/268-documentation-accuracy.sh lines 225-245, update the
forbidden-phrase check to normalize whitespace and newlines before matching so
hard-wrapped occurrences in documentation are detected.

In `@docs/workflow-template-inventory.md`:
- Around line 7-19: Update the pinning-status documentation in
workflow-template-inventory.md to consistently state that GitHub Action uses
references are already full-SHA pinned. Revise the blanket statement and the
per-template bullets for sentinel-shield.yml, sentinel-shield-pr-fast.yml,
sentinel-shield-main.yml, sentinel-shield-dast.yml, and
sentinel-shield-ai-review.yml to scope remaining consumer pinning requirements
only to container images and SENTINEL_SHIELD_REF, preserving the existing
scanner-image and engine-ref guidance.

In `@examples/laravel-react-docker/docs/security/github-fixture-run.md`:
- Line 73: Update the first-run reference instruction near the
SENTINEL_SHIELD_REF example so it uses v2.2.0 instead of v0.1.0, keeping the
documented workflow consistent with the configured tag.

In `@profiles/hardened-enterprise/profile.manifest.json`:
- Line 9: Update the hardened-enterprise profile manifest’s ignore-file overlay
so it includes the Node/React rules, including .next/, .turbo/, generated/, and
the other relevant build outputs, while retaining the Laravel rules. Prefer a
shared overlay if the repository already provides one, and ensure the resulting
profile applies all required exclusions.

In `@scripts/doctor.sh`:
- Around line 324-361: Escape each configured path in _p before interpolating it
into the grep -E expression used by the .semgrepignore validation, so regex
metacharacters such as the leading dot in .sentinel-shield are matched
literally. Preserve the existing accepted path spellings and per-path degraded
diagnostics in the surrounding Semgrep scope check.

In `@scripts/validate-release-status.sh`:
- Around line 240-260: Update the stale-latest sweep around the git ls-files
enumeration to distinguish an unavailable Git checkout/tool from a genuinely
empty active-document sweep. When Git metadata or the git command is
unavailable, return the documented exit-3 tool/checkout-unavailability result
instead of emitting SWEEP_EMPTY; retain SWEEP_EMPTY only when Git is available
and the active sweep yields no documents.

In `@templates/workflows/sentinel-shield-main.yml`:
- Around line 32-36: Align the scanner image documentation with the default
value in SENTINEL_SHIELD_GRYPE_IMAGE: describe the default as digest-pinned and
remove or revise the guidance that implies readable tags are used by default,
while preserving the existing digest value.

In `@templates/workflows/sentinel-shield-scheduled.yml`:
- Around line 24-31: Update the resolution-date comment in the scheduled
workflow to match the scanner contract’s resolved_at value, 2026-07-26. Keep the
digest-pinned image values and accompanying tag comments unchanged.

In `@templates/workflows/sentinel-shield.yml`:
- Around line 64-74: The scanner image documentation in the workflow is
inconsistent with the existing digest-pinned values. Update the comments near
SENTINEL_SHIELD_SEMGREP_IMAGE and SENTINEL_SHIELD_GITLEAKS_IMAGE, plus the
related Semgrep and Gitleaks step comments in pr-fast and main-gate, to state
that the images are already pinned by digest and remove guidance implying
production pinning remains pending.

In `@tests/prod/268-documentation-accuracy.sh`:
- Around line 199-224: Scope each managed-target lookup in the inventory
validation loop to the documentation section for the current profile `$_pname`,
rather than grepping the entire managed-file-inventory.md. Preserve the existing
profile-presence check and report missing entries using the existing
`${_pname}:${_tgt}` format.

In `@tests/prod/281-release-status-contract.sh`:
- Around line 61-68: Update the active-template assertion in the loop over
active_workflow_templates to accept the same SENTINEL_SHIELD_REF forms as
check_templates, including surrounding quotes and trailing comments. Escape _ref
before embedding it in the grep regular expression so regex metacharacters are
matched literally, while continuing to validate the actual shipped template
files.

---

Outside diff comments:
In @.github/workflows/ci-pipeline.yml:
- Around line 241-246: Update the comment above the Semgrep docker invocation to
reflect that the default SENTINEL_SHIELD_SEMGREP_IMAGE is already pinned by
digest, removing the stale “before production” wording while preserving the
existing -w /src explanation.

In @.github/workflows/ci-security.yml:
- Around line 196-203: Update the comments above the Semgrep docker invocation
to reflect that the default SENTINEL_SHIELD_SEMGREP_IMAGE is already pinned to a
digest, removing the stale “pin to a digest before production” TODO while
preserving the existing command and override behavior.

---

Nitpick comments:
In @.github/workflows/ci-pipeline.yml:
- Around line 275-277: Update the Gitleaks step in the CI workflow to use a
SENTINEL_SHIELD_GITLEAKS_IMAGE environment override with the existing pinned
Gitleaks image as its default, matching the Semgrep configuration pattern. Keep
the Docker invocation and pinned fallback unchanged.

In @.github/workflows/ci-security.yml:
- Around line 367-369: Update the Gitleaks Docker image reference in the
security-acceptance-live workflow step to use the repository’s established
pinned image consistently, replacing the differing hardcoded digest in the
command under “Gitleaks (JSON → reports/raw/gitleaks.json)”.
- Around line 236-238: Update the Gitleaks step to use the governed
SENTINEL_SHIELD_GITLEAKS_IMAGE environment variable with the pinned
zricethezav/gitleaks image as its fallback, matching the override pattern used
by the Semgrep step and the key defined in scanner-images.json.

In `@docs/semgrep-scoping.md`:
- Around line 26-61: Update the fenced warning block in the “Existing consumers”
section to declare the text language for MD040 compliance. In the same section,
standardize “analyses” with the analyze/analyzing spelling used elsewhere in
docs/semgrep-scoping.md, without changing the surrounding guidance.

In `@examples/laravel-react-docker/.github/workflows/sentinel-shield.yml`:
- Around line 293-298: Update the comment immediately before the Semgrep docker
run command to reflect that the image is already pinned by digest, or remove the
stale comment; leave the existing digest-pinned image invocation unchanged.

In `@scripts/check-release-readiness.sh`:
- Around line 300-338: Keep the release-status validator invocation in the
release-status contract gate from mutating positional parameters. Replace the
set -- "$@" construction around the published check with a dedicated _rs_args
argument list or a direct sh invocation, including --verify-github when
VERIFY_MODE is verify-github, so the subsequent validate-release-evidence.sh
gate retains its original positional arguments.

In `@scripts/validate-release-status.sh`:
- Around line 218-222: Gate the success message after the declared-file
validation loop so pass "declared status surfaces and workflow templates exist"
is emitted only when no missing files were recorded. Preserve the existing fail
behavior and exit status for DECLARED_FILE_MISSING cases.
- Around line 192-197: Update the version comparison in the superseded-release
validation around CUR_VERSION and _newest so pre-release suffixes such as alpha,
beta, and rc are ordered distinctly from the corresponding GA version. Preserve
the existing failure when a superseded version is newer than the current
release, while ensuring an equal numeric core with a pre-release suffix does not
compare as equal to the GA release.

In `@tests/prod/281-release-status-contract.sh`:
- Around line 278-293: Replace the prose-literal grep checks in the
release-publish contract test with structural assertions against stable workflow
markers, such as relevant job or step identifiers and their if: conditions.
Ensure the checks reliably verify publication is not skipped when notes are
absent and that the workflow never creates tags, regardless of message wording;
keep the workflow_dispatch assertion unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: da9a62bf-1aee-4e90-a2bc-e2f36798b5e3

📥 Commits

Reviewing files that changed from the base of the PR and between d2f416b and d4c26ea.

📒 Files selected for processing (64)
  • .github/workflows/ci-pipeline.yml
  • .github/workflows/ci-security.yml
  • .github/workflows/release-publish.yml
  • CHANGELOG.md
  • README.md
  • config/release-status.json
  • config/scanner-images.json
  • docs/consumer-onboarding.md
  • docs/dependency-check-runbook.md
  • docs/gate-resolution.md
  • docs/index.md
  • docs/managed-file-inventory.md
  • docs/product-contract.md
  • docs/product-readiness-checklist.md
  • docs/product-status.md
  • docs/production-readiness-audit.md
  • docs/production-release-runbook.md
  • docs/production-rollout.md
  • docs/profile-tool-policy.md
  • docs/regulated-mode-readiness.md
  • docs/roadmap.md
  • docs/scanner-image-digest-pinning.md
  • docs/scanner-normalization.md
  • docs/security-summary-schema.md
  • docs/semgrep-scoping.md
  • docs/sentinel-shield-release-process.md
  • docs/strict-mode-readiness.md
  • docs/supply-chain-reproducibility.md
  • docs/support-policy.md
  • docs/upgrading.md
  • docs/v1-readiness.md
  • docs/v2-merge-commit-ci-evidence.md
  • docs/v2-release-scope.md
  • docs/v2-tracking-issues.md
  • docs/workflow-template-inventory.md
  • examples/laravel-react-docker/.github/workflows/sentinel-shield.yml
  • examples/laravel-react-docker/docs/security/github-fixture-run.md
  • examples/laravel-react-docker/docs/security/sentinel-shield-adoption.md
  • profiles/hardened-enterprise/profile.manifest.json
  • profiles/laravel/.semgrepignore
  • profiles/node/.semgrepignore
  • profiles/node/profile.manifest.json
  • profiles/react/.semgrepignore
  • schemas/release-status.schema.json
  • schemas/scanner-images.schema.json
  • scripts/audits/tool-provenance-audit.sh
  • scripts/check-release-readiness.sh
  • scripts/doctor.sh
  • scripts/self-test.sh
  • scripts/validate-release-status.sh
  • scripts/validate-scanner-images.sh
  • templates/workflows/sentinel-shield-ai-review.yml
  • templates/workflows/sentinel-shield-dast.yml
  • templates/workflows/sentinel-shield-dependency-check.yml
  • templates/workflows/sentinel-shield-main.yml
  • templates/workflows/sentinel-shield-pr-fast.yml
  • templates/workflows/sentinel-shield-scheduled.yml
  • templates/workflows/sentinel-shield.yml
  • tests/prod/170-readiness.sh
  • tests/prod/221-scanner-provenance-health.sh
  • tests/prod/268-documentation-accuracy.sh
  • tests/prod/281-release-status-contract.sh
  • tests/prod/282-scanner-image-pinning.sh
  • tests/prod/283-semgrep-scope.sh

Comment thread .github/workflows/ci-security.yml Outdated
Comment thread docs/profile-tool-policy.md Outdated
Comment thread docs/scanner-image-digest-pinning.md
Comment thread docs/supply-chain-reproducibility.md Outdated
Comment thread docs/upgrading.md
Comment thread templates/workflows/sentinel-shield-main.yml
Comment thread templates/workflows/sentinel-shield-scheduled.yml
Comment thread templates/workflows/sentinel-shield.yml
Comment thread tests/prod/268-documentation-accuracy.sh
Comment thread tests/prod/281-release-status-contract.sh

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/profile-tool-policy.md`:
- Around line 300-302: Update the gate-resolution sentence near “Additive engine
capability” to say “which gates are enforced in which modes,” preserving the
surrounding links and meaning.

In `@profiles/hardened-enterprise/profile.manifest.json`:
- Around line 19-22: Update the manifest entry for
profiles/hardened-enterprise/.semgrepignore so an existing consumer
.semgrepignore is not left unchanged: merge the required tools/sentinel-shield/
exclusion idempotently while preserving user-defined entries, or enforce a
fail-closed behavior when the rule cannot be installed. Keep the
create-if-missing behavior for repositories without an existing file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: af1b299f-3e71-443b-b51b-efba8993ecbb

📥 Commits

Reviewing files that changed from the base of the PR and between d4c26ea and 9ff3360.

📒 Files selected for processing (19)
  • .github/workflows/ci-security.yml
  • docs/managed-file-inventory.md
  • docs/profile-tool-policy.md
  • docs/scanner-image-digest-pinning.md
  • docs/scanner-normalization.md
  • docs/security-summary-schema.md
  • docs/supply-chain-reproducibility.md
  • docs/upgrading.md
  • docs/v2.2.0-known-limitations.md
  • docs/workflow-template-inventory.md
  • examples/laravel-react-docker/docs/security/github-fixture-run.md
  • profiles/hardened-enterprise/.semgrepignore
  • profiles/hardened-enterprise/profile.manifest.json
  • scripts/doctor.sh
  • scripts/validate-release-status.sh
  • templates/workflows/sentinel-shield-scheduled.yml
  • templates/workflows/sentinel-shield.yml
  • tests/prod/268-documentation-accuracy.sh
  • tests/prod/281-release-status-contract.sh
🚧 Files skipped from review as they are similar to previous changes (15)
  • examples/laravel-react-docker/docs/security/github-fixture-run.md
  • docs/security-summary-schema.md
  • templates/workflows/sentinel-shield-scheduled.yml
  • docs/scanner-normalization.md
  • docs/managed-file-inventory.md
  • scripts/doctor.sh
  • templates/workflows/sentinel-shield.yml
  • docs/supply-chain-reproducibility.md
  • tests/prod/268-documentation-accuracy.sh
  • docs/scanner-image-digest-pinning.md
  • .github/workflows/ci-security.yml
  • docs/workflow-template-inventory.md
  • docs/upgrading.md
  • tests/prod/281-release-status-contract.sh
  • scripts/validate-release-status.sh

Comment thread docs/profile-tool-policy.md Outdated
Comment thread profiles/hardened-enterprise/profile.manifest.json Outdated

@bogdaniel bogdaniel left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review verdict: PR-specific delta approved, contingent on the parent stack.

I reviewed this branch against #143 rather than against master, so inherited release/status changes were not counted again. The scanner contract now ships full repository@sha256:… references for all five governed images, templates are covered by a machine-readable contract, malformed/missing contract state fails closed, and the Semgrep profile/install coverage addresses the embedded-checkout scope defect without excluding normal application roots.

No blocking finding in the #191-specific delta. This must still wait for the blocking trust-anchor issue on parent PR #143, and the stack should be rebased/validated in merge order before merge.

GitHub does not allow the connected account to formally approve its own PR, so this approval is recorded as a comment review.

@bogdaniel
bogdaniel force-pushed the fix/scanner-image-pinning-and-semgrep-scope branch from 9ff3360 to d1a242c Compare July 27, 2026 15:57
@bogdaniel

Copy link
Copy Markdown
Owner Author

Two remaining threads:

"Clarify the gate-resolution sentence" — fixed, now reads "for which gates are enforced in which modes" (three docs). Committed locally; it goes out with the next batched push (see the note on review cadence below).

"Ensure the Semgrep exclusion is installed when .semgrepignore already exists" — deliberately not changed, and here is the reasoning so you can push back if you disagree:

create-if-missing is the contract for consumer-owned files. .semgrepignore lives at the consumer's repository root and routinely carries their own rules; silently merging into it — or worse, rewriting it — is precisely what that mode exists to prevent, and sync-baseline is documented as never rewriting it.

The scope guarantee is not lost, it is enforced through a different channel:

  • doctor reports an existing .semgrepignore that does not exclude the configured SENTINEL_SHIELD_PATH as degraded (exit 1), naming the path and the exact line to add — and after this round it accepts every spelling Semgrep honours and emits one suggestion per unexcluded path, so it is actionable rather than noisy.
  • tests/prod/283-semgrep-scope.sh pins that behaviour: a legacy consumer whose ignore file leaves the engine checkout scanned is reported by doctor, and sync-baseline preserving the project-owned file is asserted as the documented behaviour.

So the outcome is "install never touches your file; the gate tells you loudly and blocks at degraded". If you want install to fail closed instead when the exclusion is missing — refusing to complete rather than reporting — say so and I will make that the behaviour for hardened-enterprise specifically; it is a policy change to a documented contract, so I would rather it be an explicit decision than my inference.


On review cadence: noted that every push triggers a fresh review and burns the hourly budget. I am batching from here — one push per round for the whole stack, then waiting for the review rather than pushing incrementally.

@bogdaniel
bogdaniel force-pushed the fix/scanner-image-pinning-and-semgrep-scope branch from c7ef714 to 1539fed Compare July 30, 2026 22:14

@bogdaniel bogdaniel left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new execution sweep closes the undeclared-known-variable fallback, but it is still closed-world and does not yet prove the header's stronger contract that no shipped template executes an unapproved mutable image.

check_templates() loops only over repositories already present in .images. The grep pattern is constructed from each known _erepo, so a newly added inline command such as docker run evil.example/scanner:latest (or any Docker Hub repository absent from the contract) is never discovered. It is not an image declaration, it references no known SENTINEL_SHIELD_*_IMAGE variable, and _seen is already nonzero from legitimate images, so the validator can still pass.

Please derive an execution-image inventory independently of the allowlist—at minimum direct docker/podman run image operands plus workflow container.image / service images and supported action inputs—then require every discovered reference to map to exactly one contract entry and its approved reference. Add a negative fixture containing a completely unknown inline repository and assert templates mode fails. Searching only for already-approved repositories cannot detect a new unapproved one.

@bogdaniel
bogdaniel force-pushed the fix/scanner-image-pinning-and-semgrep-scope branch from 1539fed to a76943e Compare August 1, 2026 13:07
@bogdaniel
bogdaniel force-pushed the fix/scanner-image-pinning-and-semgrep-scope branch from a76943e to f28b888 Compare August 2, 2026 13:06
bogdaniel and others added 12 commits August 2, 2026 19:18
…ngine

Two shipped-default defects with the same shape: what a fresh install
executes and scans was not what the repository claimed.

owasp/dependency-check:latest shipped as the DEFAULT in the scheduled and
dedicated Dependency-Check templates, so the scanner implementation could
change with no repository change and no review, and no evidence run was
reproducible from the workflow source alone.

- config/scanner-images.json (+ schemas/scanner-images.schema.json) is the
  approved-image contract: repository, the tag each digest was resolved
  from, the immutable digest, and whether the shipped default is the digest
  or a readable immutable version tag.
- Both Dependency-Check templates now execute the digest by default.
- scripts/validate-scanner-images.sh rejects moving tags, unapproved
  images, reference drift and digest-pin downgrades; its opt-in `registry`
  mode re-resolves the recorded tags and reports drift without auto-bumping.
- tool-provenance-audit.sh now reports image-mutable-tag and
  image-digest-drift for what a run actually executed.

Semgrep scanned the embedded engine checkout as application code: the
Laravel and React ignore templates never excluded tools/sentinel-shield,
and the node and hardened-enterprise profiles selected semgrep as REQUIRED
while installing no ignore file at all. The engine's own scripts and its
intentionally insecure test fixtures therefore counted as consumer
findings, so a required gate could fail on clean consumer source.

- Every shipped ignore template excludes the checkout; node gains its own
  and hardened-enterprise installs one.
- doctor.sh reports an unexcluded checkout, including a NON-DEFAULT
  SENTINEL_SHIELD_PATH read from the installed workflows, with the exact
  rule to add. .semgrepignore stays project-owned, so sync never rewrites
  an existing consumer file — doctor is the migration signal.
- tests/prod/283-semgrep-scope.sh derives its matrix from the resolved
  profiles, so a new profile cannot reintroduce self-scanning unnoticed.

Closes #85
Closes #86

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…cope

- schemas/scanner-images.schema.json: the per-image constraints sat behind
  `$ref: '#/$defs/image'`, and this repo's jq-based schema validation does not
  resolve $ref — so required/format constraints on every image were
  unenforced at the schema layer. The subschema is inlined.
- doctor.sh: the .semgrepignore exclusion check matched only `path` and
  `path/`, so the other spellings Semgrep honours (`/path`, `path/**`,
  `/path/**`) produced a false WARN on a correctly configured project — a
  check that cries wolf teaches adopters to ignore it. It now accepts every
  spelling and still fails closed on an unrelated exclusion. The remediation
  hint joined several unexcluded paths into one nonsense token ('a b/'); it
  emits one suggestion per path. 283 covers all six spellings, the negative
  control, and the multi-path hint.
- docs: the tag-by-default guidance and the Dependency-Check `latest`
  exception were left standing beside the new digest-by-default contract, so
  a reader could still find instructions contradicting what
  validate-scanner-images.sh enforces. Both are struck through and marked
  superseded in scanner-image-digest-pinning.md and
  supply-chain-reproducibility.md (§4, §8 checklist, References).
- docs/managed-file-inventory.md: hardened-enterprise was missing entirely, so
  its managed .semgrepignore was undocumented. Added, and 268 now asserts that
  every profile manifest AND every managed target it declares appears in the
  inventory, so the next profile cannot ship undocumented.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…make the contract non-bypassable

- config/scanner-images.json + templates: Semgrep, Gitleaks, Grype and
  Dockle shipped as VERSION TAGS. A tag is a mutable registry pointer — the
  publisher can retarget it, so shipped workflows could execute changed
  scanner bytes with no repository change, and `templates` mode only checked
  the reference text while registry drift detection is opt-in. Every shipped
  default is now `repository@sha256:…`; the readable tag survives as
  `resolved_from` and as the trailing comment. The engine's own CI had the
  same problem inline (`${VAR:-semgrep/semgrep:1.165.0}`, a hardcoded
  `zricethezav/gitleaks:v8.18.4`) and is pinned to the same digests.
- tool-provenance-audit.sh: a BARE image reference (no tag, no digest) left
  the tag empty, so the mutable-tag check was skipped for exactly the
  reference Docker resolves to `:latest`. Bare now classifies as `latest`.
- tool-provenance-audit.sh: a MALFORMED contract was collapsed into "no
  contract", which silently switched OFF mutable-tag and digest-drift
  enforcement — corrupting the file was a way to disable the check the file
  exists to perform. Absent is still tolerated; malformed now exits 2.
- validate-scanner-images.sh: a JSON-valid but structurally broken contract
  reached the modes, where a missing `.images` read as "nothing to check".
  Shape is validated before ANY mode. Value rules (full-length digest,
  default_pin enum) stay in `contract` mode so its documented exit-code
  contract is unchanged.
- 283: the Semgrep-scope sweep piped the resolver through `2>/dev/null ||
  printf ''`, so a resolver crash or an unknown profile produced an empty
  policy — which the case below treats as "this profile does not select
  Semgrep", passing it WITHOUT checking its ignore file. Fail-open in the one
  suite whose job is to make self-scanning impossible to reintroduce
  unnoticed. It now fails explicitly on a non-zero resolver exit.
- 282: adds the contract-cannot-be-disabled cases (non-JSON, empty images,
  an image record missing its fields, no mode able to run) plus assertions
  that the schema inlines its per-image constraints and that every shipped
  default is a digest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ks, no stale pinning prose

Code:
- doctor.sh interpolated SENTINEL_SHIELD_PATH straight into an ERE, so the
  leading dot every default path starts with matched ANY character — the
  .semgrepignore exclusion check was more permissive than it claimed and
  would have accepted a subtly wrong ignore line. The path is escaped now.
- validate-release-status.sh reported SWEEP_EMPTY (a contract violation, exit
  1) when `git ls-files` returned nothing — which is what happens in a
  tarball or a vendored copy. A missing tool is exit 3 by the documented
  contract; the repository is no longer blamed for the absence of git.
- 281 required the BARE ref literal in every template while the validator
  strips quotes and trailing comments (its own t4 case codifies the quoted
  form), so a legitimately quoted template would have failed the suite. The
  ref is also escaped, for the same ERE reason as above.
- 268's per-target inventory check searched the WHOLE document, so a missing
  row passed because the same target name appears under other profiles. It
  now slices the profile's own section — verified by deleting the
  hardened-enterprise .semgrepignore row, which the scoped check catches and
  the unscoped one did not.
- 268's stale-claim sweep matched one exact phrasing, so
  "(engine-only; off by default in existing modes)" and "additive, off by
  default in existing modes" walked straight past it. The pattern now covers
  every phrasing, and five more docs are corrected.

Profiles:
- hardened-enterprise extends BOTH the Laravel and Node stacks but shipped
  the Laravel-only ignore file, so Semgrep scanned .next/, .turbo/,
  generated/ and *.generated.* as application code. It now ships its own
  ignore file carrying both scopes.

Prose that contradicted the digest-pinning change in the same PR: the
ci-security image comment, the template header comments, the resolution date
(now taken from the contract's own resolved_at), the pinning-status paragraph
in the template inventory, the shipped-default table, the supersession note in
supply-chain-reproducibility, the v0.1.0 first-run reference in the example,
and the upgrade guidance in upgrading.md / v2.2.0-known-limitations.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
'for which enforce in which mode' was grammatically incomplete; it now reads
'for which gates are enforced in which modes'.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…gnore file

CodeRabbit review follow-up on the scanner-pinning / Semgrep-scope work.

`.semgrepignore` shipped as `create-if-missing`, so a project that adopted before the
exclusion existed kept its old file and NEVER received `tools/sentinel-shield/`. Semgrep then
scanned the embedded engine checkout as application code and the scope guarantee — the whole
point of the file — was silently lost. `doctor.sh` reported the gap, but a warning does not
close it.

New manifest mode `merge-required-lines`: the file stays project-owned (nothing is
overwritten, reordered or removed) and every non-comment line of the shipped template is
treated as REQUIRED — the missing ones are appended under a marker comment. Matching is on
exact rule text, so the merge is idempotent. `install-baseline.sh` still only creates
(and points at the sync when the file already exists); `sync-baseline.sh` performs the merge.
Applied to the five profiles that ship a `.semgrepignore`.

Also from the same review:
- tool-provenance-audit: validate the per-image RECORDS, not just the containers.
  `{"images":{"x":{}},"mutable_tags":[]}` used to pass and then left every lookup empty —
  an unenforceable contract that read as an enforced one. Now matches
  validate-scanner-images.sh: non-empty mutable tags, and a sha256 digest on every record.
- tool-provenance-audit: a tag can only live in the FINAL path component. Matching `*:*`
  against the whole reference read the REGISTRY PORT of `registry.internal:5000/dep-check`
  as a tag, so that bare reference dodged the implicit-`latest` rule.
- self-test: the cheap ignore-template guard skipped profiles/hardened-enterprise.
- docs: MD028 blank line inside the historical blockquote; `analyses` -> `analyzes`;
  fenced block language.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…guidance

CodeRabbit review follow-up.

- scanner-image-digest-pinning.md: the retained v0.1.28 block still read as operational advice —
  a table recommending readable tags for development and a bullet saying default templates stay
  tag-based. Under the v2.2+ approved-image contract no shipped template may execute a mutable
  tag in any context, so a reader could copy an insecure default out of a section meant to be a
  historical record. The block is now explicitly framed as history and struck through.
- v2.2.0-known-limitations.md: the document warns that several gates block from `baseline` and
  from every mode, then closed with "Existing mode defaults are unchanged: a project that
  upgrades and changes nothing sees no new blocking gate" — the opposite guarantee, in the
  authoritative limitations document referenced by GATE 10. Corrected to match the matrix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CodeRabbit review follow-up.

`sync-baseline.sh` exits 2 without `--target`, so the hint the installer printed —
`run sync-baseline.sh --apply` — failed before it could merge anything. The installer knows
both the target and the profile, so it now prints the full command, and the quickstart
troubleshooting row does the same. Verified by running the printed command verbatim: it merges
the required rules into the existing project file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CodeRabbit review follow-up.

- validate-scanner-images.sh: `pass "declared templates exist"` ran unconditionally right after
  the loop that checks them, so a report could carry both DECLARED_TEMPLATE_MISSING and
  "declared templates exist" — a reader scanning for the pass line would read a broken contract
  as satisfied. Now conditional, matching check_templates(). Confirmed with a contract
  declaring a template that does not exist: previously both lines appeared, now only the
  failure.
- tool-provenance-audit.sh: the contract check accepted any digest STARTING with `sha256:`, so
  a truncated or malformed digest passed structural validation and was then compared against a
  resolved digest it could never match. Requires the full `sha256:` + 64 lowercase hex form.
- config/scanner-images.json: the Semgrep rationale said "templates keep the readable form for
  legibility; production pins the digest" and then "shipped default is the DIGEST" in the same
  paragraph. The first half described the superseded policy; removed.
- sentinel-shield-main.yml: the comment above the image block still advertised readable tags as
  the default with digests as the production override. It is the other way round.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…one declared

CodeRabbit flagged a mutable Gitleaks tag shipping in two workflow templates. The tag is
the symptom; this is the reason the contract validator never saw it.

`check_templates` swept `SENTINEL_SHIELD_*_IMAGE: <value>` ASSIGNMENT lines only. A template
does not execute a declaration, it executes a command, and an image named inline — as a
literal or as a `${VAR:-default}` fallback — matched nothing in that sweep. The file header
has promised "no template executes a mutable tag" since it was written; nothing enforced it.

Three fixes, each with a negative control in tests/prod/282:

1. EXECUTION sweep. Every reference to an approved repository anywhere in a declared
   template (whole-line comments excluded — those are documentation) must equal the approved
   reference. A moving tag reports MUTABLE_TAG_EXECUTED, anything else IMAGE_DRIFT_EXECUTED.
   Reading a contract variable the template never declares is IMAGE_VAR_UNDECLARED: the
   inline fallback is what runs, and the declaration sweep sees nothing to check.

2. `ref_tag` parsed the tag as the text after the LAST ':' in the whole reference, so a
   ported registry — registry.example.com:5000/team/img — was read as carrying the tag
   '5000/team/img'. The separator is only a ':' after the last '/'.

3. `[ "$_decl_ok" -eq 1 ] && pass …` was the last command in check_contract, making the
   function's exit status the test's. Under `set -eu` a missing declared template killed the
   script there: no summary, and in `all` mode the template sweep never ran — the run
   reported strictly less than it had already found.

The sweep count over the real tree goes 8 -> 18 inspected references; all pass. The two
templates that actually carry the moving tag are fixed on the branch that introduced them.
…rift in the known

Both existing sweeps start FROM the approved contract and look for those repositories in the
template. That can only ever find drift in an image we already approved. An entirely unknown
image has no repository pattern to search for, so this passed validation cleanly:

    docker run --rm -v "$PWD:/repo" evilcorp/backdoor:latest

An allowlist that cannot reject an unlisted entry is not an allowlist.

The OPERAND sweep runs the other way round: take every image the template actually executes
and require it to BE an approved reference. It reads both shapes that carry one — the operand
of `docker run` (after its flags and the values they take) and a job/service `image:` key —
and reports `IMAGE_NOT_APPROVED` for an unknown literal and `IMAGE_VAR_UNKNOWN` for an image
chosen by a variable that is not in the contract at all.

Running it immediately found a real one: the engine's own CI executes `rhysd/actionlint:1.7.7`
in ci-self-test.yml and ci-security.yml. Two other workflows already pinned that image by
digest, so this was inconsistent as well as mutable. actionlint is now in the approved contract
(digest resolved from the registry: sha256:887a2599…147e9, matching what the pinned workflows
already used) and both bare-tag invocations are pinned.

`repo:tag@sha256:X` is accepted as equivalent to `repo@sha256:X`. When both are present the
DIGEST is what the daemon resolves, so the reference is immutable and the tag is a readability
annotation; rejecting that spelling would have pushed the two already-correct workflows to
look wrong.

A bare `$VAR` operand is skipped rather than treated as the image: in these templates it holds
flags (`$MOUNTS` expands to several -v pairs). If one ever held an image, the token after it
would be read and reported as unapproved — noisy, but the fail-closed direction.

tests/prod/282 covers all three new refusals plus the digest-form acceptance. One existing
assertion moved from "exactly one IMAGE_DRIFT_EXECUTED" to "at least one": both sweeps now
legitimately report that fixture, which is duplicate noise rather than a wrong answer.
Found while checking that the shared normaliser preserves every supported reference form, as
the review asked. It did not.

`${ref%%:*}` cuts at the FIRST colon, which on a ported registry is the PORT:

    registry.example.com:5000/team/img:v1@sha256:bbbb
      -> registry.example.com@sha256:bbbb        (before)
      -> registry.example.com:5000/team/img@sha256:bbbb   (now)

That discarded the repository path entirely, so every image on a given ported host
canonicalised to the same string — two different private-registry images would have compared
equal against the approved contract. Consolidating three sweeps onto one parser is only safer
if the parser is right; this is the failure mode that consolidation creates.

The tag separator is the `:` after the last `/`, the same rule ref_tag already uses, and a tag
is only stripped when the reference carries a digest to fall back on.

tests/prod/282 now pins all seven forms — tag-only, digest-only, tag+digest, ported registry in
each of those shapes, and a bare repository — plus the property that actually matters: two
different images on one ported host do not canonicalise to the same string.

This branch was previously deployed

1 inactive deployment
production — fb25efac Deployed Aug 3, 2026 by bogdaniel via release-gate #523
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant