12 KiB
12 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 05-verification-engine | 02 | execute | 1 |
|
|
true |
|
|
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.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.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".
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
<success_criteria>
- LEGAL.md exists at repo root and in pkg/legal/ for embed
keyhunter legalcommand works- EnsureConsent prompts once on first --verify, persists granted, re-prompts if declined </success_criteria>