diff --git a/pkg/verify/consent.go b/pkg/verify/consent.go new file mode 100644 index 0000000..521130a --- /dev/null +++ b/pkg/verify/consent.go @@ -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 +}