Add --verify-shas and --fail-on-warnings#4
Merged
TooFastTooCurious merged 2 commits intomainfrom Apr 15, 2026
Merged
Conversation
Verify that SHA-pinned action references are actually reachable from
their upstream repo's refs. GitHub's fork-network object store means a
workflow pinned to owner/repo@sha will still resolve and execute even
if the SHA originated from a fork, not the claimed repo — defeating the
supply-chain protection that SHA pinning is supposed to provide.
--verify-shas (opt-in) issues HEAD requests to
api.github.com/repos/{owner}/{repo}/commits/{sha} for each unique
SHA-pinned ref and emits a warning on 404. Resolution behavior is
unchanged — ABOM still mirrors GitHub's actual fetch path.
--fail-on-warnings is a generic exit gate chosen over per-warning flags
so future warning categories (deprecated refs, suspicious renames,
etc.) compose without growing CLI surface.
New packages:
- pkg/warnings: Collector for non-fatal runtime diagnostics
- pkg/resolver/verify.go: SHAVerifier interface + GitHub impl
Exit code contract: 0 clean, 1 compromised advisory (existing), 2
warnings-only with --fail-on-warnings. Compromised wins when both
apply. Cmd RunE paths now return a structured exitError so logic is
unit-testable instead of calling os.Exit directly.
Edge cases handled:
- Short SHAs (7-39 hex) warned without API call (ambiguous resolution)
- Transport errors emit RateLimit (advisory), not SHAUnreachable (false positive)
- Mid-run 403/429 emits one warning, suppresses remaining verifications
- Dedup keyed on owner/repo@sha (subdirectory variants collapse)
- Docker and local actions skipped
- --verify-shas --offline / --verify-shas --no-network fail fast
sschuberth
reviewed
Apr 13, 2026
Per sschuberth's review on #4.
digorgonzola
added a commit
to digorgonzola/abom
that referenced
this pull request
Apr 23, 2026
Expand the comment to explain why tag resolution is gated on --check independently of --verify-shas: git ls-remote doesn't consume REST API quota, making it safe to run without explicit opt-in to SHA verification. Addresses PR review item JulietSecurity#4. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TooFastTooCurious
pushed a commit
that referenced
this pull request
Apr 23, 2026
…omised (#8) * fix: resolve SHA-pinned refs to upstream tags before advisory matching SHA-pinned action refs (e.g., `owner/repo@<sha>`) that match an advisory's affected package are currently flagged as compromised with "verify manually", even when the SHA corresponds to a fixed version. This is because SHAs cannot be ordinally compared against version ranges, so `matchAffected` short-circuits with `"verify-sha"`. This change adds a tag resolution step that uses the GitHub API to determine which tag a verified SHA actually points to. Once the upstream tag is known, the advisory checker can compare it against affected version ranges and correctly clear the compromised flag for refs pinned to fixed versions. Changes: - Add `ResolvedTag` field to `ActionRef` model - Add `TagResolver` interface and `GitHubTagResolver` implementation that resolves a SHA to its tag via the GitHub tags API - Add `ResolveABOMTags` function to populate `ResolvedTag` for all SHA-pinned actions after SHA verification - Update `matchAffected` to use `ResolvedTag` for version comparison when available, falling back to `"verify-sha"` only when the tag is unknown - Add `RecheckSHARefs` method to re-evaluate previously flagged SHA refs after tag resolution, clearing false positives - Wire tag resolution and re-checking into the scan pipeline after `--verify-shas` Fixes false positives where `--verify-shas --check` flags SHA-pinned refs at fixed versions (e.g., setup-trivy@<sha> # v0.2.6). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: skip detected-tool match for actions with resolved tag outside affected range The detected-tool pathway in Check() was re-flagging SHA-pinned actions (like setup-trivy) even after matchAffected correctly determined they were outside the affected version range via ResolvedTag. This happened because detected-tool matching only checks tool names, not versions. Also fix tag propagation: CollectActions deduplicates by Raw, so when the same action appears in multiple workflows, only one ActionRef gets ResolvedTag set by ResolveABOMTags. RecheckSHARefs now propagates resolved tags to all duplicate workflow-level refs before rechecking. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: only resolve tags for advisory-flagged SHA refs ResolveABOMTags was calling the GitHub tags API for every SHA-pinned action (~20-30 per repo), when only the 0-2 refs flagged by advisory checking actually need tag resolution. This triggered GitHub's secondary rate limit in CI, causing tag resolution to be skipped entirely and leaving false positives unresolved. Add a Compromised filter to ResolveABOMTags so it only resolves tags for refs that CheckAll flagged with "verify manually". This reduces API calls from ~N to ~0-2, avoiding rate limits in practice. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: run tag resolution before SHA verification to avoid rate limits Move advisory-flagged tag resolution ahead of VerifyABOMShas in the scan pipeline. SHA verification makes ~30 HEAD requests in a tight loop and can trigger GitHub's secondary rate limit, leaving no API budget for the 1-2 tag resolution calls that clear false positives. Also decouple tag resolution from --verify-shas: it now runs whenever --check is used, since advisory false positives need clearing regardless of whether full SHA verification was requested. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use git ls-remote for tag resolution instead of REST API The GitHub REST API tags endpoint shares rate limit quota with all other API calls. In CI environments, the GITHUB_TOKEN budget is often exhausted by other workflow steps or concurrent runs, causing tag resolution to fail with 403 even on its very first request. Switch GitHubTagResolver to use `git ls-remote --tags` which uses the git protocol instead of the REST API. This doesn't consume API rate limit quota and works in CI where git credentials are already configured by actions/checkout. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: clean up dead code and auth path in GitHubTagResolver.ResolveTag Remove fake GIT_ASKPASS_TOKEN env var (not a real git variable), eliminate the confusing cmd.Args rebuild pattern, and set GIT_TERMINAL_PROMPT=0 unconditionally. Document the http.extraHeader trade-off (token visible in process list). Addresses PR review blocker #1. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove unreachable ErrVerifyRateLimit branch from ResolveABOMTags git ls-remote never returns ErrVerifyRateLimit (only GitHubSHAVerifier does, for HTTP 403/429). Remove the dead rateLimited flag and sentinel check, keeping the general error path for actual git ls-remote failures. Addresses PR review blocker #2. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: eliminate duplicate advisory DB load in runScan Hoist the Database instance above both advisory blocks so CheckAll and RecheckSHARefs share a single load. The second NewDatabase call was re-fetching from remote (NoCache: true skips cache, not uses it) and could return a different advisory version than the first. Addresses PR review blocker #3. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: clarify that tag resolution runs with --check, not --verify-shas Expand the comment to explain why tag resolution is gated on --check independently of --verify-shas: git ls-remote doesn't consume REST API quota, making it safe to run without explicit opt-in to SHA verification. Addresses PR review item #4. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove redundant second loop in RecheckSHARefs CollectActions populates abom.Actions from the workflow walk, so every entry is already reached by the first loop. The second loop rechecked the same refs with a misleading comment about unreachable entries. Addresses PR review item #6. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #2.
Adds two new flags that let ABOM detect SHA-pinned action references that aren't actually reachable from their claimed upstream repo — the real class of supply-chain risk SHA pinning is supposed to protect against.
--verify-shas(opt-in) hitsapi.github.com/repos/{owner}/{repo}/commits/{sha}for each unique SHA-pinned reference and emits a warning when the SHA isn't reachable from the claimed repo's refs.--fail-on-warningsexits2if any warnings were emitted. Generic on purpose so future warning categories (deprecated refs, suspicious renames, missing metadata, etc.) compose without growing the CLI surface.Resolution behavior is unchanged. ABOM still mirrors GitHub's actual fetch path;
--verify-shasonly adds advisory signal on top.What
--verify-shasactually catchesA pin like
actions/checkout@<sha>succeeds even when<sha>was only ever committed to a fork. GitHub's object store doesn't care which fork originated the commit — the raw content endpoint serves it regardless. The commits API, scoped toowner/repo, is the first endpoint that distinguishes "reachable from this repo's refs" from "known to the object store."Pipeline integration
Verification runs as a post-pass over the deduplicated action list (not during resolution). Dedup keys on
owner/repo@sha, so subdirectory variants of the same SHA collapse into a single API call.Verifier response handling
Exit code contract
--fail-on-warningsPreviously the only exit signal was
1on compromised actions — existing CI pipelines continue to work identically when neither new flag is set. Exit2is a new signal and only engages when--fail-on-warningsis explicitly requested.CI usage
GITHUB_TOKENis essentially required — anonymous API requests are capped at 60/hour; authenticated requests get 5000/hour. A startup warning fires (and is caught by--fail-on-warnings) when--verify-shasruns without a token.Files changed
Total: ~700 insertions, 22 deletions. No pre-existing tests modified.
Test plan
Automated (all passing locally):
go test ./...— newpkg/warnings(4 tests) andpkg/resolver/verify_test.go(9 tests) plus all pre-existing testsgo vet ./...go build ./...Verify test matrix:
SHAUnreachablewarningowner/repo@shaacross multiple action entries → one API callRateLimitcategory (notSHAUnreachable— avoids false positives)RateLimitwarning, subsequent verifications suppressedManual smoke:
--verify-shas --offline→ fails fast with "--verify-shas requires network"--verify-shas --no-network→ fails fastabom --helplists both new flags plus the exit code contractLive integration against
api.github.comwas not included — anonymous rate limits make it flaky in CI, and the mock-based tests exhaustively cover the response-code matrix. Happy to add a tagged integration test if desired.Design notes for reviewers
pkg/warningspackage instead of attaching toABOM? Warnings are runtime diagnostics against a live API, not a property of the workflows scanned. Keeping them out of the serialized BOM preserves artifact reproducibility. If consumers ever want per-ref status in JSON output, an optionalVerifiedSHAfield onActionRefis easy to add as a follow-up.CollectActions(), so the post-pass avoids redundant verification of the same SHA appearing in multiple composite actions. It also means--verify-shasworks identically underabom checkon a pre-generated ABOM.Collectorcomment makes this assumption explicit — if concurrency is introduced later, the comment forces the decision to be revisited.Follow-ups not included
fmt.Fprintf(os.Stderr, "Warning: ...")sites inpkg/resolver/remote.goand elsewhere still bypass the collector. Migrating them would give--fail-on-warningsuniform coverage. Intentionally deferred to keep this PR focused.ActionRef.VerifiedSHA *boolfield for per-ref status in JSON output. Not added to avoid changing the serialized BOM schema in this PR.