Pular para conteúdo

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 base gitleaks-default-config-v8.30.1, sha256:e163e53b...) and no code ran it.
  • scanner.Request carried Root, Trust, Options, Command: no commit range, so no engine could scan a pull request's history.
  • scanner.Outcome dropped Report.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.Version carries Report.Version.
  • internal/scanner/gitleaks (new; engine.go, invocation.go, report.go), registered in internal/scanner/engines: name and origin gitleaks, category secrets, no options.
  • gitleaks version first: a missing binary keeps exec.ErrNotFound (secrets_unavailable); any version but v8.30.1 is 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.toml is [extend] useDefault = true: the bundled (pinned) rule base, ahead of GITLEAKS_CONFIG, GITLEAKS_CONFIG_TOML and a root .gitleaks.toml.
  • A non-zero exit, an ERR/FTL log 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 for Secret, Match, Line, the commit Message, Author or Email, so they are discarded by the decoder before anything else (gitleaks' own --redact is the second line). A finding is gitleaks:<rule> at file:line, severity error (gitleaks has no severity; the entry's fail_on, default ERROR, decides), message <rule description> in commit <12 hex> (gitleaks v8.30.1).
  • Policy (TrustPolicy): --ignore-gitleaks-allow and --gitleaks-ignore-path <tmp>. Because the root .gitleaksignore is still read (measured above), its presence under policy is the blocking finding gitleaks:ignore-file-present at .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: step Install pinned gitleaks reads 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) into aurumcode-review and fails unless gitleaks version equals the lock's version; the pull request checkout uses fetch-depth: 0 (a shallow clone would cut the range); the review container gets GIT_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, secrets source), 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, gets Request.Range. AUR-577's MUT-001 anchor out := s.deps.scanners.Scan(s.ctx, engine, scanner.Request{ is unchanged. Each non-empty Outcome.Version is 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): --base and HEAD resolved to full ids through analyzer.Repo.ResolveRef; an unresolvable end leaves the range empty and the engine fails closed.
  • review_weighing.go: the evidence digest covers evidenceIdentity(), 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) pass scanner.Range{}: a signature change, no assertion changed.
  • Dockerfile: stage FROM docker.io/zricethezav/gitleaks@sha256:c00b6bd0... AS gitleaks, COPY --from=gitleaks /usr/bin/gitleaks /usr/local/bin/gitleaks and RUN test "$(gitleaks version)" = "v8.30.1".
  • Dockerfile runtime base pinned like the builder: FROM alpine:3.24@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 (was alpine:3.20 without digest). Digest = the multi-arch index, from docker buildx imagetools inspect alpine:3.24 on the host (2026-10-03: MediaType: application/vnd.oci.image.index.v1+json, Digest: sha256:294b683c...). The built image reports gitleaks 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 in docs/index.md, docs/tutorials/README.md and demo/tutoriais/README.md; AUR-561..564 enumerate fixed tutorial lists and need no entry). The token exists only at run time (built by concatenation in run.sh); commit dates are fixed so the commit cited by each finding is checked in expected/. No out/ file carries the value.
  • docs/architecture.md names 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 answers gitleaks dir with nothing, as the real binary does) is reported at config/app.env:3, rule gitleaks:github-pat, the commit named; through gate.ApplyScannerGate the line starts gitleaks:github-pat and ends origem gitleaks, secao policy, the audit's blocking finding has origin gitleaks; 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, ERR logged at exit 0 (secrets_execution_error; gitleaks is never invoked on a range it could not verify); a non-array, null or 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 real review --base HEAD~1 with the gitleaks engine and a fake command hands --log-opts=<base id>..<head id> (40-hex ids resolved from the fixture), the gate line reads policy gate: gitleaks:github-pat - ... in commit abababababab ... origem gitleaks, secao repo), the audit has origin gitleaks, 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 is fetch-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 --check passes.
  • MUT-001 — "git" + --log-opts replaced by "dir": AC-001 RED (history leak not reported: []).
  • MUT-002 — a Secret field 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.