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):
- Every pinned scanner digest in
.board/bootstrap/locks/scanners.ymlwas run live in this session (docker run --rm <pinned digest> ...), for real, against network access confirmed available (docker versionsucceeded). The complete, verbatim stdout/stderr and exit code of every such invocation is captured intests/bootstrap/locks/AUR-362/ground-truth/RAW_CAPTURE.txt, timestamped2026-08-05T14:45:36Z. tests/bootstrap/locks/AUR-362/ground-truth/cli-format-fields.tsvmechanically extracts the twelve*_finding_format_*values from that raw transcript — akey<TAB>valuefile, following the same parse disciplinetests/acceptance/AUR-362.shalready applies to.board/bootstrap/locks/scanners.ymlitself (bounded, non-symlink, duplicate/unknown-key detection, every declared key required present).tests/acceptance/AUR-362.shreads this file into itsexpectedmap 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) incli-format-fields.tsvis this program's own fixture machinery failing, not a verdict about the real lock: it failsinconclusive(exit79), same discipline ascases.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 threeread_pathsfor a fixed set of scanner-tool keywords disjoint from the four this card locks (bandit,snyk,codeql,checkov,trufflehog,grype,syft,gosec) and failsscanner-unlockedon any match. This still never writes to.github/workflows,Dockerfileor.docker/docs.Dockerfilein the repository proper (read_paths, notpaths) — the two new fixtures below (unlocked-workflow,unlocked-dockerfile) each materialize their own private copy of the affectedread_pathunder this card's ownpaths(tests/bootstrap/locks/AUR-362/fixtures/<case>/...), and the live round-2 proof mutated.github/workflows/ci.ymlonly transiently, restoring it viagit checkoutand 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 failsfinding-schema-mismatchon any divergence from the ground-truth CLI contract, independent of the selector (AC-001,ContractAUR362,IntegrationAUR362,E2EAUR362all 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-similarmutable_referenceround 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 exactlyscanner-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 theoverlimitfixture, closing theinput_limit_exceededgap;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.txtsection). secrets_finding_format_defaultis 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 intotests/bootstrap/locks/AUR-362/ground-truth/cli-format-fields.tsv, a separately-versioned, separately-hashed fixture mechanically extracted fromRAW_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. Thenominal,unlocked-workflowandunlocked-dockerfilefixtures (byte-identical copies of the real lock) andcases.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 inverify_scanners_dirand 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.