Pular para conteúdo

AUR-362 — Fixar scanners de segurança

Documento histórico do board. Pode descrever componentes removidos; para uso atual, consulte o guia do produto.

Purpose

Pins the identity, version/digest, versioned rule base, finding format and offline/egress behavior of the four security scanner categories this reconstruction's security office intends to invoke in later cards: a secrets scanner, a dependency/vulnerability scanner, a static-application-security (SAST) scanner and a shell-script linter. tests/acceptance/AUR-362.sh verifies .board/bootstrap/locks/scanners.yml exists as a plain (non-symlink) file within its byte bound, declares the bootstrap-lock-v1 schema line, and parses each of its 46 fields against a ground-truth table — every pinned image digest, every rule-base identity, and every finding-format CLI contract — while also cross-checking .github/workflows, Dockerfile and .docker/docs.Dockerfile for a scanner tool the lock does not carry an entry for. It does all of this without running any scanner. Thirty-four of the 46 fields' ground truth is carried directly in the acceptance script, resolved live against each tool's release API / registry digest as recorded below; the twelve *_finding_format_* fields' ground truth is instead read from a separately-versioned, separately-hashed fixture, tests/bootstrap/locks/AUR-362/ground-truth/cli-format-fields.tsv, mechanically extracted from live pinned-digest CLI transcripts captured in tests/bootstrap/locks/AUR-362/ground-truth/RAW_CAPTURE.txt (see "Ground truth for the finding-format fields" below) — this split exists because a round-2 adversarial review found three of those twelve values wrong in both the lock and the script's own hand-typed literal at once, which a single-file, single-author "independently carried" table cannot catch by construction: a lock value and its own oracle can go wrong together when both are typed by the same hand from the same mistaken belief.

