---
phase: 05-verification-engine
plan: 02
type: execute
wave: 1
depends_on: [05-01]
files_modified:
- LEGAL.md
- pkg/legal/legal.go
- pkg/legal/legal_test.go
- pkg/verify/consent.go
- pkg/verify/consent_test.go
- cmd/legal.go
- cmd/root.go
autonomous: true
requirements: [VRFY-01, VRFY-06]
must_haves:
truths:
- "Running `keyhunter legal` prints the embedded LEGAL.md to stdout"
- "First invocation of --verify prompts for consent; typing 'yes' (case-insensitive) proceeds; anything else aborts"
- "Consent decision is stored in the settings table and not re-prompted on subsequent runs"
- "LEGAL.md ships in the binary via go:embed (not read from filesystem at runtime)"
artifacts:
- path: "LEGAL.md"
provides: "Legal disclaimer text for verification feature"
min_lines: 40
- path: "pkg/legal/legal.go"
provides: "Embedded LEGAL.md via go:embed"
contains: "go:embed"
- path: "pkg/verify/consent.go"
provides: "EnsureConsent(db, in, out) (bool, error) — prompts once, persists decision"
contains: "EnsureConsent"
- path: "cmd/legal.go"
provides: "keyhunter legal subcommand"
contains: "legalCmd"
key_links:
- from: "pkg/verify/consent.go"
to: "pkg/storage settings table"
via: "db.GetSetting/SetSetting('verify.consent')"
pattern: "verify.consent"
- from: "pkg/legal/legal.go"
to: "LEGAL.md"
via: "go:embed"
pattern: "go:embed LEGAL.md"
---
Create the legal disclaimer document, embed it in the binary, add a `keyhunter legal` subcommand that prints it, and implement the consent prompt that gates `--verify` on first use.
Purpose: Legal safety — VRFY-01 requires a one-time consent prompt with clear language; VRFY-06 requires LEGAL.md shipping with the binary.
Output: LEGAL.md file, pkg/legal with embed, pkg/verify consent logic, `keyhunter legal` command.
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
@.planning/PROJECT.md
@.planning/phases/05-verification-engine/05-CONTEXT.md
@pkg/storage/settings.go
@cmd/root.go
@cmd/scan.go
From pkg/storage (existing settings API used by cmd/scan.go):
```go
func (db *DB) GetSetting(key string) (value string, found bool, err error)
func (db *DB) SetSetting(key, value string) error
```
Use setting key `"verify.consent"` with values `"granted"` or `"declined"`.
Task 1: Write LEGAL.md and embed it in pkg/legal, wire keyhunter legal subcommand
LEGAL.md, pkg/legal/legal.go, pkg/legal/legal_test.go, cmd/legal.go, cmd/root.go
1. Create `LEGAL.md` at the repo root. Contents must include these H2 sections (minimum 40 lines total):
- `## Purpose` — explains KeyHunter's verification feature makes HTTP calls to third-party APIs
- `## What --verify Does` — bullet list: sends single lightweight request per found key to provider's documented endpoint, does not modify any account or data, reads only metadata the API returns
- `## Legal Considerations` — covers unauthorized access laws including US CFAA (18 U.S.C. § 1030), UK Computer Misuse Act 1990, EU Directive 2013/40/EU; warn that verifying a key you do not own or have permission to test may constitute unauthorized access
- `## Responsible Use` — only verify keys you own, keys from authorized engagements (pen-test / bug bounty with explicit scope), or keys in your own CI/CD pipelines. Do NOT verify keys found in random public repositories without owner consent
- `## Responsible Disclosure` — when you find a leaked key belonging to someone else, contact the key owner directly via their security contact or security.txt; do not publish the key
- `## Disclaimer` — tool authors and contributors disclaim all liability; the user is solely responsible for compliance with applicable laws and terms of service
- `## Consent Record` — note that running `keyhunter scan --verify` the first time shows an interactive prompt; typing `yes` records consent in the local SQLite settings table
2. Create `pkg/legal/legal.go`:
```go
package legal
import _ "embed"
//go:embed LEGAL.md
var legalMarkdown string
// Text returns the embedded LEGAL.md contents.
func Text() string { return legalMarkdown }
```
NOTE: Go embed cannot traverse up directories. Create `pkg/legal/LEGAL.md` as a copy of the root LEGAL.md, OR use a build step. Prefer: keep the canonical file at `pkg/legal/LEGAL.md` and have the repo-root `LEGAL.md` be a symlink OR a second identical copy. Simplest: write content to both `LEGAL.md` and `pkg/legal/LEGAL.md` (identical). Document this dual-location pattern in a `// Note:` comment in legal.go (mirrors the providers/ vs pkg/providers/definitions/ pattern from Phase 1).
3. Create `pkg/legal/legal_test.go`:
- `TestText_NonEmpty` — `legal.Text()` returns string with len > 500
- `TestText_ContainsKeyPhrases` — contains "CFAA", "Responsible Use", "Disclaimer"
4. Create `cmd/legal.go`:
```go
package cmd
import (
"fmt"
"github.com/salvacybersec/keyhunter/pkg/legal"
"github.com/spf13/cobra"
)
var legalCmd = &cobra.Command{
Use: "legal",
Short: "Print the legal disclaimer for the --verify feature",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println(legal.Text())
return nil
},
}
```
5. Register `legalCmd` in `cmd/root.go` via `rootCmd.AddCommand(legalCmd)` in the existing init() function (follow the pattern of other commands like scanCmd).
cd /home/salva/Documents/apikey && go build ./... && go test ./pkg/legal/... -v && ./keyhunter legal 2>&1 | grep -q "CFAA" || go run ./cmd/keyhunter legal 2>&1 | grep -q "CFAA"
- `test -f LEGAL.md && test -f pkg/legal/LEGAL.md`
- `grep -q 'CFAA' LEGAL.md && grep -q 'Responsible Use' LEGAL.md`
- `grep -q 'go:embed LEGAL.md' pkg/legal/legal.go`
- `grep -q 'legalCmd' cmd/root.go`
- `go test ./pkg/legal/... -v` passes
- `go run . legal` (or equivalent) prints text containing "CFAA"
LEGAL.md exists, is embedded, `keyhunter legal` prints it.
Task 2: Implement consent prompt (pkg/verify/consent.go)
pkg/verify/consent.go, pkg/verify/consent_test.go
- EnsureConsent returns (true, nil) immediately when settings key "verify.consent" == "granted"
- When no prior decision exists, writes a prompt to `out` io.Writer, reads a line from `in` io.Reader
- Input "yes", "YES", "Yes" (case-insensitive full word) -> persists "granted" and returns (true, nil)
- Any other input (including "y", "no", empty) -> persists "declined" and returns (false, nil)
- When "verify.consent" == "declined", re-prompts (declined is not sticky; user might change their mind). Only "granted" is sticky
- Prompt text contains phrases: "legal implications", "keyhunter legal", "yes"
Create `pkg/verify/consent.go`:
```go
package verify
import (
"bufio"
"fmt"
"io"
"strings"
"github.com/salvacybersec/keyhunter/pkg/storage"
)
const ConsentSettingKey = "verify.consent"
const (
ConsentGranted = "granted"
ConsentDeclined = "declined"
)
// EnsureConsent checks whether the user has previously granted consent to run
// active key verification. If not, it prints a warning and prompts on `out`,
// reads one line from `in`, and persists the decision via the settings table.
//
// Returns true if consent is granted (either previously or just now).
// Declined decisions are not sticky — the next call will re-prompt.
func EnsureConsent(db *storage.DB, in io.Reader, out io.Writer) (bool, error) {
val, found, err := db.GetSetting(ConsentSettingKey)
if err != nil {
return false, fmt.Errorf("reading verify.consent: %w", err)
}
if found && val == ConsentGranted {
return true, nil
}
fmt.Fprintln(out, "⚠ Active Key Verification — Legal Notice")
fmt.Fprintln(out, "")
fmt.Fprintln(out, "Using --verify will send HTTP requests to third-party provider APIs")
fmt.Fprintln(out, "for every API key KeyHunter finds. You are responsible for the legal")
fmt.Fprintln(out, "implications of these requests in your jurisdiction (CFAA, Computer")
fmt.Fprintln(out, "Misuse Act, GDPR, provider ToS).")
fmt.Fprintln(out, "")
fmt.Fprintln(out, "Run `keyhunter legal` to read the full disclaimer.")
fmt.Fprintln(out, "")
fmt.Fprint(out, "Type 'yes' to proceed: ")
reader := bufio.NewReader(in)
line, err := reader.ReadString('\n')
if err != nil && err != io.EOF {
return false, fmt.Errorf("reading consent input: %w", err)
}
answer := strings.ToLower(strings.TrimSpace(line))
if answer == "yes" {
if err := db.SetSetting(ConsentSettingKey, ConsentGranted); err != nil {
return false, fmt.Errorf("persisting consent: %w", err)
}
fmt.Fprintln(out, "Consent recorded. Proceeding with verification.")
return true, nil
}
if err := db.SetSetting(ConsentSettingKey, ConsentDeclined); err != nil {
return false, fmt.Errorf("persisting declined consent: %w", err)
}
fmt.Fprintln(out, "Consent declined. Verification skipped.")
return false, nil
}
```
Create `pkg/verify/consent_test.go`:
- `TestEnsureConsent_GrantedPrevious` — seed settings with "granted", call with empty in reader, assert (true, nil), no prompt written
- `TestEnsureConsent_TypeYes` — fresh DB, in = strings.NewReader("yes\n"), assert (true, nil), settings now "granted", prompt text contains "legal implications"
- `TestEnsureConsent_TypeYesUppercase` — in = strings.NewReader("YES\n"), assert (true, nil)
- `TestEnsureConsent_TypeNo` — in = strings.NewReader("no\n"), assert (false, nil), settings now "declined"
- `TestEnsureConsent_Empty` — in = strings.NewReader("\n"), assert (false, nil)
- `TestEnsureConsent_DeclinedNotSticky` — seed settings with "declined", in = strings.NewReader("yes\n"), assert (true, nil) — i.e. re-prompted and now granted
Use `storage.Open(":memory:")` for test DB setup.
cd /home/salva/Documents/apikey && go test ./pkg/verify/... -run Consent -v
- `grep -q 'EnsureConsent' pkg/verify/consent.go`
- `grep -q 'ConsentSettingKey' pkg/verify/consent.go`
- All 6 consent test cases pass
- `go build ./...` succeeds
EnsureConsent gates --verify correctly and persists only granted decisions as sticky.
- `go build ./...` clean
- `go test ./pkg/legal/... ./pkg/verify/... -v` all pass
- `go run . legal` prints LEGAL.md content
- LEGAL.md exists at repo root and in pkg/legal/ for embed
- `keyhunter legal` command works
- EnsureConsent prompts once on first --verify, persists granted, re-prompts if declined