266 lines
12 KiB
Markdown
266 lines
12 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/phases/05-verification-engine/05-CONTEXT.md
|
|
|
|
@pkg/storage/settings.go
|
|
@cmd/root.go
|
|
@cmd/scan.go
|
|
|
|
<interfaces>
|
|
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"`.
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Write LEGAL.md and embed it in pkg/legal, wire keyhunter legal subcommand</name>
|
|
<files>LEGAL.md, pkg/legal/legal.go, pkg/legal/legal_test.go, cmd/legal.go, cmd/root.go</files>
|
|
<action>
|
|
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).
|
|
</action>
|
|
<verify>
|
|
<automated>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"</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `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"
|
|
</acceptance_criteria>
|
|
<done>LEGAL.md exists, is embedded, `keyhunter legal` prints it.</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 2: Implement consent prompt (pkg/verify/consent.go)</name>
|
|
<files>pkg/verify/consent.go, pkg/verify/consent_test.go</files>
|
|
<behavior>
|
|
- 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"
|
|
</behavior>
|
|
<action>
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/salva/Documents/apikey && go test ./pkg/verify/... -run Consent -v</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `grep -q 'EnsureConsent' pkg/verify/consent.go`
|
|
- `grep -q 'ConsentSettingKey' pkg/verify/consent.go`
|
|
- All 6 consent test cases pass
|
|
- `go build ./...` succeeds
|
|
</acceptance_criteria>
|
|
<done>EnsureConsent gates --verify correctly and persists only granted decisions as sticky.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `go build ./...` clean
|
|
- `go test ./pkg/legal/... ./pkg/verify/... -v` all pass
|
|
- `go run . legal` prints LEGAL.md content
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- 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
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/05-verification-engine/05-02-SUMMARY.md`
|
|
</output>
|