At the time this card was authored, none of .github/workflows/, Dockerfile or .docker/docs.Dockerfile (this card's read_paths) invoke any security scanner: this repository does not yet run secrets, vulnerability, SAST or shell-lint scanning anywhere. This card therefore does not derive its pins from an existing in-repo invocation (there is none to derive from); it fixes, from each tool's own primary source (its GitHub release and its Docker Hub / GHCR image, resolved live against the network available in this workspace), the exact identity every later card that actually wires a scanner into a workflow must reproduce byte-for-byte. Non-goal, restated from the card: this lock does not execute a scanner, produce a finding, or triage a vulnerability.

Artifacts and schema

.board/bootstrap/locks/scanners.yml

Flat key: value document, LC_ALL=C, one pair per line, no comments. First line is exactly schema: bootstrap-lock-v1. Four scanner blocks, one per category, sharing the same 11-field shape (scanner_name, scanner_role, scanner_version, scanner_image, rulebase_id, a rule base identity field, finding_format_flag, finding_format_default, finding_format_values, offline_mode [+ offline_flag where the tool needs one], egress), each under a category prefix (secrets_, vuln_, sast_, shell_).

Field (per prefix) Meaning
<prefix>_scanner_name The tool's canonical name.
<prefix>_scanner_role One of secrets, vulnerabilities, sast, shell.
<prefix>_scanner_version The tool's own release tag.
<prefix>_scanner_image <registry>/<repo>@sha256:<64 hex> — the OCI image reference this card resolved for that release tag, pinned by digest, never by tag alone.
<prefix>_rulebase_id A human identity for the rule base bound to this exact scanner release.
rule base identity field <prefix>_rulebase_sha256 (secrets, shell) or <prefix>_rulebase_image (vuln) or <prefix>_rulebase_ref (sast) — the concrete, recomputed-or-resolved identity of the rule base itself, never a value trusted from a floating pointer.
<prefix>_finding_format_flag The tool's real CLI flag that selects a machine-readable finding output.
<prefix>_finding_format_default The format the tool emits with no flag.
<prefix>_finding_format_values The full comma-separated set of formats the flag accepts, as documented by the tool.
<prefix>_offline_mode How the tool behaves without network: fully-offline (never calls out), pre-cached-db-required (needs a rule base fetched ahead of time, then runs offline), or local-config-required (needs a local, non-registry rule path).
<prefix>_offline_flag The concrete flag that selects the offline behavior, where the tool has one.
<prefix>_egress Always denied — every later card that runs one of these scanners inside the board's network: none container profiles must supply the rule base/config locally; none of the four tools is invoked with network available.

Per-scanner pins and their primary source

All four were resolved live against each tool's own GitHub release API and Docker Hub / GHCR registry from this workspace on 2026-08-05; the network access itself is evidence, not an assumption — curl -sI https://github.com returned HTTP/2 200 in this session before any pin below was resolved.

secrets — gitleaks v8.30.1 - Release tag: https://api.github.com/repos/gitleaks/gitleaks/releases/latest → tag_name: v8.30.1. - Image digest: docker buildx imagetools inspect zricethezav/gitleaks:v8.30.1 → index digest sha256:c00b6bd0aeb3071cbcb79009cb16a60dd9e0a7c60e2be9ab65d25e6bc8abbb7f. - Rule base: gitleaks' own bundled default config at that same tag, https://raw.githubusercontent.com/gitleaks/gitleaks/v8.30.1/config/gitleaks.toml, fetched and hashed in this session: sha256:e163e53b9e7e8a8511e77271e2b323ed057759542a6d988258afe3a1fa329caf (97731 bytes). - Finding format: -f/--report-format, values json,csv,junit,sarif,template (confirmed live against the pinned digest's own --help, round 3). Default: none — corrected in round 3. Running the pinned digest with no --report-path emits no report at all (log lines only); running it with --report-path but no --report-format fails fatally (Unknown report format:, exit 1) rather than falling back to json. Raw transcript: tests/bootstrap/locks/AUR-362/ground-truth/RAW_CAPTURE.txt, section 4. - Offline: gitleaks scans local git history/files with its bundled config; it never calls out. fully-offline.

vulnerabilities — trivy 0.73.0 - Release tag: https://api.github.com/repos/aquasecurity/trivy/releases/latest → tag_name: v0.73.0. - Image digest: docker buildx imagetools inspect aquasec/trivy:0.73.0 → index digest sha256:7cced7cae583819fc7806d4cbc0dbbc7cad18b99f7d3e235192e6da8c091045c. - Rule base: trivy's vulnerability DB is a separate, versioned OCI artifact, schema 2: docker buildx imagetools inspect ghcr.io/aquasecurity/trivy-db:2 → digest sha256:cce7b2ad966d6fe3789d65879579b8b24fce9d31052ed3ff73b1c5224f049142. - Finding format: --format, default table (Cobra's own (default "table") flag annotation, confirmed live). Values: table,json,template,sarif,cyclonedx,spdx,spdx-json,github,cosign-vuln — corrected in round 3. A round-2 adversarial review ran trivy image --help against the exact pinned digest and found the enum this card previously carried (table,json,sarif,template,sbom,github) both missing four real values (cyclonedx, spdx, spdx-json, cosign-vuln) and inventing one that does not exist (sbom — trivy's SBOM output is a shape selected via cyclonedx/spdx/spdx-json, never a literal sbom token). Raw transcript: tests/bootstrap/locks/AUR-362/ground-truth/RAW_CAPTURE.txt, section 1. - Offline: trivy's own air-gap documentation states --offline-scan "prevent[s] Trivy from trying to connect" during scanning; it still requires the DB artifact above to be present locally first. pre-cached-db-required.

sast — semgrep 1.172.0 - Release tag: https://api.github.com/repos/semgrep/semgrep/releases/latest → tag_name: v1.172.0. - Image digest: docker buildx imagetools inspect semgrep/semgrep:1.172.0 → index digest sha256:65dcd4408adda7c183a6b4550cb1e9b19f7f627a6fbb7e0559bd466bedc44d7b. - Rule base: the community semgrep/semgrep-rules repository ships no version tags (.../releases and .../tags both return empty at the time of writing); this card therefore pins the repository's own commit identity instead of an unpinned branch pointer — the branch tip resolved via the GitHub API in this session, commit 311ca4e9ba59d700624539bf658e3d29b134ee77. A later card that adopts this rule base must clone or fetch that exact commit, never a branch name. - Finding format: --json (also --sarif, --text, --junit-xml, --emacs, --vim, --gitlab-sast, --gitlab-secrets) (confirmed live against the pinned digest's own semgrep scan --help, round 3). Default: text — corrected in round 3. A round-2 adversarial review ran the pinned digest with --config=auto and no format flag and found the actual default output is semgrep's own colored "Scan Status"/"Scan Summary" table, never json; round 3 confirmed this transcript is byte-identical to an explicit --text run of the same input, establishing text (not merely "not json") as the correct value. Raw transcript: tests/bootstrap/locks/AUR-362/ground-truth/RAW_CAPTURE.txt, section 3. - Offline: semgrep's --config accepts a local YAML file or directory instead of a registry entry name, which the docs confirm avoids any registry network call. local-config-required.

shell — shellcheck v0.11.0 - Release tag: https://api.github.com/repos/koalaman/shellcheck/releases/latest → tag_name: v0.11.0. - Image digest: docker buildx imagetools inspect koalaman/shellcheck:v0.11.0 → index digest sha256:61862eba1fcf09a484ebcc6feea46f1782532571a34ed51fedf90dd25f925a8d. - Rule base: shellcheck's SC-code checks are compiled into the binary itself; there is no separate rule artifact to pin, so this card uses the same image digest as the rule base identity, on the basis that the two are inseparable for this tool. - Finding format: --format / -f, default tty (confirmed live: the pinned digest's default-invocation transcript is byte-identical to an explicit --format=tty run). Values: checkstyle,diff,gcc,json,json1,quiet,tty — corrected in round 3. A round-2 adversarial review ran the pinned digest with --format=xml and found it rejected (Unknown format xml, exit 4) with the tool's own Supported formats: list matching its --help text exactly — seven values, none named xml. Raw transcript: tests/bootstrap/locks/AUR-362/ground-truth/RAW_CAPTURE.txt, section 2. - Offline: a pure static analyzer over shell script text; it makes no network calls during a scan (this is separate from the unrelated shellcheck.net web front end). fully-offline.

Ground truth for the finding-format fields

A round-2 adversarial review (/tmp/aurum-reviews/AUR-362-r2-reviewB.md) found vuln_finding_format_values, shell_finding_format_values and sast_finding_format_default wrong by running the exact pinned scanner digests directly, and identified the structural cause: the round-2 acceptance script's expected ground-truth table was a static bash literal typed by the same author, at the same time, from the same belief as the lock's own values — its comment claimed the table was carried "independently of the lock file it checks," but nothing forced that independence, so a wrong belief could and did land in both places at once, and the "independent" check could never catch its own author's mistake.

Round 3 fixes this for the twelve *_finding_format_* fields specifically (the class where the defect was found), without altering the design for the other 34 fields (each of which round 2 confirmed correct against its own live primary source — GitHub release API, registry digest — a check that already does not depend on the lock's own text):

  1. Every pinned scanner digest in .board/bootstrap/locks/scanners.yml was run live in this session (docker run --rm <pinned digest> ...), for real, against network access confirmed available (docker version succeeded). The complete, verbatim stdout/stderr and exit code of every such invocation is captured in tests/bootstrap/locks/AUR-362/ground-truth/RAW_CAPTURE.txt, timestamped 2026-08-05T14:45:36Z.
  2. tests/bootstrap/locks/AUR-362/ground-truth/cli-format-fields.tsv mechanically extracts the twelve *_finding_format_* values from that raw transcript — a key<TAB>value file, following the same parse discipline tests/acceptance/AUR-362.sh already applies to .board/bootstrap/locks/scanners.yml itself (bounded, non-symlink, duplicate/unknown-key detection, every declared key required present).
  3. tests/acceptance/AUR-362.sh reads this file into its expected map at run time instead of hand-typing these twelve values into its own source a second time. A value wrong in the lock is now only ever caught if it also happens to be wrong, independently, in a separately-versioned, separately-hashed fixture derived mechanically from a raw tool transcript — not merely wrong in the same author's second literal. Absence, corruption, or structural defect (duplicate key, unknown key, a declared field missing) in cli-format-fields.tsv is this program's own fixture machinery failing, not a verdict about the real lock: it fails inconclusive (exit 79), same discipline as cases.tsv.

While reproducing reviewer-b's method against all twelve fields (not only the three named), this round also found secrets_finding_format_default false under the identical test: gitleaks v8.30.1's --report-format flag has no default — omitting it never yields json (see the gitleaks entry above and RAW_CAPTURE.txt section 4). This field is corrected here too, disclosed separately since it fell outside reviewer-b's named blocker; it was found only because this round re-derived the ground truth for the entire *_finding_format_* field group from real tool output rather than touching only the three named fields and trusting the rest.

Bounds

Bound Value
.board/bootstrap/locks/scanners.yml size 8 KiB (observed at 2350 bytes today; independently enforced by tests/acceptance/AUR-362.sh before any field is parsed — see the overlimit fixture below).
tests/bootstrap/locks/AUR-362/cases.tsv size 4 MiB (card-declared bound; enforced by the sealed acceptance program).
cases.tsv rows 256 (card-declared bound; enforced by the sealed acceptance program).
tests/bootstrap/locks/AUR-362/ground-truth/cli-format-fields.tsv size 4 KiB (round-3 bound; enforced before any of its rows are parsed — absence or a bound violation is inconclusive, never a lock verdict).
Acceptance container wall-clock (accept vector, informational) 15 s.

Codes

Typed dispositions tests/acceptance/AUR-362.sh emits on stderr as AUR-362/AC-001/<code> (exit 1, behavioral). The script parses .board/bootstrap/locks/scanners.yml field by field against a ground-truth table (34 fields resolved live from each tool's own release API / registry digest and carried directly in the script; the twelve *_finding_format_* fields read at run time from the separately-versioned tests/bootstrap/locks/AUR-362/ground-truth/cli-format-fields.tsv — see "Ground truth for the finding-format fields" above), and cross-checks .github/workflows, Dockerfile and .docker/docs.Dockerfile for a scanner this card never locked:

Code Trigger
scanner_lock_incomplete .board/bootstrap/locks/scanners.yml is absent (not a symlink; a symlink is source_integrity_error, below).
source_integrity_error The lock is a symlink rather than a plain file, or a pinned identity field (a *_scanner_image, vuln_rulebase_image, *_rulebase_sha256/_digest/_ref) is well-formed but its value diverges from the ground-truth table — a supply-chain value tampered without breaking its own shape.
invalid_lock_or_manifest The file is empty, misses the schema: bootstrap-lock-v1 or card: AUR-362 line, misses any sha256:<64 hex> digest, is missing one of the schema's 46 declared fields outright, or a non-identity, non-format field (name/role/version/rulebase_id/offline_mode/offline_flag/egress) has a well-formed but wrong value.
input_limit_exceeded .board/bootstrap/locks/scanners.yml exceeds 8 KiB, or cases.tsv exceeds its own 4 MiB / 256-row bound.
scanner-ref-mutable A pinned OCI image field (any *_scanner_image, or vuln_rulebase_image) does not match <registry>/<repo>@sha256:<64 hex> — a digest replaced by a floating tag or branch reference (MUT-003's class; vuln_rulebase_image is this card's own extra field in the same class).
finding-schema-mismatch Any *_finding_format_flag/_default/_values field diverges from the ground-truth CLI contract for that scanner's pinned version (MUT-002's class).
scanner-unlocked .github/workflows, Dockerfile or .docker/docs.Dockerfile reference a scanner tool this card's lock does not carry a prefix for (MUT-001's class).

Known gap between this card's prose and the sealed acceptance program

This gap, as recorded by this card's round-1 builder, is now closed. The round-2 acceptance program (tests/acceptance/AUR-362.sh) parses .board/bootstrap/locks/scanners.yml field by field against an independently-carried ground-truth table (not merely three whole-file structural checks), and separately cross-checks this card's read_paths (.github/workflows, Dockerfile, .docker/docs.Dockerfile) for a scanner tool absent from the lock. Per gap item recorded by round 1:

  • MUT-001 (scanner-unlocked) is now produced: the script scans the three read_paths for a fixed set of scanner-tool keywords disjoint from the four this card locks (bandit, snyk, codeql, checkov, trufflehog, grype, syft, gosec) and fails scanner-unlocked on any match. This still never writes to .github/workflows, Dockerfile or .docker/docs.Dockerfile in the repository proper (read_paths, not paths) — the two new fixtures below (unlocked-workflow, unlocked-dockerfile) each materialize their own private copy of the affected read_path under this card's own paths (tests/bootstrap/locks/AUR-362/fixtures/<case>/...), and the live round-2 proof mutated .github/workflows/ci.yml only transiently, restoring it via git checkout and a matching SHA-256 before the patch was cut.
  • MUT-002 (finding-schema-mismatch) is now produced: the script parses all twelve *_finding_format_* fields and fails finding-schema-mismatch on any divergence from the ground-truth CLI contract, independent of the selector (AC-001, ContractAUR362, IntegrationAUR362, E2EAUR362 all run the identical check; the selector remains a passthrough label).
  • MUT-003 (scanner-ref-mutable) now emits the card's own literal code instead of the structurally-similar mutable_reference round 1 used: any of the five pinned-image fields (secrets_scanner_image, vuln_scanner_image, vuln_rulebase_image, sast_scanner_image, shell_scanner_image) failing the @sha256:<64 hex> shape now fails exactly scanner-ref-mutable.
  • The byte-size bound is now implemented directly on .board/bootstrap/locks/scanners.yml (8 KiB, checked before any field is parsed) and exercised by the overlimit fixture, closing the input_limit_exceeded gap; cases.tsv's own 4 MiB / 256-row bound was already enforced by the sealed program and remains so.

A round-2 adversarial review (/tmp/aurum-reviews/AUR-362-r2-reviewB.md) blocked that round on exactly this design: it ran the pinned scanner digests directly and found vuln_finding_format_values, shell_finding_format_values and sast_finding_format_default wrong in both the lock and the script's own "independently carried" literal at once — because both were typed by the same author from the same belief, the round-2 script's own comment claiming independence was false for these three fields. Round 3 closes this:

  • The three named fields are corrected to what the pinned digests actually do, confirmed live in this round (see "Ground truth for the finding-format fields" and the per-scanner entries above, each citing its RAW_CAPTURE.txt section).
  • secrets_finding_format_default is also corrected (json → none), found false under the identical live-digest test while this round re-derived ground truth for the whole *_finding_format_* field group rather than only the three named fields — disclosed separately since it was not part of reviewer-b's blocker.
  • The twelve *_finding_format_* fields' ground truth moved out of the script's own hand-typed literal into tests/bootstrap/locks/AUR-362/ground-truth/cli-format-fields.tsv, a separately-versioned, separately-hashed fixture mechanically extracted from RAW_CAPTURE.txt's live pinned-digest transcripts — so this class of defect (one author's same wrong belief landing in the lock and its own oracle at once) requires tampering two independently-versioned artifacts, not editing one file twice. The nominal, unlocked-workflow and unlocked-dockerfile fixtures (byte-identical copies of the real lock) and cases.tsv's SHA-256 for all three were updated to match the corrected lock; every other fixture (tamper, invalid, mutable, mutable-rulebase, format-mismatch, digest-tamper, overlimit) triggers its disposition on a field checked earlier than the *_finding_format_* group in verify_scanners_dir and needed no change.

