diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index ee9961c..a5a8512 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -41,12 +41,12 @@ Requirements for initial release. Each maps to roadmap phases. ### Verification -- [ ] **VRFY-01**: Active key verification via lightweight API calls when --verify flag is set +- [x] **VRFY-01**: Active key verification via lightweight API calls when --verify flag is set - [x] **VRFY-02**: Verification is opt-in only (off by default) with consent prompt on first use - [x] **VRFY-03**: Each provider YAML defines verify endpoint, method, headers, success/failure codes - [ ] **VRFY-04**: Verification extracts additional metadata (org, rate limit, permissions) when available - [ ] **VRFY-05**: Configurable verification timeout (default 10s, --verify-timeout flag) -- [ ] **VRFY-06**: Legal disclaimer and documentation ships with verification feature +- [x] **VRFY-06**: Legal disclaimer and documentation ships with verification feature ### Output & Reporting diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 2ef669c..bf31813 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -124,7 +124,7 @@ Plans: Plans: - [x] 05-01-PLAN.md — Wave 0: extend VerifySpec schema, Finding struct, storage schema; add gjson dep -- [ ] 05-02-PLAN.md — LEGAL.md + pkg/legal embed + consent prompt + keyhunter legal command +- [x] 05-02-PLAN.md — LEGAL.md + pkg/legal embed + consent prompt + keyhunter legal command - [ ] 05-03-PLAN.md — pkg/verify HTTPVerifier: template sub, gjson metadata extraction, ants pool - [x] 05-04-PLAN.md — Update 12 Tier 1 provider YAMLs with extended verify specs + guardrail test - [ ] 05-05-PLAN.md — cmd/scan.go --verify wiring + --verify-timeout/workers flags + output verify column diff --git a/.planning/STATE.md b/.planning/STATE.md index bbda817..148d173 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,14 +3,14 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone status: executing -stopped_at: Completed 05-04-PLAN.md -last_updated: "2026-04-05T12:48:10.887Z" +stopped_at: Completed 05-02-PLAN.md +last_updated: "2026-04-05T12:48:48.980Z" last_activity: 2026-04-05 progress: total_phases: 18 completed_phases: 4 total_plans: 28 - completed_plans: 25 + completed_plans: 26 percent: 20 --- @@ -26,7 +26,7 @@ See: .planning/PROJECT.md (updated 2026-04-04) ## Current Position Phase: 05 (verification-engine) — EXECUTING -Plan: 3 of 5 +Plan: 4 of 5 Status: Ready to execute Last activity: 2026-04-05 @@ -71,6 +71,7 @@ Progress: [██░░░░░░░░] 20% | Phase 04 P05 | 3min | 1 tasks | 2 files | | Phase 05 P01 | 3m43s | 2 tasks | 10 files | | Phase 05 P04 | 10m | 2 tasks | 25 files | +| Phase 05-verification-engine P02 | 7m | 2 tasks | 9 files | ## Accumulated Context @@ -95,6 +96,8 @@ Recent decisions affecting current work: - [Phase 04]: Introduced selectSource dispatcher with sourceFlags struct for testable CLI source routing - [Phase 05]: Keep legacy VerifySpec ValidStatus/InvalidStatus alongside canonical SuccessCodes/FailureCodes; Effective*() helpers pick canonical-first with fallback - [Phase 05]: Store Finding.VerifyMetadata as JSON TEXT column; legacy DBs migrated in-place via PRAGMA table_info + conditional ALTER TABLE in storage.Open() +- [Phase 05-verification-engine]: LEGAL.md dual-location mirror (root + pkg/legal/) required because go:embed cannot traverse parents — mirrors Phase 1 providers pattern +- [Phase 05-verification-engine]: verify.consent setting: granted is sticky across runs; declined is not — users who initially refuse can change mind without manual reset ### Pending Todos @@ -109,6 +112,6 @@ None yet. ## Session Continuity -Last session: 2026-04-05T12:48:10.879Z -Stopped at: Completed 05-04-PLAN.md +Last session: 2026-04-05T12:48:48.976Z +Stopped at: Completed 05-02-PLAN.md Resume file: None diff --git a/.planning/phases/05-verification-engine/05-02-SUMMARY.md b/.planning/phases/05-verification-engine/05-02-SUMMARY.md new file mode 100644 index 0000000..f507d96 --- /dev/null +++ b/.planning/phases/05-verification-engine/05-02-SUMMARY.md @@ -0,0 +1,151 @@ +--- +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