Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
---
status: Proposed
date: 2026-08-13
issue: launchpad-26/buzz#63
decided_in: launchpad-26/buzz#63
supersedes: none
---

# ADR-0006 — Secret-scanning engine for the fork, and where its allowlist lives

## Decision

The fork's secret-detection engine is **gitleaks**, invoked both on the PR-diff path and
on a full-history scan on the scheduled path, per #62. Detection rules and allowlist live
together in a single `.gitleaks.toml` at the repo root, extending gitleaks' default
ruleset (`useDefault = true`) with fork-specific rules for the material stock rulesets do
not know about: Nostr `nsec`/hex private keys, `BUZZ_PRIVATE_KEY`, Unix/glibc crypt hashes
(`$1$`/`$5$`/`$6$`/`$y$`), and `BUZZ_S3_*` / Postgres-URL-with-embedded-password shapes.

The allowlist is **hand-maintained TOML `[allowlist]` / per-rule `allowlist` blocks in
`.gitleaks.toml`, each entry carrying a `#` comment stating why it's safe** — the same
shape as the `.intersect/sadscan.yaml` precedent #62 points to. Gitleaks' other allowlist
mechanism, an auto-generated `--baseline-path` JSON snapshot of accepted fingerprints, is
explicitly **not** used: it has no field for a reason, and #63's own decision drivers flag
it as "a loaded footgun: regenerated carelessly, it silently accepts a real finding." A
TOML block requires a human to type a reason; a regenerated baseline requires nothing.

This decision covers the CI-side detection engine only. It does **not** decide whether
GitHub's native secret scanning + push protection gets enabled — that is a repository
setting requiring admin, tracked separately (see Context).

## Context

No secret detection runs on this fork today. `.intersect/sadscan.yaml` configures a
scanner driven by Block-internal CI that never triggers here — `grep -rn
"sadscan\|intersect" .github/ Justfile` returns nothing, confirmed in #62's own evidence.

Three options were considered and rejected in #63 as filed, on grounds independent of
anything tested below:

- **TruffleHog** — rejected because its core feature, live credential verification, sends
candidate secrets to third-party APIs (documented example: an AWS credential detector
calling `GetCallerIdentity`). For a repo whose likeliest findings are live host and
database credentials, that turns a detector into an egress channel. The AGPL-3.0 licence
was noted for the record, not the basis for rejection.
- **GitHub secret scanning + push protection as the sole answer** — rejected as the *only*
answer because enabling it needs repository admin, which this work does not have. It
remains valuable as a complementary, *preventive* control (see below).
- **Hand-rolled grep/regex script** — rejected for no history-walk, no baseline/allowlist
semantics, and making the cohort maintainer of a bespoke scanner.

That left gitleaks as the only candidate meeting every driver (local, no network egress,
history-capable, TOML rules and allowlist reviewable in-repo, one local command). Rather
than accept that conclusion on paper, it was run directly against this repository before
being decided:

- **Full-history scan**: `gitleaks detect --source .` completed in **12.4s** over 5,537
commits / 102.55 MB — comfortably inside #62's 3-minute PR budget, and this is the
*slower* of the two scan modes.
- **False-positive rate, measured, not assumed**: that same scan surfaced **65 findings**
on real repo history, nearly all plausible false positives at a glance (test-fixture
tokens, example JWTs, an example `nsec1abc123def456` in documentation). This is direct
evidence for the allowlist requirement, not a restatement of the issue's prose.
- **Default-ruleset gap, measured**: a fixture file was staged in a throwaway scratch repo
containing four planted secrets shaped like this cohort's real material — a `BUZZ_PRIVATE_KEY=nsec1…`
value, a `$6$rounds=5000$…` crypt hash, `BUZZ_S3_ACCESS_KEY`/`BUZZ_S3_SECRET_KEY`
values, and a `postgres://user:pass@host/db` URL — then scanned with `gitleaks protect
--staged` using gitleaks' default rules only. **Only 1 of 4 was caught**, and only
incidentally, via the generic high-entropy rule rather than anything Nostr-aware. The
hash, the S3 credentials, and the Postgres URL all passed through undetected. This
confirms #63's decision driver ("must cover material stock rulesets do not know about")
is not a hypothetical — it is the default behavior.
- **Custom-rule feasibility, measured**: a minimal `.gitleaks.toml` with four fork-specific
rules was written and re-run against the same fixture. The S3-credential and
Postgres-URL rules caught their targets on the first pass. The Nostr and crypt-hash
rules did not, on the first attempt — the planted fixture's `nsec` string wasn't valid
bech32 (it contained characters bech32 excludes), and the crypt-hash regex didn't
account for glibc's optional `rounds=N$` segment. Both are fixable, ordinary regex
iteration — the real cost is that iteration itself, not a blocked path. Writing that
ruleset correctly is scoped to #67, not this ADR; this ADR only needed to confirm the
gap is closeable, which it is.

