--- phase: 05-verification-engine plan: 02 subsystem: verification-consent tags: [legal, consent, embed, cli, verify] requirements: [VRFY-01, VRFY-06] dependency-graph: requires: - "pkg/storage settings table GetSetting/SetSetting (Phase 1)" - "cmd/root.go cobra command tree (Phase 1)" provides: - "pkg/legal.Text() — compile-time embedded LEGAL.md" - "pkg/verify.EnsureConsent(db, in, out) — consent gate for --verify" - "keyhunter legal subcommand" - "verify.consent setting key contract" affects: - "cmd/root.go (new subcommand registration)" tech-stack: added: [] patterns: - "go:embed dual-location mirror (root LEGAL.md + pkg/legal/LEGAL.md), same pattern as providers/ vs pkg/providers/definitions/" - "Sticky-granted, transient-declined consent semantics (granted persists, declined re-prompts)" - "io.Reader/io.Writer injection for testable interactive prompts" key-files: created: - "LEGAL.md" - "pkg/legal/LEGAL.md" - "pkg/legal/legal.go" - "pkg/legal/legal_test.go" - "cmd/legal.go" - "pkg/verify/consent.go" - "pkg/verify/consent_test.go" - ".planning/phases/05-verification-engine/05-02-SUMMARY.md" modified: - "cmd/root.go" decisions: - "LEGAL.md duplicated at repo root and pkg/legal/ because go:embed cannot traverse parents" - "verify.consent setting: 'granted' is sticky, 'declined' is not — users who initially refuse can change their mind on the next run without manual reset" - "EnsureConsent takes io.Reader/io.Writer rather than reading os.Stdin/os.Stdout directly, so tests can drive it with strings.NewReader + bytes.Buffer" - "Consent prompt text explicitly mentions CFAA, Computer Misuse Act, GDPR, provider ToS and directs users to `keyhunter legal` for the full document" metrics: duration: "~7m" completed_date: "2026-04-05" tasks_completed: 2 files_created: 8 files_modified: 1 --- # Phase 5 Plan 2: Legal Disclaimer & Consent Prompt Summary **One-liner:** Shipped LEGAL.md embedded in the binary via go:embed, added a `keyhunter legal` subcommand, and implemented `verify.EnsureConsent` with sticky-granted / transient-declined semantics backed by the SQLite settings table. ## What Was Built ### LEGAL.md (repo root + pkg/legal mirror) A 109-line user-facing disclaimer covering: - **Purpose** — what the verification feature does at a high level - **What --verify Does** — lightweight single HTTP call per key, no mutations, masked output - **Legal Considerations** — CFAA (18 U.S.C. § 1030), UK Computer Misuse Act 1990, EU Directive 2013/40/EU, provider ToS - **Responsible Use** — own keys, authorized engagements, own CI/CD only - **Responsible Disclosure** — security.txt, never publish, reasonable rotation window - **Disclaimer** — AS IS, no warranty, user bears sole responsibility - **Consent Record** — explains the first-run prompt and where the decision is persisted The root copy is what users read; `pkg/legal/LEGAL.md` is a byte-identical mirror required by `//go:embed` (Go embed directives cannot traverse `..`). This mirrors the Phase 1 decision to keep `providers/*.yaml` user-visible with a `pkg/providers/definitions/*.yaml` embed mirror. ### pkg/legal package ```go //go:embed LEGAL.md var legalMarkdown string func Text() string { return legalMarkdown } ``` Two tests: `TestText_NonEmpty` (>500 bytes) and `TestText_ContainsKeyPhrases` (contains "CFAA", "Responsible Use", "Disclaimer"). Both pass. ### keyhunter legal command `cmd/legal.go` registers a trivial `legalCmd` that prints `legal.Text()` to stdout. Wired in `cmd/root.go` alongside the existing `scanCmd`, `providersCmd`, `configCmd`. `go run . legal | grep CFAA` returns a match. ### pkg/verify.EnsureConsent ```go func EnsureConsent(db *storage.DB, in io.Reader, out io.Writer) (bool, error) ``` Behavior: 1. Reads `verify.consent` from the settings table. 2. If previously `"granted"` → returns `(true, nil)` immediately, writes nothing. 3. Otherwise writes a multi-line warning to `out` referencing "legal implications", CFAA, Computer Misuse Act, GDPR, provider ToS, and pointing the user to `keyhunter legal`. 4. Reads one line from `in`, lowercases and trims. 5. If the answer is `"yes"` → persists `"granted"` and returns `(true, nil)`. 6. Any other input (including `"y"`, `"no"`, empty) → persists `"declined"` and returns `(false, nil)`. 7. Declined is **not** sticky — the next call re-prompts (found && value != "granted" falls through to the prompt branch). Six test cases cover the full decision matrix: - `TestEnsureConsent_GrantedPrevious` — sticky short-circuit with no prompt output - `TestEnsureConsent_TypeYes` — prompts, persists granted, prompt text contains "legal implications" and "keyhunter legal" - `TestEnsureConsent_TypeYesUppercase` — case-insensitive match - `TestEnsureConsent_TypeNo` — persists declined, returns false - `TestEnsureConsent_Empty` — empty input treated as decline - `TestEnsureConsent_DeclinedNotSticky` — seed declined + type yes → re-prompted and now granted All 6 pass. ## TDD Trace (Task 2) - **RED** (`e5f7214`) — wrote `consent_test.go` referencing `ConsentSettingKey`, `ConsentGranted`, `ConsentDeclined`, and `EnsureConsent` before the implementation existed. Build failed with "undefined:" errors for all 5 symbols. ✅ confirmed failing. - **GREEN** (`d4c1403`) — created `consent.go` with the constants and function. One iteration needed: the initial prompt wrapping split "legal implications" across two lines ("legal\nimplications"), which broke the literal substring assertion in `TestEnsureConsent_TypeYes`. Rewrapped the prompt text to keep the phrase contiguous on one line. All 6 tests green. - **REFACTOR** — none needed; implementation matched the plan's reference code modulo the prompt rewrap. ## Verification ``` $ go build ./... (clean) $ go test ./pkg/legal/... ./pkg/verify/... ok github.com/salvacybersec/keyhunter/pkg/legal 0.002s ok github.com/salvacybersec/keyhunter/pkg/verify 0.331s $ go run . legal | grep -c CFAA 1 ``` Must-haves truths verified: - ✅ `keyhunter legal` prints the embedded LEGAL.md to stdout (`grep -c CFAA` returns 1) - ✅ First invocation prompts; "yes" (case-insensitive) proceeds; anything else aborts (6 tests) - ✅ Consent decision stored in settings and not re-prompted when granted (`TestEnsureConsent_GrantedPrevious`) - ✅ LEGAL.md ships via `go:embed` (`grep -q "go:embed LEGAL.md" pkg/legal/legal.go` — present) ## Commits | Hash | Type | Summary | |----------|---------|---------| | 260e342 | feat | Add LEGAL.md, pkg/legal embed, and `keyhunter legal` command | | e5f7214 | test | RED: failing consent tests | | d4c1403 | feat | GREEN: implement EnsureConsent | ## Deviations from Plan None — plan executed as written. One minor in-task iteration: the prompt text in the reference code happened to wrap "legal implications" across two `Fprintln` calls, which broke the test assertion on literal substrings. Rewrapped the prompt lines to keep "legal implications" contiguous. Functionally identical; no new behavior. ## Out-of-Scope Note During verification I observed an uncommitted modification to `pkg/verify/verifier.go` unrelated to this plan — it contains an in-progress `HTTPVerifier` implementation from what appears to be Plan 05-03 work (the git log shows `3ceccd9 test(05-03): add failing tests for HTTPVerifier single-key verification`). I deliberately did not touch that file; it will be committed by plan 05-03 executor. ## Self-Check: PASSED All 8 claimed files exist on disk. All 3 claimed commits exist in git log. ## Dependencies Satisfied For Next Plans - **05-03 (HTTP verifier)** — can call `verify.EnsureConsent` at the top of the scan/verify entry point once this plan lands - **05-04 (verify CLI flag integration)** — has `ConsentSettingKey`, `ConsentGranted`, `ConsentDeclined` constants to key off - **Any future plan** that needs to surface legal disclaimers or track user acknowledgements — can follow the same `pkg/legal` + dual-LEGAL.md embed pattern