chore(deps): Dependabot digest rotation for pinned Semgrep image [DEVA11Y-476] - #38
Conversation
…VA11Y-476) The Semgrep workflow container image is already pinned by immutable @sha256 digest (PR #15), which is the DEVA11Y-476 chain-breaker for the C-001 chain (DEVA11Y-485: CI image compromise -> mutable main -> SPM plugin RCE). A static digest, however, never receives upstream security patches — the current pin already lags returntocorp/semgrep:latest. Add .github/dependabot.yml with a `docker` ecosystem entry over /.github/workflows so Dependabot bumps the pinned digest to the newest build on a weekly cadence, keeping immutability without freezing the image. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a release-age cooldown so Dependabot does not adopt a freshly-published returntocorp/semgrep digest immediately — a poisoned-image would otherwise be pulled in within a day of publication, the exact window digest-pinning is meant to defend (DEVA11Y-476 / chain DEVA11Y-485). Also resolves the semgrep/ci finding flagging the update config for lacking a minimum release age. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nmnishant-browserstack
left a comment
There was a problem hiding this comment.
Review
The intent is right and the drift is real — I reproduced it (returntocorp/semgrep:latest = sha256:f1f7b71…, last pushed 2026-08-25, vs the pinned sha256:f682953…). But as written this config will not rotate the pin, and it will leave a permanently failing Dependabot run behind. Details and a working alternative below.
🔴 Blocker — Dependabot's docker ecosystem cannot see container: images in workflow files
package-ecosystem: docker + directory: "/.github/workflows" finds zero dependency files here. Two independent confirmations:
1. Upstream doesn't support it yet. dependabot-core#5819 — "Update container image references in GitHub Action workflows" is still open (filed 2022-09-30, last activity 2026-06-03, where a commenter is holding off stalebot with "This remains a feature that I would find very valuable… important as one measure to reduce the risks of supply chain attacks — as well as facilitating the use of some other measures… such as cooldown" — exactly this PR's goal). The github-actions ecosystem only handles uses: with GitHub-repo syntax; docker:// refs and registry URLs are explicitly unsupported.
2. The file fetcher provably skips Semgrep.yml. From dependabot-core/docker/lib/dependabot/docker/file_fetcher.rb and …/shared/shared_file_fetcher.rb:
DOCKER_REGEXP = /dockerfile|containerfile/i # file_fetcher.rb
YAML_REGEXP = /^[^\.].*\.ya?ml$/i # shared_file_fetcher.rb
def likely_kubernetes_resource?(resource)
resource.is_a?(::Hash) && resource.key?("apiVersion") && resource.key?("kind")
end
HELM_REGEXP = /values[\-a-zA-Z_0-9.]*\.ya?ml$/i # utils/helpers.rbSemgrep.yml matches YAML_REGEXP, so it gets fetched — then dropped by correctly_encoded_yamlfiles, because a workflow has no apiVersion/kind and isn't a values*.yaml Helm file. With fetched_files empty, raise_appropriate_error fires:
raise Dependabot::DependencyFileNotFound.new(
File.join(directory, "Dockerfile"),
"No Dockerfiles nor Kubernetes YAML found in #{directory}"
)So the observable outcome isn't a silent no-op — it's a red Dependabot update job every week under Insights → Dependency graph → Dependabot, and no rotation. The green .github/dependabot.yml check on this PR only validates schema, not that any dependency file is discoverable.
The docker ecosystem parses Dockerfiles/Containerfiles, Kubernetes manifests + Helm and Docker Compose — none of which exist in this repo.
Credit where it's due: the two design choices that look wrong are actually correct, so they need no change once discovery is solved. I checked docker/lib/dependabot/docker/update_checker.rb: a bare digest pin with no tag is supported and resolves against latest (fetch_latest_tag → return Tag.new(latest_digest) if version_tag.digest?; latest_digest → digest_of("latest")), which matches the docker manifest inspect returntocorp/semgrep:latest comment in the workflow. And cooldown does gate that path — digest_requirement_up_to_date? has a dedicated branch: "Pure digest pin (no tag): the proposed digest resolves from the latest tag, so gate it on the same cooldown window" → digest_within_cooldown?("latest") using cooldown.default_days. default-days is also the only cooldown key Docker supports (semver-*-days is unsupported for Docker), which is exactly what you used. Also non-issue: returntocorp/semgrep is still actively rebuilt (1.175.0 pushed 2026-08-26) and :latest is digest-identical to semgrep/semgrep:latest, so the deprecated-looking org name is fine.
🟠 The rotation PR would never exercise the new image
if: (github.actor != 'dependabot[bot]')That's the only job in the workflow, so a Dependabot-authored digest bump skips Semgrep entirely — the new image is adopted without ever having been run. Worth fixing under whatever mechanism you land on, since it's the whole safety value of a bump PR. Note the same hole exists for a self-rolled bumper: a PR created with the default GITHUB_TOKEN doesn't trigger workflows at all. Options: allow the actor when the diff only touches the pin, or dispatch the check manually on those PRs.
🟠 The ecosystem entry that is supported, and that this repo needs, is missing
github-actions version updates work today, and the action pins in this repo are far more stale than the Semgrep image — including in the very file this PR is trying to keep current:
| Pin | Committed | Current |
|---|---|---|
actions/checkout@c85c95e (v3.5.3) — Semgrep.yml:38 |
2023-06-09 | v7.0.1 (2026-07-20) |
github/codeql-action/upload-sarif@6c089f5 (v2.20.0) — Semgrep.yml:46 |
2023-06-13 | v4.37.9 |
actions/checkout@11bd719 (v4.2.2) — other 2 workflows |
2024-10-23 | v7.0.1 |
CodeQL Action v2 was retired in January 2025 (changelog) — no updates, no support, "workflows using it may eventually break". checkout@v3 is three majors back. The PR's own argument ("a static pin never receives upstream security patches") applies to these with more force, and here Dependabot can actually act on it:
- package-ecosystem: "github-actions"
directory: "/" # note: "/" for this ecosystem — it searches .github/workflows itself
schedule:
interval: "weekly"
cooldown:
default-days: 7
commit-message:
prefix: "chore(deps)"
labels:
- "dependencies"🟡 Nits
labels: ["dependencies", "security"]— neither label exists in this repo. Dependabot auto-createsdependencies, but per the docs "if any of these labels is not defined in the repository, it is ignored" — sosecuritywill be silently dropped. Create it or drop the line.open-pull-requests-limit: 5is already the default; redundant for a single image.- PR body says "21 lines"; the file is 26.
Suggested path forward
- Replace the
dockerentry with thegithub-actionsentry above — real, supported rotation for pins that are three years stale. - For the Semgrep image, drop auto-rotation and add a drift alarm instead. It needs no write permissions, keeps a human in the loop on what digest gets adopted (better for the C-001 threat model than auto-adopting whatever
latestresolves to), and can't silently stop working. Verified working against this repo's current pin — it correctly reports DRIFTED:
name: Semgrep image pin drift
on:
schedule: [{ cron: '0 7 * * 1' }]
workflow_dispatch:
permissions:
contents: read
jobs:
drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<sha> # v4.2.2
- name: Compare pinned digest against returntocorp/semgrep:latest
run: |
set -euo pipefail
pinned=$(grep -oE 'returntocorp/semgrep@sha256:[0-9a-f]{64}' \
.github/workflows/Semgrep.yml | cut -d@ -f2)
token=$(curl -fsS "https://auth.docker.io/token?service=registry.docker.io&scope=repository:returntocorp/semgrep:pull" | jq -r .token)
latest=$(curl -fsSI -H "Authorization: Bearer $token" \
-H 'Accept: application/vnd.oci.image.index.v1+json' \
-H 'Accept: application/vnd.docker.distribution.manifest.list.v2+json' \
"https://registry-1.docker.io/v2/returntocorp/semgrep/manifests/latest" \
| tr -d '\r' | awk -F': ' 'tolower($1)=="docker-content-digest"{print $2}')
[ "$pinned" = "$latest" ] || {
echo "::error::Semgrep image pin drifted ($pinned != $latest) — bump the digest in .github/workflows/Semgrep.yml"
exit 1; }If you'd rather keep it fully automated, Renovate does handle container:/services: image refs in workflow files, and would cover both the image and the action pins in one tool — heavier to adopt, but it's the one off-the-shelf thing that does what this PR set out to do.
3. Either way, update the DEVA11Y-476 notes: "Dependabot rotates the pin" isn't achievable with Dependabot alone today, and the ticket shouldn't close on a config that can't fire.
Happy to push either variant as a commit on this branch if that's easier.
✨ ai-drafted via Claude — reviewed by Nishant
…grep image Dependabot's `docker` ecosystem cannot discover a `container:` image ref in a workflow file (dependabot-core#5819), so the previous `docker`/`/.github/workflows` config would never rotate the pin and would fail a weekly Dependabot job. Pivot: - dependabot.yml: replace the unusable `docker` entry with a `github-actions` entry (directory "/", weekly, cooldown default-days 7) — real, supported rotation for the action `uses:` pins here, several of which are 3 years stale. Drop the undefined `security` label and the redundant open-pull-requests-limit. Cooldown retained (also clears the semgrep/ci dependabot-missing-cooldown rule). - Add semgrep-image-pin-drift.yml: read-only scheduled/dispatch job that alarms (fails) when the pinned returntocorp/semgrep@sha256 digest drifts from :latest, keeping a human in the loop on the image bump (better for the C-001 threat model than auto-adopting latest). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@nmnishant-browserstack — all addressed in 🔴 docker ecosystem can't see 🟠 image never re-run on a bump / actor guard → moot now: Dependabot no longer bumps the image. Instead added 🟠 github-actions ecosystem missing → that's now the sole Dependabot entry, so 🟡 nits → dropped the undefined DEVA11Y-476 note → corrected in the PR body: closure does not rely on Dependabot rotating the image (it can't); the digest is covered by the drift alarm, action pins by Dependabot. All 9 checks green on ✨ ai-drafted via Claude — reviewed by Rishabh |
nmnishant-browserstack
left a comment
There was a problem hiding this comment.
Review — ca95c3a
The pivot lands the main thing: the docker ecosystem entry is gone, so the blocker from the last round is resolved, and the drift check's happy path genuinely works — I ran it against this branch and it correctly resolves both digests and annotates the drift.
Four things below. Two are cheap defects, one is a claim in the PR body that isn't true, and one is a hole the pivot changed the shape of but didn't close.
🔴 1. cooldown is not supported for github-actions — the block is inert
GitHub's cooldown support table lists Bazel, Bundler, Bun, Cargo, Composer, Conda, Deno, Devcontainers, Docker, Docker Compose, Dotnet SDK, Elm, Gitsubmodule, Gomod, Gradle, Helm, Hex, Julia, Maven, Nix flakes, NPM/Yarn, NuGet, OpenTofu, Pip, pre-commit, Pub, Rust toolchain, sbt, Swift, Terraform, UV, vcpkg. github-actions is absent (options reference). Ironically it was supported on the docker entry you just removed.
The trap is the same one that sank the first attempt: the green .github/dependabot.yml check is schema validation only, so an unsupported-but-well-formed key passes. Net effect — the PR body says "Cooldown retained — also clears the semgrep/ci dependabot-missing-cooldown finding." The finding does clear. The protection doesn't exist. That's strictly worse than not having it, because the alert that would have told you is now silenced.
Not asking you to rip it out — keep the key if it keeps semgrep/ci green, but comment it as ecosystem-inert and fix the claim in the PR body and the DEVA11Y-476 notes, so the next person adding a cooldown-supporting ecosystem doesn't inherit a config that looks protected. If cooldown for actions genuinely matters, Renovate's minimumReleaseAge is the lever that works today.
🔴 2. Both error branches in the drift job are dead code under GHA's bash -e
No shell: is set, so GitHub runs the step as bash -e {0}, and set -uo pipefail doesn't clear that injected -e. pinned=$(grep … | head -n1 | cut …) is a simple command, so a non-matching grep aborts the script before if [ -z "$pinned" ] is reached. Ran it both ways against this branch:
happy path → pinned/latest printed, ::error:: drift annotation, exit 1 ✅
pin removed → exit 1, NO ::error:: annotation at all ❌
registry down → exit 7, NO ::error:: annotation at all ❌
So a malformed Semgrep.yml or a Docker Hub blip produces a bare red square with no explanation — on a scheduled job, where that annotation is the only artifact anyone will ever see. This is exactly the trap you fixed in #37 66249fe, and the same || true capture from sidecar-availability fixes it:
pinned=$(grep -oE '…' .github/workflows/Semgrep.yml | head -n1 | cut -d@ -f2 || true)
token=$(curl -fsS "…" | jq -r .token || true)
latest=$(curl -fsSI … | awk '…' || true)Please add a [ -z "${token:-}" ] guard too — a registry auth failure currently has no diagnostic at all. || true also covers grep | head -n1 exiting 141 on SIGPIPE.
For the record this one is partly mine — the snippet I handed you last round had the same latent issue.
🟠 3. The alarm is red on arrival, and will be red most weeks
Drift exists right now (f682953… vs f1f7b71…), which you noted — so this job fails the first Monday after merge. And returntocorp/semgrep:latest moves roughly weekly: latest pushed 2026-08-25, 1.175.0 on 08-26, 1.174.0 on 08-20. So the cycle is: bump the pin → green for a few days → red again.
An alarm that is already failing when it merges, and returns to failing within a week of every fix, gets muted — and a muted supply-chain alarm is worse than none, because it reads as coverage. Two ways out, and I'd do both:
- Bump the pin in this PR so it lands green. That's also the thing DEVA11Y-476 actually asks for.
- Alarm on staleness, not on difference — fail only when the pinned digest is more than N days old, rather than whenever
latesthas moved at all. "Your pin is 90 days stale" is actionable; "upstream pushed yesterday" is not.
🟠 4. The dependabot[bot] guard isn't moot — it now defeats the entry you just added
The reply says the guard is moot "now that Dependabot no longer bumps the image." But the new github-actions entry bumps actions/checkout@c85c95e (v3.5.3) and github/codeql-action/upload-sarif@6c089f5 (v2.20.0) — and both of those live in Semgrep.yml itself, lines 38 and 46. if: (github.actor != 'dependabot[bot]') sits on that workflow's only job, so every Dependabot PR touching those pins skips semgrep entirely, and the bumped action merges without the workflow it lives in ever having run against it.
That's the same hole I flagged last round — the pivot changed what triggers it, not whether it's there. And it now fires more often, since actions rotate more than the image did. The guard's comment says it's about "permission issues", but the job has had an explicit permissions: block with security-events: write since before this PR — worth checking whether the original problem still exists, and if not, dropping the guard. Otherwise scope it to just the upload-sarif step rather than the whole job.
Checked and fine — no action
dependencieslabel doesn't exist in this repo (labels arebug,documentation,duplicate,enhancement,good first issue,help wanted,invalid,question,wontfix). Unlike thesecuritylabel you dropped,dependenciesis one of Dependabot's default labels and gets auto-created, so this one is safe.directory: "/"is right forgithub-actions.permissions: contents: readon the new workflow — correct, and no write scope anywhere.- Only two distinct action repos across all workflows, so skipping
groups:is fine at this size.
One operational note
GitHub disables scheduled workflows after 60 days of repository inactivity, and a failed scheduled run only emails whoever last edited the cron — there's no team routing here. For a supply-chain alarm meant to outlive whoever wrote it, both are worth at least a comment in the file, and ideally an issue-creating step or a Slack ping.
Verdict
Changes requested (posted as a comment, not a formal block — say the word and I'll convert it). #2 and #4 are cheap and concrete; #1 is a one-line claim correction plus a comment; #3 is a design call I'd like made before this merges rather than after it's been ignored for two months. The core direction — Dependabot for what it can rotate, a drift alarm for what it can't — is right, and the docker-ecosystem blocker is properly resolved.
✨ ai-drafted via Claude — reviewed by Nishant
Resolves Nishant's second review on PR #38: 1. cooldown is inert for the github-actions Dependabot ecosystem — kept only to satisfy Semgrep's dependabot-missing-cooldown rule; documented as inert in dependabot.yml (no real release-age protection for action bumps). 2. Semgrep image drift workflow: made the intended-failure branches reachable under GHA's injected `bash -e` (|| true on the grep/token/latest command substitutions + explicit empty-token guard), same class of fix as #37 66249fe. Verified: happy/stale/pin-removed/registry-down all annotate+exit. 3. Reworked the alarm from "differs from :latest" (red most weeks → muted) to "pinned image older than 45 days" (actionable). Bumped the Semgrep image pin to the current :latest digest so it lands green: f682953… -> f1f7b71861c7b28b6e0f661225a2c4f58a484f5d0f182465c6d6b3b22f972ade (created 2026-08-20, ~8 days old). 4. Removed the job-level `if: github.actor != 'dependabot[bot]'` guard on Semgrep.yml and scoped it to only the privileged upload-sarif step, so Dependabot PRs that bump the action pins living in Semgrep.yml still get scanned; only the security-events:write SARIF upload is skipped for them. Also documented the scheduled-workflow ops caveat (60-day inactivity disable; failed runs notify only the last cron editor) in the drift workflow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@nmnishant-browserstack — all four addressed in 🔴 1. 🔴 2. Drift job dead branches under injected
🟠 3. Red-on-arrival / flapping → both fixes:
🟠 4. Ops note → documented in the drift workflow: scheduled workflows disable after 60 days of repo inactivity and a failed run only notifies the last cron editor, so this alarm is a backstop, not a substitute for the committed pin. Left issue/Slack routing out to keep the job read-only, as you noted. All 9 checks green on ✨ ai-drafted via Claude — reviewed by Rishabh |
nmnishant-browserstack
left a comment
There was a problem hiding this comment.
Review — abc0e1d ✅ Approving
Clean pass over the whole PR, and re-ran every round-2 finding rather than reading the reply. All four are genuinely fixed, and the reworked freshness job works end-to-end against the live registry.
Verified by execution
Happy path — lands green, as intended:
[index] → linux/amd64 child sha256:155f999d…
[config] sha256:32381bb4…
pinned digest: sha256:f1f7b71861c7b28b6e0f661225a2c4f58a484f5d0f182465c6d6b3b22f972ade
image created: 2026-08-20T15:31:17Z (7 days ago)
::notice::Pinned Semgrep image is 7 days old (<= 45); no action needed.
EXIT=0
The multi-arch descent works (the pinned digest is an OCI index, so the .manifests[] | select(linux/amd64) hop is load-bearing, not defensive), the config-blob fetch survives Docker Hub's cross-host redirect to the CDN with the bearer header intact, and the single-arch fallback (imgman="${man}") is the right shape for when it isn't an index.
🔴 2 — bash -e dead branches: fixed. Every intended-failure path now annotates and exits non-zero. This was the one that mattered most, since it's the difference between an actionable alarm and a bare red square:
| case | annotation | exit |
|---|---|---|
pin removed from Semgrep.yml |
::error::Could not find a returntocorp/semgrep@sha256 pin |
1 |
| digest absent from registry | ::error::Could not fetch the manifest for pinned digest… |
1 |
| registry auth unreachable | ::error::Could not obtain a Docker registry auth token… |
1 |
| genuinely stale pin (real 64-day-old digest) | ::error::Pinned Semgrep image is 64 days old (> 45)… |
1 |
I tested the stale case with a real 64-day-old returntocorp/semgrep digest rather than a synthetic clock, so the age arithmetic is confirmed against actual registry metadata.
🔴 1 — inert cooldown: correctly handled. Keeping the key to hold dependabot-missing-cooldown green while documenting it as ecosystem-inert is the right call, and the in-file comment is unambiguous about it providing no release-age protection. PR body and the DEVA11Y-476 note now say the same thing. Nobody inherits a false belief here.
🟠 3 — red-on-arrival: fixed properly, both halves. The pin is bumped to sha256:f1f7b71…, which I re-resolved live — it matches returntocorp/semgrep:latest exactly, so it was re-derived rather than copied from my review. And semgrep/ci is green on this commit, which means the bumped digest is validated as a working CI container, not just a string. Switching the predicate from "differs from :latest" to "older than 45 days" is the substantive fix: 45 days against semgrep's ~weekly rebuild cadence gives roughly a six-week actionable window instead of near-permanent red.
🟠 4 — the Dependabot guard: correctly scoped. Parsed the YAML to confirm rather than eyeballing the diff — job-level if is gone, and always() && github.actor != 'dependabot[bot]' sits on the upload-sarif step alone. That's exactly right: Dependabot PRs get a read-only token, so only the security-events: write step needs skipping, and the scan itself now runs on the PRs that bump checkout/codeql-action — both of which live in this very workflow. The hole is closed.
Also confirmed: all three YAML files parse, all 9 checks green, and the ops caveat (60-day scheduled-workflow disablement, no team routing) is documented in the workflow header with the honest framing that this alarm is a backstop rather than the control itself.
Non-blocking nits — take or leave
- Registry rate limiting can surface as a false alarm. The job now makes four anonymous Docker Hub calls per run (token → index → child manifest → config blob), up from two. GitHub runners share egress IPs and Docker Hub rate-limits anonymous pulls aggressively, so a 429 would land as
::error::…registry unreachable or digest gone— a red alarm that isn't about staleness. Weekly cadence makes this unlikely and the message is at least diagnosable; if it ever flaps, distinguishing 429 from a genuine miss would fix it. - Cosmetic:
Semgrep.ymlnow has a double blank line where the job-levelif:was removed. date -u -dis GNU-only — fine on the pinnedubuntu-latest, just noting it if the runner ever changes.
Verdict: Approve ✅
Three rounds in, this landed somewhere better than where it started. The original premise — "Dependabot rotates the pinned image" — turned out to be impossible, and rather than paper over that, the PR now says plainly what each mechanism does and does not cover: Dependabot rotates what it can (action pins), a freshness alarm backstops what it can't (the container digest), and the cooldown key is documented as decorative. The DEVA11Y-476 notes match reality, which is the part that outlives the diff.
Nice work on re-resolving the digest independently and on testing the failure paths rather than asserting them.
✨ ai-drafted via Claude — reviewed by Nishant
What & why (DEVA11Y-476 / chain DEVA11Y-485)
The image-pin chain-breaker for C-001 is already merged (PR #15) — the Semgrep CI image is pinned to an immutable
@sha256digest. This PR is optional hardening on top: keep pins current without silently rotting.Pivot after review
The original attempt used Dependabot's
dockerecosystem over/.github/workflowsto bump the image digest. That cannot work: Dependabot'sdockerecosystem only discovers Dockerfiles/Containerfiles, Kubernetes manifests, Helm values and Compose files — notcontainer:image refs in workflow files (dependabot-core#5819, still open). It would have produced a weekly failing Dependabot job and never rotated the pin. Reworked into two mechanisms that do work:.github/dependabot.yml→github-actionsecosystem. Rotates the actionuses:pins across the workflows (several are ~3 years stale, e.g.actions/checkout@v3.5.3,codeql-action@v2.20.0— v2 retired Jan 2025).directory: "/", weekly. This is real, supported rotation..github/workflows/semgrep-image-pin-drift.yml(new). Read-only (contents: read) scheduled +workflow_dispatchjob that compares the pinnedreturntocorp/semgrep@sha256digest against:latestvia the Docker registry v2 API and fails on drift, so a human bumps the image digest deliberately after reviewing it. For the C-001 threat model (poisoned upstream tag), human-in-loop is preferable to auto-adopting whateverlatestresolves to. Verified against the current pin — correctly reports DRIFTED today (f682953…vsf1f7b71…).Notes
cooldownis not supported for thegithub-actionsecosystem (only docker/npm/pip/… support it), so it applies no release-age protection to action bumps; it is kept solely to satisfy Semgrep'sdependabot-missing-cooldownrule and is commented as inert in the file. Real cooldown for actions would need Renovate'sminimumReleaseAge.securitylabel (undefined in this repo → silently ignored) and the redundantopen-pull-requests-limit.Semgrep.yml'sif: github.actor != 'dependabot[bot]'guard is left as-is; it's moot now that Dependabot no longer bumps the image.🤖 Generated with Claude Code
Round 2 (
abc0e1d): drift alarm reworked from "differs from:latest" to "pinned image older than 45 days" (actionable, not weekly noise) and madebash -e-safe; Semgrep image pin bumped to the current:latestdigestsha256:f1f7b71861c7b28b6e0f661225a2c4f58a484f5d0f182465c6d6b3b22f972adeso the alarm lands green;Semgrep.ymljob-level dependabot guard scoped to only theupload-sarifstep so Dependabot PRs still get scanned.DEVA11Y-476 note: closure does not rely on Dependabot rotating the container image (it cannot). The image digest is covered by the freshness alarm + manual bump; the action
uses:pins are rotated by Dependabot. Thecooldownkey is inert for github-actions and provides no protection.