**On GitHub's native secret scanning + push protection**: #63 as filed rejected this as
the sole answer because it "needs repository admin, which this work does not have."
That's true of the account doing this work, but the cohort's PM does hold admin on this
repo. That reopens a door #63 and #65 (a related, still-open ADR, about the *audit
workflow's own CI token* — a different question, still admin-less either way) both treated
as closed. Push protection is the only *preventive* control available anywhere in this
PRD — everything gitleaks does here is detective, catching material after it's already
committed. Requesting the PM enable it is now a live, actionable ask, tracked under #72
("request and verify the admin-only repository security settings"), not a permanently
blocked item. It does not change this ADR's engine choice: GitHub's native scanning still
doesn't cover `$6$` hashes or Nostr keys without custom patterns (themselves admin-gated to
configure), and isn't reproducible locally by an agent the way gitleaks is. The two are
complementary, exactly as #63 as filed anticipated — this ADR just confirms the admin path
is reachable via a different person, not that it changes what CI runs.

## Consequences

**Good.** One engine, one rule format, rules and allowlist reviewable in a PR diff
alongside the code they protect. A local `gitleaks detect`/`gitleaks protect --staged` run
reproduces CI exactly — verified, not assumed, since that is literally what was run above.
Nothing leaves the runner: no third-party API calls, no verification step to accidentally
enable later. Measured startup cost is low enough that #62's 3-minute budget is not a
concern for this piece of the audit.

**Bad, stated honestly.** The 65-finding run against real history is concrete proof that
every finding needs human triage — a green run only means "no *new* uncaught pattern,"
never "no secrets." The allowlist is the soft spot: an over-broad entry silences detection
without looking like it changed anything, which is why entries must be scoped to a rule
or a specific match, never a whole file or path, and each must carry a reason. Writing
rules that correctly match this cohort's real material (valid bech32 charset, glibc's
optional `rounds=` segment, and whatever else surfaces) is real, ongoing work — this ADR's
own first-attempt rules needed a second pass, and #67's eventual ruleset should expect the
same. And because this is a regex-only engine with no liveness signal, a genuinely novel
secret shape not yet written into a rule will pass silently — planted-fixture proof per
issue, not a one-time check, is why #62 demands it as an ongoing success criterion rather
than a launch-day checkbox.

## Provenance

This decision was made directly while working #63 — `issue` and `decided_in` point to the
same place.

Verified live, on this machine, against this repository, rather than assumed from #63's
filed text:
- gitleaks 8.30.1 full-history scan timing and finding count (5,537 commits, 102.55 MB,
12.4s, 65 findings) — reproducible with `gitleaks detect --source . --no-banner -v`.
- The default-ruleset gap against four planted, cohort-shaped fake secrets in an isolated
scratch git repository (never committed to this repository) — reproducible with
`gitleaks protect --staged --no-banner -v` against a staged file containing an
`nsec1…` value, a `$6$rounds=…` hash, `BUZZ_S3_ACCESS_KEY`/`SECRET_KEY` values, and a
`postgres://user:pass@host/db` URL.
- That a minimal custom `.gitleaks.toml` closes part of that gap immediately, and that the
remaining gap is a regex-correctness problem, not a capability gap.

Not verified independently: TruffleHog's and GitHub secret scanning's own detection
behavior against the same fixture — their rejection rests on #63's documented licence,
egress, and privilege grounds, which were not re-tested, only gitleaks' fitness was
verified hands-on.

This decision directly shapes #67 (the secret-detection task under #62, which owns writing
and hardening the actual `.gitleaks.toml` ruleset — the draft rules used here are a
starting point, not the final ruleset) and #72 (which should now carry the specific,
actionable ask that the PM enable GitHub push protection, rather than treating it as
blocked).
145 changes: 145 additions & 0 deletions launchpad/decisions/ADR-0007-dependency-update-path.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
---
status: Proposed
date: 2026-08-13
issue: launchpad-26/buzz#64
decided_in: launchpad-26/buzz#64
supersedes: none
---

# ADR-0007 — Dependency update path for this fork

## Decision

