AUR-581: gitleaks is a registered engine
Measured before (base e1f14ef7)
- gitleaks was pinned in
.board/bootstrap/locks/scanners.yml(v8.30.1,docker.io/zricethezav/gitleaks@sha256:c00b6bd0..., rule basegitleaks-default-config-v8.30.1,sha256:e163e53b...) and no code ran it. scanner.RequestcarriedRoot, Trust, Options, Command: no commit range, so no engine could scan a pull request's history.scanner.OutcomedroppedReport.Version.
The pinned binary, measured on the host (2026-10-03)
docker run --rm docker.io/zricethezav/gitleaks@sha256:c00b6bd0aeb3071cbcb79009cb16a60dd9e0a7c60e2be9ab65d25e6bc8abbb7f git --help
and ... version (v8.30.1), then runs over a fixture repository created at
run time (base commit; a commit adding a GitHub token built by concatenation;
a commit removing it; a commit adding the same token with # gitleaks:allow):
| Run | Result |
|---|---|
git --log-opts=<base>..<head> |
1 leak (github-pat, leak.txt:1, the intermediate commit); the allowed line is suppressed |
same + --ignore-gitleaks-allow |
2 leaks |
same + --redact |
Secret = REDACTED, Match = REDACTED |
dir over the final tree |
0 leaks (the token was removed; the allowed line is suppressed) |
.gitleaksignore with the leak's fingerprint at the repository root, cwd = repo / cwd elsewhere / -i <empty dir> / both |
the leak is ignored in every case: gitleaks always reads <source>/.gitleaksignore; no flag disables it |
.gitleaksignore in a subdirectory |
not read |
.gitleaks.toml at the root allowlisting both files |
0 leaks (the repository replaces the rule base) |
same + --config <file with [extend] useDefault = true> |
2 leaks (explicit config outranks the root file) |
--log-opts=deadbeef..<head> (unknown base) |
ERR [git] fatal: ambiguous argument, exit 0, report [], "no leaks found" |
--log-opts=<head>..<head> |
exit 0, [] |
unknown base with the engine's flags (--no-color included), stderr through cat -v |
plain 7:51PM ERR [git] fatal: ambiguous argument ..., no escape codes; the engine still strips ANSI color before matching ERR/FTL |
The report's keys: Author, Commit, Date, Description, Email, EndColumn,
EndLine, Entropy, File, Fingerprint, Match, Message, RuleID, Secret,
StartColumn, StartLine, SymlinkFile, Tags.
What changed
internal/scanner/scanner.go:Request.Range{Base, Head}(the reviewed commit range;Empty());executor.go:Outcome.VersioncarriesReport.Version.internal/scanner/gitleaks(new;engine.go,invocation.go,report.go), registered ininternal/scanner/engines: name and origingitleaks, categorysecrets, no options.gitleaks versionfirst: a missing binary keepsexec.ErrNotFound(secrets_unavailable); any version butv8.30.1is an error.- The range is verified before gitleaks sees it, because gitleaks itself
reads an unknown revision as a clean scan: both ends must be full commit
ids (so neither can be an option), the repository must not be shallow
(
git rev-parse --is-shallow-repository), and both commits must exist (git cat-file -e <id>^{commit}). An empty range is an error: there is no fallback to the final tree. gitleaks git --log-opts=<base>..<head> --report-format json --report-path <tmp>/report.json --exit-code 0 --no-banner --no-color --redact --config <tmp>/pinned.toml ., the temporary directory private and removed.pinned.tomlis[extend] useDefault = true: the bundled (pinned) rule base, ahead ofGITLEAKS_CONFIG,GITLEAKS_CONFIG_TOMLand a root.gitleaks.toml.- A non-zero exit, an
ERR/FTLlog line, a missing report, a report that is not a JSON array, or an entry without rule, file or line is a failed scan (secrets_execution_error/secrets_invalid_output). - The report is decoded into
leak{RuleID, Description, File, StartLine, Commit}: there is no field forSecret,Match,Line, the commitMessage,AuthororEmail, so they are discarded by the decoder before anything else (gitleaks' own--redactis the second line). A finding isgitleaks:<rule>atfile:line, severityerror(gitleaks has no severity; the entry'sfail_on, defaultERROR, decides), message<rule description> in commit <12 hex> (gitleaks v8.30.1). - Policy (
TrustPolicy):--ignore-gitleaks-allowand--gitleaks-ignore-path <tmp>. Because the root.gitleaksignoreis still read (measured above), its presence under policy is the blocking findinggitleaks:ignore-file-presentat.gitleaksignore:1: the policy owner has to clear it. Residue: findings the file hides are not reported individually; the scan as a whole cannot pass while it exists. Report.Version=gitleaks v8.30.1 rulebase gitleaks-default-config-v8.30.1 sha256:e163e53b...(Identity)..github/workflows/review.yml: stepInstall pinned gitleaksreads image and version from the lock in the tool checkout, refuses an image not pinned by digest, pulls by digest (fails if the bytes differ), copies/usr/bin/gitleaks(static) intoaurumcode-reviewand fails unlessgitleaks versionequals the lock's version; the pull request checkout usesfetch-depth: 0(a shallow clone would cut the range); the review container getsGIT_CONFIG_COUNT/KEY_0/VALUE_0=safe.directory=/github/workspace(git in the container is another user than the checkout's owner).- Docs:
docs/configuration.md(engine section,secretssource),docs/gate-corporativo.md.
Observable change
Registering a second category extends the accepted-source list of
gate.sources/gate.triage errors: gate.sources: unknown source "supply"
(accepted: skills, analysis, sast, secrets) (AUR-577's spec said the text
"keeps listing skills, analysis, sast"; it now also lists secrets). The gate
tutorial's expected/falha-fonte-invalida.txt and the block in
docs/tutorials/gate.md were updated to the new text. No other output
changed.
No lock field was added: scanners.yml already holds every value used
(secrets_scanner_image, secrets_scanner_version, secrets_rulebase_id,
secrets_rulebase_sha256); TestGitleaksIdentityMatchesScannersLock ties the
engine's constants to them.
Wiring, image and tutorial (card v3: cmd/aurumcode, Dockerfile, docs/architecture.md)
cmd/aurumcode/scanner_pass.go:runScanners(root, r scanner.Range, blocked)keeps the range on the session (scanRange) and every scan, the deliberation's deferred ones included, getsRequest.Range. AUR-577's MUT-001 anchorout := s.deps.scanners.Scan(s.ctx, engine, scanner.Request{is unchanged. Each non-emptyOutcome.Versionis kept as<engine>=<version>(scanVersions).review_pr_analysis.go:scanner.Range{Base: AURUMCODE_BASE_SHA, Head: GITHUB_SHA};review_base_analysis.go+scanner_range.go(localScanRange):--baseandHEADresolved to full ids throughanalyzer.Repo.ResolveRef; an unresolvable end leaves the range empty and the engine fails closed.review_weighing.go: the evidence digest coversevidenceIdentity(), the items plus the sorted engine identities when any engine reported one; without one it is the items alone, so every existing key is unchanged.- Test callers of
runScanners(aur577_test.go,review_session_exit_test.go) passscanner.Range{}: a signature change, no assertion changed. Dockerfile: stageFROM docker.io/zricethezav/gitleaks@sha256:c00b6bd0... AS gitleaks,COPY --from=gitleaks /usr/bin/gitleaks /usr/local/bin/gitleaksandRUN test "$(gitleaks version)" = "v8.30.1".Dockerfileruntime base pinned like the builder:FROM alpine:3.24@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6(wasalpine:3.20without digest). Digest = the multi-arch index, fromdocker buildx imagetools inspect alpine:3.24on the host (2026-10-03:MediaType: application/vnd.oci.image.index.v1+json,Digest: sha256:294b683c...). The built image reportsgitleaks v8.30.1,semgrep 1.172.0,git 2.54.0,Python 3.14.8.- Tutorial
docs/tutorials/segredos.md+demo/tutoriais/segredos(five cases, registered indocs/index.md,docs/tutorials/README.mdanddemo/tutoriais/README.md; AUR-561..564 enumerate fixed tutorial lists and need no entry). The token exists only at run time (built by concatenation inrun.sh); commit dates are fixed so the commit cited by each finding is checked inexpected/. Noout/file carries the value. docs/architecture.mdnames the engine, the range and the digest.
The optional docker integration test was not written: Go runs only in the shared container, which has no docker; the real binary's behavior is proven by the host transcript above and by the tutorial, which runs the pinned binary inside the product image.
How each AC/MUT is proven
bash tests/acceptance/AUR-581.sh <selector> in the shared container (fake
runner; the sealed profile has neither gitleaks nor docker):
- AC-001 —
TestGitleaksSecretInPullRequestHistoryReachesGateWithoutValue: a leak present only in the range's history (the fake answersgitleaks dirwith nothing, as the real binary does) is reported atconfig/app.env:3, rulegitleaks:github-pat, the commit named; throughgate.ApplyScannerGatethe line startsgitleaks:github-patand endsorigem gitleaks, secao policy, the audit's blocking finding has origingitleaks; the token, its suffix, the commit message and the e-mail the fake report carries appear in none of the finding, the issue, the gate lines, the audit JSON or the version.TestGitleaksIdentityMatchesScannersLock,TestGitleaksRegistrationAndOptions. Plus AUR-582's credential-shape grep over the new files. - AC-002 —
TestGitleaksFailuresAreInconclusive: missing binary (secrets_unavailable); no range, a flag as range end, base not fetched, shallow clone, other version,ERRlogged at exit 0 (secrets_execution_error; gitleaks is never invoked on a range it could not verify); a non-array,nullor field-less report (secrets_invalid_output). Findings nil and the gate inconclusive in every case. - AC-003 —
TestGitleaksInlineAllowOnlyHonoredWithoutPolicy,TestGitleaksIgnoreFileIsAFindingUnderPolicy. - AC-001 (review level) —
TestBaseReviewHandsTheResolvedRangeToGitleaks: the realreview --base HEAD~1with the gitleaks engine and a fake command hands--log-opts=<base id>..<head id>(40-hex ids resolved from the fixture), the gate line readspolicy gate: gitleaks:github-pat - ... in commit abababababab ... origem gitleaks, secao repo), the audit has origingitleaks, and stdout, stderr and the audit lack the token.TestEngineIdentityEntersTheEvidenceDigest: two identities give two digests; none gives the items' own digest. - AC-004 — the workflow step reads image and version from the lock, requires
@sha256:<64 hex>, pulls by digest, compares the version, the checkout isfetch-depth: 0; the Dockerfile's gitleaks stage is the lock's image and its build checks the lock's version;demo/tutoriais/segredos/run.sh --checkpasses. - MUT-001 —
"git"+--log-optsreplaced by"dir": AC-001 RED (history leak not reported: []). - MUT-002 — a
Secretfield decoded and appended to the description: AC-001 RED (output carries the token; the failure names the leaked value, never prints it, so the sealed runner's secret detector stays quiet).
Credential scan fixed after review
The first no-credential-literal check ran grep -rlE --exclude-dir=.estado
... || true: the container's busybox grep refuses --exclude-dir (exit 2),
|| true swallowed it and the line passed without scanning anything.
scan_credentials now lists files with find ... -name .estado -prune -o
-type f -print0 and greps them one at a time, reading grep's status: 0 = a
credential-shaped literal (the file is named, the selector fails), 1 = clean,
anything else = infrastructure (exit 79); zero files scanned is
infrastructure too. The line prints the count:
AUR-581/AC-001/no-credential-literal (127 files scanned). MUT-003 plants
demo/tutoriais/segredos/plantado.py with a GitHub-token-shaped value built
by concatenation in a copy of the scanned paths: AUR-581/MUT-003/rejected
(stderr names credential-shaped literal: demo/tutoriais/segredos/plantado.py).
Regression (this tree, after merging main with AUR-580)
Sealed (oci-run --profile go-unit-offline-v1 --card AUR-581, cwd in the
worktree): exit_code: 0, secret_detected: false; lines AC-001/pass,
AC-001/review/pass, AC-001/no-credential-literal, AC-002/pass,
AC-003/pass, MUT-001/rejected, MUT-002/rejected, MUT-003/rejected,
AC-004/workflow/pass, AC-004/tutorial/pass, AUR-581/all/pass.
Shared container, in series: go vet ./... clean, go test -p 1 ./... rc 0
with no FAIL line; AUR-580, AUR-577, AUR-575, AUR-582, AUR-548
all/pass; AUR-561..564 all rc 0. bash scripts/docs/build.sh: no
warning. AUR-582's AC-005 greps the whole tree when run without git, so the
ignored tutorial state (demo/tutoriais/*/.estado/) must be removed first.
Tutorials: image aurum-tutoriais:aur581 built from this tree (alpine 3.24
by digest), the 17 tutorials re-run (run.sh all, then --check): all=0
check=0 for each; out/ (with .imagem) committed.