No open item remains from round 1's list. One judgment call this round adds, recorded here for the next reviewer: a pinned-format-but-wrong-value mismatch on vuln_rulebase_image and the four *_rulebase_sha256/_ref/ _digest content-hash fields is classified source_integrity_error rather than a new code, on the basis that the card's own "Acceptance scenarios" prose groups "tamper" and "symlink" under that single code — this digest-tamper fixture demonstrates a non-symlink instance of the same disposition, and the tamper fixture (a symlink) was re-pointed from scanner_lock_incomplete to source_integrity_error to match, since the card text assigns absence and tamper/symlink two different codes and the round-1 script had collapsed them into one.

tests/bootstrap/locks/AUR-362/cases.tsv and its fixtures/ (11 cases: nominal, absence, tamper, invalid, mutable, mutable-rulebase, format-mismatch, digest-tamper, unlocked-workflow, unlocked-dockerfile, overlimit) reproduce every disposition tests/acceptance/AUR-362.sh can now produce, each fixture's .board/bootstrap/locks/scanners.yml (or, for the two unlocked-* cases, its accompanying materialized read_path file) hashed by SHA-256 in cases.tsv so a byte-identical replay is independently verifiable.

Diagnostic example (synthetic)

$ bash tests/acceptance/AUR-362.sh
AUR-362/AC-001/scanner_lock_incomplete        # before the lock exists
$ bash tests/acceptance/AUR-362.sh
{"card":"AUR-362","scenario":"AC-001","selector":"AC-001","result":"pass"}
$ ./.board/bin/oci-run --profile bootstrap-readonly-v1 --card AUR-362   # offline, network=none, read-only

Compatibility, migration, rollback

Unchanged from the card: contracts and bytes outside the Public contract stay the same; divergence fails AC-001; consumers migrate only after AUR-233 accepts every sibling lock manifest; rollback removes only this card's own artifacts and repeats AC-001.