c08b09dc9f
Assisted-by: Codex
440 lines
21 KiB
Markdown
440 lines
21 KiB
Markdown
# Approving specific commits past git-gate
|
|
|
|
Research into (1) whether a dashboard or operator surface for the
|
|
git-gate (a.k.a. "gitlock", PRD 0008) already exists, and (2) what a
|
|
narrowly-scoped approval flow for false-positive gitleaks rejections
|
|
could look like without compromising the gate's "if it's bypassable it
|
|
isn't a gate" property.
|
|
|
|
Motivated by PRD 0012's open question: when an agent commits docs
|
|
containing intentionally-bogus tokens that the secret scanner
|
|
correctly flags, the rejection is correct in the literal sense and
|
|
wrong in the user-intent sense, and there is no way to say so.
|
|
|
|
## Summary
|
|
|
|
No off-the-shelf dashboard fits the shape bot-bottle needs
|
|
(per-bottle, host-local, integrated into a pre-receive rejection
|
|
with approval feeding back into the gate's own decision). Gitleaks
|
|
itself is a CLI with no UI and was declared **feature-complete** in
|
|
early 2026; the author's successor project **Betterleaks** is
|
|
explicitly "for the agentic era" but is also CLI-shaped and still
|
|
young. The closest open-source dashboard is **DefectDojo**, which
|
|
ingests gitleaks JSON but is post-hoc and org-scale — its "marked
|
|
as accepted" state does not feed back into the scanner. SaaS
|
|
dashboards (GitGuardian, TruffleHog Enterprise) ship repo content
|
|
to a vendor and were already disqualified by
|
|
`git-secret-scanning-hardening.md`.
|
|
|
|
The git-gate ships no exception mechanism today: the pre-receive
|
|
hook calls `gitleaks git --log-opts="$range" --no-banner --redact`
|
|
with no `--config` and no `--baseline-path`, and PRD 0008
|
|
explicitly rejects exceptions ("Bypass for trusted commits. No
|
|
`[skip gitleaks]` trailer, no allowlist by commit hash. If the
|
|
gate is bypassable it isn't a gate.").
|
|
|
|
That non-goal is correct against the *agent* but conflates two
|
|
questions: can the *agent* bypass the gate (must be no), and can
|
|
the *user* approve a narrowly-scoped exception out-of-band (could
|
|
be yes). PRD 0012's recovery flow is exactly the seam where the
|
|
user-side approval can live without giving the agent any in-band
|
|
bypass.
|
|
|
|
Gitleaks does ship one native primitive that maps well to "approve
|
|
this specific finding" — the **baseline file** — which is
|
|
semantically a better fit for per-finding approval than the
|
|
allowlist config (a suppression *rule*). This note surveys the
|
|
dashboard landscape, the two native primitives (allowlist and
|
|
baseline), and recommends a direction.
|
|
|
|
## Question 1: Existing dashboards and control surfaces
|
|
|
|
### Inside bot-bottle today
|
|
|
|
`bot_bottle/cli/` has `_common, cleanup, edit, info, init, list,
|
|
start` — nothing gate-specific. The gate appears only as a sidecar
|
|
in `bottle_plan.py`'s preflight rendering. Rejections are written
|
|
to the pre-receive hook's stderr (`echo "git-gate: gitleaks
|
|
rejected push to $ref" >&2`) and surface only in the agent's
|
|
`git push` output — nothing persists outside the container's logs.
|
|
|
|
### Native gitleaks: CLI-only, and now feature-complete
|
|
|
|
Gitleaks has no built-in dashboard or web UI. As of early 2026 the
|
|
project has been declared **feature complete** — only security
|
|
patches will be merged going forward. The original maintainer
|
|
(Zachary Rice) has moved active work to Betterleaks (below), so
|
|
any dashboard built directly against gitleaks should treat the
|
|
gitleaks surface as frozen rather than evolving.
|
|
|
|
### Betterleaks: the same author's "agentic era" successor
|
|
|
|
Started February 2026 and explicitly framed for AI agents driving
|
|
the scanner: flag-based output for low-token-overhead consumption,
|
|
parallelized Git scanning, CEL-based filtering in place of the
|
|
TOML allowlist, and a roadmap that includes LLM-assisted
|
|
classification and automatic secret revocation via provider APIs.
|
|
Still CLI-shaped — no dashboard either.
|
|
|
|
Relevant to bot-bottle in two ways:
|
|
|
|
- The upstream direction of travel is *toward* agent-driven
|
|
scanners, which makes "the bottle invokes a scanner and reports
|
|
findings up" a supported pattern rather than a hack.
|
|
- CEL is a richer expression language for filter entries than
|
|
gitleaks's selector struct, which loosens the design space for
|
|
Option B (below). If bot-bottle ever swaps gitleaks for
|
|
Betterleaks, the approval-flow design should be expressible in
|
|
both.
|
|
|
|
### Output formats: SARIF + viewers
|
|
|
|
Both gitleaks and Betterleaks can emit SARIF. That plugs into
|
|
GitHub Advanced Security's Code Scanning tab (read-only viewer
|
|
with a dismiss-as-not-a-problem state) and assorted open-source
|
|
SARIF viewers (`sarif-web-component`, Microsoft's VS Code
|
|
extension). These render findings; they do not handle approval
|
|
state or feed back into the scanner. Useful for *seeing* findings;
|
|
not useful as the approval surface.
|
|
|
|
### Findings aggregators
|
|
|
|
[**DefectDojo**](https://defectdojo.com/integrations/gitleaks) is
|
|
the closest open-source thing to "a dashboard for gitleaks." It
|
|
ingests gitleaks JSON (and ~200 other scanners), aggregates and
|
|
deduplicates, lets you triage and mark findings as accepted or
|
|
false-positive in its UI, and tracks remediation state. Designed
|
|
for org-scale: one DefectDojo instance covers many repos and
|
|
scanners.
|
|
|
|
Shape mismatch for bot-bottle:
|
|
|
|
- DefectDojo's review state is *informational* — marking a finding
|
|
as accepted in DefectDojo does not write to gitleaks's allowlist
|
|
or baseline and does not change what the gate decides on the
|
|
next push.
|
|
- It expects findings as artifacts of CI runs, not as the
|
|
rejection-cause of an in-flight push.
|
|
- A single shared instance violates the one-sidecar-per-bottle
|
|
posture; per-bottle DefectDojo instances are absurd overhead.
|
|
|
|
Useful to know it exists, especially for long-term post-hoc
|
|
finding tracking. Not the v1 answer for the in-flight approval
|
|
flow PRD 0012 needs.
|
|
|
|
A separate [JupiterOne integration](https://github.com/gitleaks-findings/gitleaks)
|
|
exists but ships findings to JupiterOne's commercial platform and
|
|
has effectively zero public adoption (0 stars, 0 forks). Mentioned
|
|
only because its repo name suggests "the dashboard" and isn't.
|
|
|
|
### SaaS dashboards (disqualified by sandbox premise)
|
|
|
|
GitGuardian / ggshield and TruffleHog Enterprise both offer
|
|
incident-triage UIs with finding-level approval state. Both ship
|
|
repo content to a vendor; already disqualified in
|
|
`git-secret-scanning-hardening.md` for a project whose entire
|
|
premise is sandbox isolation.
|
|
|
|
### Bottom line
|
|
|
|
No off-the-shelf dashboard fits bot-bottle's shape: per-bottle,
|
|
host-local, integrated into a pre-receive rejection with the
|
|
approval feeding back into the gate's own decision-making. The
|
|
nearest open-source analogue (DefectDojo) is post-hoc and
|
|
org-scale; the nearest UX (GitGuardian) is SaaS. The PRD 0012
|
|
dashboard — sharing surface with the broader stuck-agent recovery
|
|
flow — remains the right place to build this.
|
|
|
|
## Question 2: How could specific commits be approved?
|
|
|
|
### What gitleaks gives you natively
|
|
|
|
Two distinct primitives, and the distinction matters for designing
|
|
an approval flow.
|
|
|
|
**Allowlists** are *suppression rules* — config-level patterns that
|
|
say "ignore findings matching X." Gitleaks's TOML config supports
|
|
an `[allowlist]` block (or `[[rules.allowlists]]` per-rule) with
|
|
four selectors:
|
|
|
|
- `paths` — list of regex against file paths.
|
|
- `regexes` — list of regex matched against the finding bytes;
|
|
`regexTarget` directs the regex at the extracted secret
|
|
(default), the entire regex match, or the whole line.
|
|
- `stopwords` — substrings that, if present, suppress the finding.
|
|
- `commits` — explicit commit SHAs to skip entirely.
|
|
|
|
Selectors combine with `condition = "OR"` (default; suppress if any
|
|
selector matches) or `condition = "AND"` (suppress only if all
|
|
match). `commits` is the bluntest tool and the easiest to misuse:
|
|
a single SHA can hide arbitrary content. `paths + regexes` with
|
|
AND is the narrowest scope, and the form that makes a per-finding
|
|
exception still defensible.
|
|
|
|
**Baselines** are a *known-findings list* — a JSON file of
|
|
previously detected findings that gitleaks's `IsNew` function
|
|
compares against on the next scan, so only new findings get
|
|
reported. The file is generated by saving a scan's JSON output and
|
|
fed back in via `--baseline-path`. The comparison checks RuleID,
|
|
description, file path, line numbers, secret content, commit, and
|
|
author/timestamp. When `--redact` is enabled, redacted Secret and
|
|
Match fields are ignored in the comparison so the baseline still
|
|
functions with redacted reports.
|
|
|
|
Detection flow is: global allowlist → rule-specific allowlist →
|
|
baseline → reported finding. Allowlist suppressions therefore win
|
|
over baseline; baseline is the last gate before report.
|
|
|
|
The hook today passes neither `--config` nor `--baseline-path`.
|
|
Wiring either in is mechanically straightforward: the gate image
|
|
is built per `DockerGitGate.start`, so the config / baseline can be
|
|
baked into the image *or* mounted in at start.
|
|
|
|
**Allowlist vs baseline for approval storage.** Both can express
|
|
"don't reject this finding," but they imply different things about
|
|
intent:
|
|
|
|
- An *allowlist* entry says "any future finding that matches this
|
|
pattern is fine." Generative: it covers findings that don't
|
|
exist yet on commits that haven't been made.
|
|
- A *baseline* entry says "this exact finding I've already seen is
|
|
fine." Specific: it pins to the bytes / location / rule of one
|
|
observed finding; a different finding on the same path on a
|
|
later commit re-triggers.
|
|
|
|
For a per-commit user approval, baseline is the better semantic
|
|
match: each approval is an attestation about one observed finding,
|
|
not a rule that pre-approves a pattern. Baseline entries can also
|
|
be diffed in PRs trivially (it's a JSON list) — they double as the
|
|
audit record.
|
|
|
|
### The design tension
|
|
|
|
PRD 0008's "no bypass for trusted commits" non-goal is load-bearing
|
|
*against the agent*. It is not load-bearing against the user, who
|
|
already has every privilege the gate is trying to deny the agent.
|
|
The risk of letting the user approve exceptions is not direct (the
|
|
user can already do whatever they want); it is indirect:
|
|
|
|
- **Prompt-injection laundering.** An attacker who has captured the
|
|
agent's prompt-stream can ask the agent to *request* an exception
|
|
that looks plausible ("I just need to commit the test fixture for
|
|
the new auth flow"). If the user rubber-stamps the request, the
|
|
attacker has used the user as a bypass channel. This is the same
|
|
risk as any human-in-the-loop control: it degrades to "no control"
|
|
if the human always says yes.
|
|
- **Scope creep of a granted exception.** A commit-SHA allowlist
|
|
approved for one commit could, in principle, be re-targeted at a
|
|
different commit if the allowlist isn't tied to the content. This
|
|
is why `commits` alone is unsafe; `paths + regexes` is the form
|
|
that survives content-substitution.
|
|
- **Persistence past intent.** An exception granted "just for this
|
|
commit" that stays in the gate's config indefinitely is no longer
|
|
a per-commit exception; it's a permanent allowlist entry. Without
|
|
TTL or a clean teardown, exceptions accrete.
|
|
|
|
These three risks shape the design constraints below.
|
|
|
|
### Three design options
|
|
|
|
**Option A — Reject and rotate.** Treat every gitleaks hit as
|
|
"rewrite the commit to not contain the literal token, then re-push."
|
|
For docs with fake tokens, use a sentinel string the repo's
|
|
gitleaks config recognizes as obviously not a real secret (e.g.
|
|
`AKIAIOSFODNN7EXAMPLE`, AWS's documented example key, or a project-
|
|
specific placeholder like `<aws-access-key-id>`).
|
|
|
|
- *Cost:* zero. No new code.
|
|
- *Property:* gate stays unbypassable in both senses.
|
|
- *Friction:* every author must know the placeholder convention. The
|
|
first time someone pastes a realistic-looking fake into a doc,
|
|
they get rejected and have to redo the commit. Probably fine for
|
|
the host repo; less fine for bottles authoring third-party content.
|
|
- *Verdict:* this should be the *default*. The exception flow exists
|
|
only for cases where Option A genuinely fails (e.g. the example is
|
|
specifically about a real-looking token format, or the upstream
|
|
doc requires the literal pattern).
|
|
|
|
**Option B — Per-finding approval via PRD 0012 flow.** When the
|
|
agent's push is rejected, the agent invokes
|
|
`/request-gate-exception` (or `/request-bottle-change` with an
|
|
exception variant). The slash command POSTs to the cred-proxy
|
|
endpoint, carrying the gitleaks finding record (rule ID, file path,
|
|
line, redacted match) and a free-text justification ("docs example
|
|
for AWS auth flow").
|
|
|
|
The user reviews the request in the dashboard, sees the file and
|
|
the diff, and approves. The approval gets written into the gate's
|
|
**baseline file** — the JSON list of known-OK findings the gate
|
|
passes as `--baseline-path` to gitleaks. The gate restarts with
|
|
the new baseline.
|
|
|
|
- *Property:* approved findings are pinned to the specific
|
|
observed bytes / path / rule. A different secret on the same
|
|
path on a later commit re-triggers the gate.
|
|
- *Auditability:* baseline file is JSON in git history; each PR
|
|
approval becomes a diff to that file. The free-text
|
|
justification lives in the PR thread per PRD 0012.
|
|
- *Fallback to allowlist for canonical cases.* If a particular
|
|
fixture file should be permanently understood as "examples only,"
|
|
the user can promote a baseline entry to an `[allowlist]` rule
|
|
with `paths + regexes` AND — explicit generalization, opt-in by
|
|
the user, never by the agent.
|
|
- *Open: TTL.* Should baseline entries expire? Baseline is specific
|
|
by construction, so the case for expiration is weaker than for
|
|
allowlist. Lean "never" for v1; revisit if baselines balloon.
|
|
|
|
**Option C — Pre-flight scan with author signoff.** Run gitleaks
|
|
client-side inside the bottle (as a non-gating advisory check) so
|
|
the agent sees findings *before* attempting the push. The slash
|
|
command then includes the pre-known findings; the dashboard shows
|
|
the user the finding inline rather than having to go look at the
|
|
rejection log. On approval, same Option-B-style baseline entry
|
|
gets added.
|
|
|
|
- *Property:* identical end-state to Option B; better UX because
|
|
the agent stops before the rejected push, not after.
|
|
- *Cost:* one more place that needs gitleaks installed (the bottle
|
|
image), and an in-bottle advisory check that the agent can in
|
|
principle ignore. That's fine because it's *advisory* — the gate
|
|
still rejects; the in-bottle check just avoids one round-trip.
|
|
- *Verdict:* nice-to-have over Option B, not a substitute.
|
|
|
|
### Recommendation
|
|
|
|
Default to Option A as the canonical answer ("rewrite to use a
|
|
placeholder"). Build Option B as the PRD 0012 exception path,
|
|
storing approvals in the gate's **baseline file** (not in an
|
|
allowlist rule). Baseline is the right primitive because each
|
|
approval is an attestation about one observed finding, not a
|
|
generative pattern. Allowlist promotion is a separate, user-
|
|
initiated escalation for cases that genuinely deserve patterning.
|
|
The `commits` selector is never exposed to the approval flow under
|
|
either path — it hides arbitrary content. Defer Option C to a
|
|
follow-up; it's an ergonomic win, not a security property.
|
|
|
|
This puts the answer to PRD 0012's open question as:
|
|
|
|
- Same recovery shape (`/request-bottle-change`), distinguishable
|
|
request type. The dashboard renders an exception request
|
|
differently from a manifest-change request because the *diff*
|
|
being approved is to the gate's baseline file, not to the
|
|
manifest.
|
|
- Exceptions are expressed as baseline-file entries — finding-
|
|
specific JSON records — not commit SHAs or regex patterns.
|
|
- The approval is recorded twice for audit: in the PR thread
|
|
(free-text justification), and as a versioned diff to the
|
|
baseline file (which is committed alongside the manifest).
|
|
|
|
## Should we switch to Betterleaks?
|
|
|
|
Not yet. Worth tracking. The wins are real on paper, but a
|
|
security gate is the wrong place to bet on a three-month-old
|
|
project, and the vendor-side benchmark numbers (98.6% recall vs
|
|
gitleaks's 70.4% on CredData) have not been independently
|
|
reproduced in published sources.
|
|
|
|
### What Betterleaks would add for bot-bottle
|
|
|
|
- **Detection coverage on encoded secrets.** Native handling of
|
|
doubly- and triply-encoded matches. This matters in the
|
|
threat model: an agent coaxed into base64-wrapping a token to
|
|
slip past the gate is a plausible attack, and gitleaks's
|
|
entropy-based approach misses many encodings.
|
|
- **CEL filters instead of the TOML allowlist struct.** More
|
|
expressive than `paths + regexes + condition`. Doesn't unlock
|
|
anything fundamental, but cleaner if exception rules ever need
|
|
conjunctive logic ("allow if path matches X *and* line contains
|
|
a documented placeholder string").
|
|
- **Agent-aware output.** Flag-based, low-token-overhead CLI
|
|
output designed for an AI agent (like one running inside a
|
|
bottle) to consume. Useful for the `/request-gate-exception`
|
|
slash command's parsing path; ergonomic win, not security-
|
|
load-bearing.
|
|
- **Avoids the frozen-upstream problem.** Gitleaks is feature-
|
|
complete, so a migration is eventually forced; the question is
|
|
whether to pay the cost now or later.
|
|
|
|
### What it would cost
|
|
|
|
- The existing pre-receive hook calls `gitleaks git
|
|
--log-opts=<range> --no-banner --redact`. Betterleaks's CLI
|
|
surface is similar but not identical and was not designed as a
|
|
drop-in for that specific invocation. Some hook rewrite is
|
|
likely.
|
|
- Whether Betterleaks has a baseline-file equivalent (the
|
|
storage format Option B recommends) is unconfirmed at the time
|
|
of writing. If it does not, Option B's storage format would
|
|
have to be re-derived against whatever Betterleaks offers.
|
|
- A three-month-old project has fewer security audits, fewer
|
|
third-party integrations, and a smaller community than
|
|
gitleaks has accumulated since 2018. The gate is exactly where
|
|
that asymmetry matters most.
|
|
|
|
### Criteria to revisit
|
|
|
|
Revisit when at least two of the following are true:
|
|
|
|
- Betterleaks has accumulated ~12 months of stable releases and
|
|
at least one external security audit.
|
|
- The CredData benchmark numbers have been independently
|
|
reproduced.
|
|
- A baseline-file equivalent (or a clearly better primitive for
|
|
per-finding approval storage) is shipped and documented.
|
|
- Gitleaks releases a security patch we cannot apply because the
|
|
underlying issue is a design choice rather than a bug — i.e.
|
|
the frozen status starts to bite.
|
|
|
|
### Forward-compatibility for the approval flow
|
|
|
|
Independent of the switching decision, Option B should treat the
|
|
choice of scanner as substitutable. Practically: the approval-
|
|
flow contract is "an approval is a finding-specific JSON record
|
|
stored alongside the manifest"; the *format* of that record
|
|
(gitleaks baseline schema today, something else later) is a
|
|
serialization concern downstream of the contract. Swapping
|
|
scanners then becomes a serialization migration, not a flow
|
|
redesign.
|
|
|
|
## Cross-references
|
|
|
|
- PRD 0008 — git-gate design and "no bypass" non-goal.
|
|
- PRD 0010 — cred-proxy; the inbound endpoint PRD 0012 reuses for
|
|
exception requests.
|
|
- PRD 0012 — stuck-agent recovery flow; the open question this note
|
|
informs.
|
|
- `docs/research/git-secret-scanning-hardening.md` — prior research
|
|
on the secret-scanning tool landscape and why gitleaks is the fit.
|
|
|
|
## Sources
|
|
|
|
- [gitleaks repository](https://github.com/gitleaks/gitleaks) —
|
|
`[allowlist]` selectors (`paths`, `regexes`, `stopwords`,
|
|
`commits`, `regexTarget`, `condition`); also home of the
|
|
feature-complete notice.
|
|
- [Gitleaks allowlists & baselines (DeepWiki)](https://deepwiki.com/gitleaks/gitleaks/4.4-allowlists-and-baselines)
|
|
— detailed walk-through of the allowlist selector struct, the
|
|
baseline file format, the `IsNew` comparison logic, and the
|
|
global→rule→baseline detection order. Primary source for the
|
|
allowlist-vs-baseline distinction this note rests on.
|
|
- [Betterleaks (GitHub)](https://github.com/betterleaks/betterleaks)
|
|
— Zachary Rice's successor project; CEL filtering, agent-driven
|
|
output design, roadmap for LLM-assisted classification.
|
|
- [Help Net Security on Betterleaks](https://www.helpnetsecurity.com/2026/03/19/betterleaks-open-source-secrets-scanner/)
|
|
and [The New Stack](https://thenewstack.io/betterleaks-open-source-secret-scanner/)
|
|
— context on the "agentic era" framing and why gitleaks froze.
|
|
- [DefectDojo gitleaks parser](https://defectdojo.com/integrations/gitleaks)
|
|
— JSON ingest, finding triage UI, accept/false-positive state.
|
|
Open-source, generic, post-hoc; informational state only —
|
|
marking a finding as accepted does not feed back into the
|
|
scanner. Shape mismatch for in-flight per-bottle approval.
|
|
- [gitleaks-findings/gitleaks](https://github.com/gitleaks-findings/gitleaks)
|
|
— JupiterOne integration, not a dashboard. Listed because the
|
|
repo name is misleading.
|
|
- [AWS example access key (`AKIAIOSFODNN7EXAMPLE`)](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html)
|
|
— documented placeholder safe to use in examples without
|
|
triggering most secret scanners.
|
|
- `bot_bottle/git_gate.py` — pre-receive hook implementation.
|
|
Today: `gitleaks git --log-opts="$log_opts" --no-banner
|
|
--redact`; no `--config`, no `--baseline-path`.
|