The fork's dependency-update path is **native GitHub Dependabot version updates via
`.github/dependabot.yml`** (option 2 of #64 as filed), covering `cargo` (root), `npm`
(`desktop/`, `web/`), `pub` (`mobile/`), `docker` (root), and `github-actions` (root) —
six ecosystem entries, one per manifest location. Renovate (option 1) is **not chosen**,
and is currently **unreachable**, not merely undesirable — see Context. Neither option 3
(neutralize and rely on upstream) nor option 4 (both bots) is chosen, for the reasons
already given in #64 as filed.

`renovate.json` stays in the tree, unedited, and is declared **upstream-owned and inert**
by this ADR — the second resolution #62's own success criteria names as acceptable. It is
not deleted: as an upstream-tracked file, deletion would likely be silently reintroduced
on the next sync from `block/buzz` rather than stick. Its `"automerge": true` therefore
governs nothing on this fork; that must not be re-read as an active supply-chain risk here,
only on `block/buzz` itself.

This ADR decides the update path only. Building `.github/dependabot.yml` for real, and
demonstrating at least one live update PR per #62's success criteria, is #71 — not done
here, matching the split #63 already established between deciding and implementing.

## Context

Before this ADR, the fork had no dependency-update or alerting path of any kind, while
carrying `renovate.json` — inherited from upstream, `"automerge": true` — that reads as
though it does. Re-verified live rather than assumed from #64 as filed:

```
$ gh api repos/launchpad-26/buzz/dependabot/alerts
{"message":"Dependabot alerts are disabled for this repository.", ...} (HTTP 403)
```

No `.github/dependabot.yml` exists. `cargo-deny check` runs at `.github/workflows/ci.yml:900`
(filed against line 887 — the file has grown ~13 lines since #64 was written; the check
itself is unchanged, covering Rust advisories only).

#64 as filed left one thing explicitly unverified: "whether the Renovate app can be
installed on the `launchpad-26` org, and by whom." That question was resolved, not assumed,
before this decision:

```
$ gh api orgs/launchpad-26/memberships/benmitchell11
{"role": "member", ...}

$ gh api orgs/launchpad-26/members?role=admin
[{"login": "baradev", ...}, {"login": "jatin-puri-coder", ...}, {"login": "joshuavial", ...}]
```

Installing a GitHub App at the org level needs org **owner**, not repository admin — a
stricter bar than anything else this PRD has needed so far (compare ADR-0006's push
protection, which only needs repo admin). This account holds org role `member`, the same
constraint issue #65 (a related, still-open ADR) already names for GitHub App creation,
applying here to GitHub App *installation* instead.

Critically, this was checked against the actual person available to this cohort: the
project's PM holds **repository admin on `buzz` only, not organization owner**. The three
accounts holding org owner are unrelated to this cohort's chain of contacts, with one
exception — one of them is this cohort's course instructor, a materially different, heavier
ask than a routine dependency-tooling decision. Option 1 is therefore not "harder" than
option 2, it is **closed to this cohort as currently staffed**, the same way issue #65 (a
related, still-open ADR) names a GitHub App as unreachable for a different reason (org
role, not admin-read scope). This ADR
does not rule out revisiting Renovate if that staffing changes; it rules it out as a choice
available today.

That leaves option 2, which #64 as filed already noted needs no admin for version updates
(only Dependabot's *alerts* feature is admin-gated, and this ADR does not enable that — it
remains #62's admin-settings track, tracked at #72). A draft six-entry `dependabot.yml`
(cargo, npm ×2, pub, docker, github-actions) was written and validated as structurally
correct (`yaml.safe_load` parses it, all six manifest directories confirmed to exist:
`Cargo.toml`, `desktop/package.json`, `web/package.json`, `mobile/pubspec.yaml`, three
root `Dockerfile*` files, `.github/workflows/`). It was not committed here — that draft,
and confirming whether Dependabot's `docker` ecosystem watches multiple differently-named
Dockerfiles in one directory or needs one entry per file, is #71's work.

Options 3 and 4 were not re-litigated: #64 as filed already gives a sufficient, honest
account of both (3 names the current inert state as exactly the failure #62 exists to
catch; 4 creates duplicate PR traffic against manifests the cohort already doesn't fully
own). Nothing tested here changes either conclusion.

## Consequences

**Good.** The fork gains a real, self-serve dependency-update path today, with no
dependency on anyone's availability or role beyond this cohort's own write access —
verified reachable, not assumed. `renovate.json` stops being silently misread as active
protection: this ADR is the written resolution #62's success criteria requires, on record
rather than left for the next reader to guess. The path covers every ecosystem #64
identified (Cargo, pnpm ×2, Flutter/pub, Docker, Actions), matching the coverage Renovate
would have offered, without the third-party App write-access grant Renovate would have
required.

**Bad, stated honestly.** Dependabot version updates do not deliver vulnerability
*alerts* — that gap stays open regardless of this decision and belongs to #72, not here.
PR volume against manifests this fork mostly doesn't own, on CI that runs 24–41 minutes, is
a real and unmeasured cost; #64 as filed named this risk and nothing here reduces it — if
anything, native Dependabot's weekly-per-ecosystem default (six entries) could generate
more individual PRs than a single tuned Renovate config would have, since the draft config
does not reuse Renovate's existing grouping rules (e.g. `renovate.json`'s Rust minor-version
isolation, redis/deadpool-redis pairing). Whoever builds #71 should decide grouping
explicitly rather than let six independent weekly runs become six independent PR floods.
Rejecting Renovate on access grounds also means the fork gives up the specific tuning
already written into `renovate.json` (cargo lockfile updates, digest-pinned Actions,
per-package pins for `evalexpr` and `tiptap`) — none of that carries over to Dependabot
automatically; #71 inherits the job of deciding which of it still matters.

