Repository navigation
fix(supply-chain): pin scanner images and stop Semgrep scanning the engine - #191
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe 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. ChangesRelease and publication governance
Scanner reproducibility
Semgrep scope
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
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. Comment |
3f92cbc to
cdfd128
Compare
|
@coderabbitai full review |
✅ Action performedFull 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. |
cdfd128 to
62e4b8c
Compare
62e4b8c to
d4c26ea
Compare
There was a problem hiding this comment.
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 winSame stale "pin to a digest ... before production" comment here.
Line 203 already resolves
SENTINEL_SHIELD_SEMGREP_IMAGEto 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 winStale "pin to a digest before production" comment — already digest-pinned.
Line 246 already resolves
SENTINEL_SHIELD_SEMGREP_IMAGEto 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 valueAvoid mutating positional parameters before the evidence validator.
The release-status block replaces
$1/$2/$@withpublished --repo-root ..., and the next block then resets them forvalidate-release-evidence.sh. Use local argument variables (_rs_args=...) or expand theshargument 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 valueUnconditional
passafter a possibleDECLARED_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 valueVersion ordering ignores pre-release suffixes.
sort -t. -k1,1n -k2,2n -k3,3ncompares2.2.0and2.2.0-rc.1as equal (third field parses numerically as0), so a superseded pre-release of the current version silently passes the "newer than current" check even thoughcurrent.stageexplicitly allowsalpha|beta|rc. Fine for GA-only contracts today; worth tightening if pre-releases are ever recorded insuperseded.🤖 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 valueProse-literal greps make these assertions brittle.
skipping auto-publishandthis workflow never creates tagsare 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 winTwo 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
textlanguage 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 winSame 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 winGitleaks 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 tozricethezav/gitleaks@sha256:...with noSENTINEL_SHIELD_GITLEAKS_IMAGEfallback, even though that variable is a governed key inconfig/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 winGitleaks 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 noSENTINEL_SHIELD_GITLEAKS_IMAGEfallback.🤖 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 winStale 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
📒 Files selected for processing (64)
.github/workflows/ci-pipeline.yml.github/workflows/ci-security.yml.github/workflows/release-publish.ymlCHANGELOG.mdREADME.mdconfig/release-status.jsonconfig/scanner-images.jsondocs/consumer-onboarding.mddocs/dependency-check-runbook.mddocs/gate-resolution.mddocs/index.mddocs/managed-file-inventory.mddocs/product-contract.mddocs/product-readiness-checklist.mddocs/product-status.mddocs/production-readiness-audit.mddocs/production-release-runbook.mddocs/production-rollout.mddocs/profile-tool-policy.mddocs/regulated-mode-readiness.mddocs/roadmap.mddocs/scanner-image-digest-pinning.mddocs/scanner-normalization.mddocs/security-summary-schema.mddocs/semgrep-scoping.mddocs/sentinel-shield-release-process.mddocs/strict-mode-readiness.mddocs/supply-chain-reproducibility.mddocs/support-policy.mddocs/upgrading.mddocs/v1-readiness.mddocs/v2-merge-commit-ci-evidence.mddocs/v2-release-scope.mddocs/v2-tracking-issues.mddocs/workflow-template-inventory.mdexamples/laravel-react-docker/.github/workflows/sentinel-shield.ymlexamples/laravel-react-docker/docs/security/github-fixture-run.mdexamples/laravel-react-docker/docs/security/sentinel-shield-adoption.mdprofiles/hardened-enterprise/profile.manifest.jsonprofiles/laravel/.semgrepignoreprofiles/node/.semgrepignoreprofiles/node/profile.manifest.jsonprofiles/react/.semgrepignoreschemas/release-status.schema.jsonschemas/scanner-images.schema.jsonscripts/audits/tool-provenance-audit.shscripts/check-release-readiness.shscripts/doctor.shscripts/self-test.shscripts/validate-release-status.shscripts/validate-scanner-images.shtemplates/workflows/sentinel-shield-ai-review.ymltemplates/workflows/sentinel-shield-dast.ymltemplates/workflows/sentinel-shield-dependency-check.ymltemplates/workflows/sentinel-shield-main.ymltemplates/workflows/sentinel-shield-pr-fast.ymltemplates/workflows/sentinel-shield-scheduled.ymltemplates/workflows/sentinel-shield.ymltests/prod/170-readiness.shtests/prod/221-scanner-provenance-health.shtests/prod/268-documentation-accuracy.shtests/prod/281-release-status-contract.shtests/prod/282-scanner-image-pinning.shtests/prod/283-semgrep-scope.sh
There was a problem hiding this comment.
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
📒 Files selected for processing (19)
.github/workflows/ci-security.ymldocs/managed-file-inventory.mddocs/profile-tool-policy.mddocs/scanner-image-digest-pinning.mddocs/scanner-normalization.mddocs/security-summary-schema.mddocs/supply-chain-reproducibility.mddocs/upgrading.mddocs/v2.2.0-known-limitations.mddocs/workflow-template-inventory.mdexamples/laravel-react-docker/docs/security/github-fixture-run.mdprofiles/hardened-enterprise/.semgrepignoreprofiles/hardened-enterprise/profile.manifest.jsonscripts/doctor.shscripts/validate-release-status.shtemplates/workflows/sentinel-shield-scheduled.ymltemplates/workflows/sentinel-shield.ymltests/prod/268-documentation-accuracy.shtests/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
bogdaniel
left a comment
There was a problem hiding this comment.
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.
9ff3360 to
d1a242c
Compare
|
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
The scope guarantee is not lost, it is enforced through a different channel:
So the outcome is "install never touches your file; the gate tells you loudly and blocks at 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. |
c7ef714 to
1539fed
Compare
bogdaniel
left a comment
There was a problem hiding this comment.
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.
1539fed to
a76943e
Compare
a76943e to
f28b888
Compare
…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.
f28b888 to
fb25efa
Compare
Closes #85
Closes #86
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.ymlandtemplates/workflows/sentinel-shield-dependency-check.ymlboth shippedSENTINEL_SHIELD_DEPENDENCY_CHECK_IMAGE: owasp/dependency-check:latestas the default, with a "pin before production" comment.docs/supply-chain-reproducibility.mdrecorded this as "the one allowed exception … a not-yet-validated placeholder", butdocs/scanner-image-digest-pinning.mdhad 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/.semgrepignoreexcluded that path;profiles/laravel/.semgrepignoreandprofiles/react/.semgrepignoredid not. Worse, and not in the issue text:install-baseline.sh --profile nodeand--profile hardened-enterpriseinstalled no.semgrepignoreat all while both profiles selectsemgrepas 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 everySENTINEL_SHIELD_*_IMAGE: repository, the tag the digest was resolved from, the immutable digest, and whether the shipped default isdigestor a readable immutabletag.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.scripts/audits/tool-provenance-audit.shgainedimage-mutable-tagandimage-digest-driftagainst 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 toprofiles/laravel/.semgrepignoreandprofiles/react/.semgrepignore; newprofiles/node/.semgrepignore;nodeandhardened-enterprisemanifests now install one.doctor.shreadsSENTINEL_SHIELD_PATHfrom 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
config/scanner-images.json,schemas/scanner-images.schema.jsonscripts/validate-scanner-images.shtemplates/workflows/sentinel-shield-scheduled.yml,templates/workflows/sentinel-shield-dependency-check.ymlimage-mutable-tag,image-digest-drift)scripts/audits/tool-provenance-audit.shprofiles/laravel/.semgrepignore,profiles/react/.semgrepignore, newprofiles/node/.semgrepignore,profiles/node/profile.manifest.json,profiles/hardened-enterprise/profile.manifest.jsonscripts/doctor.shscanner-image-digest-pinning.md,supply-chain-reproducibility.md,dependency-check-runbook.md,semgrep-scoping.md,managed-file-inventory.mdtests/prod/282-scanner-image-pinning.sh, newtests/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_IMAGEremains an override — only its shipped default changed.Why this stays fail-closed
jq/curl→ exit 3. Never a silent pass.SWEEP_EMPTY), so coverage cannot be disabled by narrowing it.registrymode treats an unreachable registry as a failure, not a skip — an unverifiable digest is never reported as verified.283asserts consumer application source (app/,src/,resources/js,lib/) is still scanned for every profile.|| true, nocontinue-on-error.Compatibility and migration
.semgrepignoreis project-owned (create-if-missing) —sync-baseline.shdeliberately does not rewrite an existing consumer file. An existing consumer therefore keeps its old scope until they act;doctor.shreports it with the one-line fix. This behaviour is asserted by a test, not assumed.node/hardened-enterprisenow receive a.semgrepignorethey previously did not (create-if-missing, so an existing file is never touched).Validation executed
make validatesh scripts/self-test.sh syntaxsh scripts/self-test.sh workflow-sanitysh scripts/self-test.sh fixturessh scripts/self-test.sh install-syncsh tests/prod/282-scanner-image-pinning.shsh tests/prod/283-semgrep-scope.shsh tests/prod/221-scanner-provenance-health.shsh 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.shsh scripts/validate-scanner-images.sh allsh scripts/validate-scanner-images.sh registryowasp/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 allTest 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_frommismatch, 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-defaultSENTINEL_SHIELD_PATH.Skipped tests / environmental limitations
semgrepis not installed in this environment, and283printsSKIP: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.engine_versionis recorded asnull, 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.registry-1.docker.iomanifest requests), notdocker buildx imagetools, for the same reason. Digests match the previously recorded values exactly.docs/managed-file-inventory.mdhas nohardened-enterprisesection (pre-existing); thenoderow was updated.Security considerations
:latestpush no longer reaches every new adopter.registrymode is opt-in.Acceptance criteria
#86 — Dependency-Check container immutability
owasp/dependency-check:latestby default — both templates ship the digest; asserted directly against the files in282, independently of the validatorsha256:ad169904…cc77b9, re-resolved live byvalidate-scanner-images.sh registryconfig/scanner-images.json;282asserts both files pin the same approved reference:latest, unpinned production scanner images, and digest drift —282(static, in the blocking prod suite) +221(runtimeimage-mutable-tag/image-digest-drift)scanner-image-digest-pinning.md, each step naming the command that proves it--require-image-digest)#85 — Semgrep must not scan the embedded checkout
283);dockeris exempt because the resolver shows it does not select Semgrepapp/,src/,resources/js,lib/install-baseline.shand aftersync-baseline.sh— both paths asserted, including that sync does not regress the exclusionsemgrepscan when the binary exists (skipped here, disclosed above)doctor.shreads the path from the installed workflows and names the exact rule to add; asserted in283.semgrepignorefiles and the doctor check; the scope note is restated insemgrep-scoping.mdSummary by CodeRabbit