feat(05-02): implement EnsureConsent prompt gating --verify

- Add EnsureConsent(db, in, out) that returns (true, nil) immediately if
  verify.consent==granted, otherwise prompts once, reads a line, persists
  'granted' on 'yes' (case-insensitive), 'declined' otherwise.
- Declined is not sticky — next call re-prompts; only granted persists.
- Prompt references legal implications and directs users to 'keyhunter legal'.
This commit is contained in:
salvacybersec
2026-04-05 15:47:30 +03:00
parent 6a94ce5903
commit d4c140371e

70
pkg/verify/consent.go Normal file
View File

@@ -0,0 +1,70 @@
package verify
import (
"bufio"
"fmt"
"io"
"strings"
"github.com/salvacybersec/keyhunter/pkg/storage"
)
// ConsentSettingKey is the settings table key under which the user's
// verification consent decision is persisted.
const ConsentSettingKey = "verify.consent"
// Consent decision values stored under ConsentSettingKey.
const (
ConsentGranted = "granted"
ConsentDeclined = "declined"
)
// EnsureConsent checks whether the user has previously granted consent to run
// active key verification. If consent was previously granted it returns
// (true, nil) immediately without prompting. Otherwise it writes a warning
// and prompt to out, reads one line from in, and persists the decision via
// the settings table.
//
// Declined decisions are not sticky — a subsequent call will re-prompt. Only
// "granted" persists across runs.
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")
fmt.Fprintln(out, "legal implications of these requests in your jurisdiction")
fmt.Fprintln(out, "(CFAA, Computer 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
}