## Provenance

Like ADR-0006, made directly while working #64 — `issue` and `decided_in` point to the
same place.

Verified live against this repository and this GitHub org, not assumed from #64 as filed:
- `renovate.json` content, including `"automerge": true`, re-read directly from the
working tree.
- Dependabot alerts status via `gh api repos/launchpad-26/buzz/dependabot/alerts` —
explicitly disabled, not an ambiguous 404.
- `cargo-deny check`'s current line number in `ci.yml`, confirming the one working
advisory check is unchanged since #64 was filed.
- This account's org role (`member`) and the org's three actual owners, via
`gh api orgs/launchpad-26/memberships/benmitchell11` and
`gh api orgs/launchpad-26/members?role=admin` — resolving #64's own explicitly-named
open question about who can install Renovate, rather than leaving it open.
- That the six proposed `dependabot.yml` ecosystem entries correspond to real manifest
paths in this repository, and that the draft file parses as valid YAML.

Not verified: actual PR volume or noise from either path — #64 as filed already named this
as unmeasured, and nothing here measures it; that evidence can only come from #71 actually
running. Not verified: whether the PM's repository-admin role could be escalated to org
owner by request — only the PM's *current* role was established, not whether it could
change. Not verified: Dependabot's exact `docker` ecosystem behavior against multiple
differently-named Dockerfiles in one directory — left for #71.

This decision directly shapes #71 (builds the real `.github/dependabot.yml`, decides
grouping/scheduling the draft here left unresolved, and must produce the live update PR
#62's success criteria demands) and #72 (owns the Dependabot *alerts* admin-gated half this
ADR explicitly does not close).
54 changes: 54 additions & 0 deletions launchpad/scripts/test-adr-0006-0007-renumbering.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# Verifies the fixes from Serina's PR #161 review: correct sequential ADR
# numbers (0006/0007, coordinating with #152's 0005), no stale ADR-0065
# citations to a decision that doesn't exist yet, and status: Proposed
# rather than Accepted while the PR is still open.
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DECISIONS_DIR="${SCRIPT_DIR}/../decisions"

PASS=0
FAIL=0

check() {
local label=$1 ok=$2
if [ "${ok}" = "0" ]; then
echo "PASS: ${label}"
PASS=$((PASS + 1))
else
echo "FAIL: ${label}"
FAIL=$((FAIL + 1))
fi
}

status=1
[ -f "${DECISIONS_DIR}/ADR-0006-secret-scanning-engine-and-allowlist-location.md" ] && status=0
check "ADR-0006 (secret scanning) exists at the correct sequential number" "${status}"

status=1
[ -f "${DECISIONS_DIR}/ADR-0007-dependency-update-path.md" ] && status=0
check "ADR-0007 (dependency update path) exists at the correct sequential number" "${status}"

status=1
[ ! -f "${DECISIONS_DIR}/ADR-0063-secret-scanning-engine-and-allowlist-location.md" ] \
&& [ ! -f "${DECISIONS_DIR}/ADR-0064-dependency-update-path.md" ] && status=0
check "the old issue-numbered filenames (0063/0064) are gone" "${status}"

status=0
grep -rq "ADR-0063\|ADR-0064\|ADR-0065" "${DECISIONS_DIR}/ADR-0006-secret-scanning-engine-and-allowlist-location.md" \
"${DECISIONS_DIR}/ADR-0007-dependency-update-path.md" && status=1
check "no stale ADR-0063/0064/0065 references remain in either file" "${status}"

status=1
grep -q "^status: Proposed$" "${DECISIONS_DIR}/ADR-0006-secret-scanning-engine-and-allowlist-location.md" && status=0
check "ADR-0006 status is Proposed, not Accepted, while the PR is open" "${status}"

status=1
grep -q "^status: Proposed$" "${DECISIONS_DIR}/ADR-0007-dependency-update-path.md" && status=0
check "ADR-0007 status is Proposed, not Accepted, while the PR is open" "${status}"

echo ""
echo "==================================================="
echo "${PASS} passed, ${FAIL} failed"
[ "${FAIL}" -eq 0 ]
Loading