chore(canary-phase7): envfile generator complete

Phase 7 ships the sixth generator in the canary registry. envfile
canary tokens are rendered .env-style text artifacts containing
shuffled bait sections (aws/stripe/github/db) plus an
INTERNAL_METRICS_ENDPOINT line carrying the actual canary URL.
The bait keys look real enough to pass gitleaks-class regex
scanners but are NOT live credentials — the detection mechanism
is the embedded canary URL.

Implementation commits in this phase:
  - 003aa997 feat(canary): envfile bait recipes (aws/stripe/github/db)
  - 2a2a3c4d feat(canary): envfile generator (shuffled bait + embedded
    canary URL)
  - 1cde61ef feat(canary): register envfile generator in registry

Deliverables:
  - envfile/recipes/ sub-package with 4 bait recipes + shared
    crypto/rand helpers (RandomAlnumUpper/RandomAlnumMixed/
    RandomHexLower/RandomBase64/RandomChoice)
  - aws.go: AKIA + 16-char [A-Z0-9] body + 40+ base64 secret + region
    + bucket
  - stripe.go: sk_live_/pk_live_/whsec_ + 24/24/32 base62 bodies
  - github.go: ghp_ + 42 base62 (36 body + 6 checksum) + base64
    deploy key
  - db.go: postgres://app_writer:<24-char>@db.internal:5432/app_prod
    + redis://default:<32-char>@cache-prod.internal:6379/0
  - envfile/generator.go: extractIncludeKeys with 7 defensive fallback
    cases (empty/invalid/wrong-type/null/empty-array/missing/whitespace)
    + Fisher-Yates shuffle on crypto/rand + canary section appended
    last before shuffle so canary URL can land at any position
  - Spec §9.6 deviation: crypto/rand throughout (vs spec's math/rand
    + rng.Shuffle) — matches Phase 0 supplement's discipline, no
    gosec G404 fight, consistent with Phase 5 (pdf) and Phase 6
    (kubeconfig)
  - Trigger: byte-identical content-copy of webbug/docx/pdf
    (200 + pixel.Clone GIF + cache headers + realIP triplet)
  - Registry expanded 5 → 6; pending list shrinks to {mysql}
    (1 remaining — Phase 8)
  - ~60 test cases across recipes + generator including 11-subcase
    IP precedence parity, 6 malformed-metadata fallback subcases,
    shuffle variability across 30 invocations, format-regex assertions
    per recipe, URL parsing for postgres/redis (real net/url validation),
    token-id-leak invariant (uniqueprobe appears exactly once)

Audit outcome (2 agents in parallel per standing pattern):
  - superpowers:code-reviewer: PASS (0 BLOCKER, 0 SHOULD-FIX, 6 NITs
    all marked informational / below-threshold / acceptable-trade)
  - general-purpose spec-adherence vs §9.6 + §8.5: PASS (0 BLOCKER,
    0 SHOULD-FIX, 2 LOW informational items — hostname pick is one of
    spec's enumerated values, extra realism keys are additions not
    violations)

No audit-fix commit needed this phase — zero actionable findings,
matching the Phase 5 pattern (Phase 6 had one actionable header-
filename NIT cleared before rollup; Phase 7 is fully clean).

Pre-rollup gate clean at HEAD 1cde61ef:
  - go build / vet clean
  - go test -race -timeout=60s ./... all packages pass
  - go test -tags=integration -race -timeout=300s ./internal/token/...
    ./internal/event/... all pass
  - golangci-lint run ./... → 0 issues.
  - grep -rn "//nolint" --include="*.go" returns nothing
  - BACKLOG.md open section still _(none)_
  - go.mod surface unchanged (no new deps for this phase)

Forward state for Phase 8 (mysql fake server): the LAST generator.
TCP wire-protocol listener (NOT HTTP) that accepts MySQL handshake,
extracts username from the auth packet, looks up token by
metadata.mysql_username, records event, returns ERR_Packet 1045.
First time we spawn a non-HTTP listener from main.go (cmd/canary
wire-up via `if cfg.MySQL.Enabled { go mysql.Run(...) }`). All
7 generators close after Phase 8.
This commit is contained in:
CarterPerez-dev 2026-05-13 14:40:37 -04:00
parent 1cde61efbb
commit 3d9b97da21

Diff Content Not Available