From 6fc0abe8ae71a24592cbbe3f989e53f2da8d5ea5 Mon Sep 17 00:00:00 2001 From: salvacybersec Date: Sun, 5 Apr 2026 15:53:47 +0300 Subject: [PATCH] feat(05-05): wire --verify into scan pipeline with consent gate - Add --verify-timeout (default 10s) and --verify-workers (default 10) flags - Refactor scan loop to collect findings, verify, then persist - Gate verification behind verify.EnsureConsent(db, stdin, stderr) - Route findings through verify.HTTPVerifier.VerifyAll with configurable timeout and worker pool, back-assign Result.Status/HTTPCode/Metadata onto engine.Finding by provider+masked-key tuple - Persist verify_* columns via storage.SaveFinding after verification --- cmd/scan.go | 71 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 61 insertions(+), 10 deletions(-) diff --git a/cmd/scan.go b/cmd/scan.go index ede62d0..62e3155 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -16,6 +16,7 @@ import ( "github.com/salvacybersec/keyhunter/pkg/output" "github.com/salvacybersec/keyhunter/pkg/providers" "github.com/salvacybersec/keyhunter/pkg/storage" + "github.com/salvacybersec/keyhunter/pkg/verify" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -30,8 +31,10 @@ var ( flagURL string flagClipboard bool flagSince string - flagMaxFileSize int64 - flagInsecure bool + flagMaxFileSize int64 + flagInsecure bool + flagVerifyTimeout time.Duration + flagVerifyWorkers int ) var scanCmd = &cobra.Command{ @@ -107,18 +110,62 @@ var scanCmd = &cobra.Command{ return fmt.Errorf("starting scan: %w", err) } + // Collect findings first (no immediate save) so verification can populate + // verify_* fields before persistence. var findings []engine.Finding for f := range ch { findings = append(findings, f) - // Persist to storage + } + + // Phase 5: if --verify is set, gate on consent and route findings + // through the HTTPVerifier before persisting. Declined consent skips + // verification but still prints + persists the unverified findings. + if flagVerify && len(findings) > 0 { + granted, consentErr := verify.EnsureConsent(db, os.Stdin, os.Stderr) + if consentErr != nil { + return fmt.Errorf("consent check: %w", consentErr) + } + if !granted { + fmt.Fprintln(os.Stderr, "Verification skipped (consent not granted). Run `keyhunter legal` for details.") + } else { + verifier := verify.NewHTTPVerifier(flagVerifyTimeout) + resultsCh := verifier.VerifyAll(context.Background(), findings, reg, flagVerifyWorkers) + + // Build an index keyed by provider+maskedKey so we can back-assign + // results onto the findings slice (VerifyAll preserves neither + // order nor identity — only the provider/masked tuple). + idx := make(map[string]int, len(findings)) + for i, f := range findings { + idx[f.ProviderName+"|"+f.KeyMasked] = i + } + for r := range resultsCh { + if i, ok := idx[r.ProviderName+"|"+r.KeyMasked]; ok { + findings[i].Verified = true + findings[i].VerifyStatus = r.Status + findings[i].VerifyHTTPCode = r.HTTPCode + findings[i].VerifyMetadata = r.Metadata + if r.Error != "" { + findings[i].VerifyError = r.Error + } + } + } + } + } + + // Persist all findings with verify_* fields populated (if verification ran). + for _, f := range findings { storeFinding := storage.Finding{ - ProviderName: f.ProviderName, - KeyValue: f.KeyValue, - KeyMasked: f.KeyMasked, - Confidence: f.Confidence, - SourcePath: f.Source, - SourceType: f.SourceType, - LineNumber: f.LineNumber, + ProviderName: f.ProviderName, + KeyValue: f.KeyValue, + KeyMasked: f.KeyMasked, + Confidence: f.Confidence, + SourcePath: f.Source, + SourceType: f.SourceType, + LineNumber: f.LineNumber, + Verified: f.Verified, + VerifyStatus: f.VerifyStatus, + VerifyHTTPCode: f.VerifyHTTPCode, + VerifyMetadata: f.VerifyMetadata, } if _, err := db.SaveFinding(storeFinding, encKey); err != nil { fmt.Fprintf(os.Stderr, "warning: failed to save finding: %v\n", err) @@ -288,5 +335,9 @@ func init() { scanCmd.Flags().Int64Var(&flagMaxFileSize, "max-file-size", 0, "max file size in bytes to scan (0 = unlimited)") scanCmd.Flags().BoolVar(&flagInsecure, "insecure", false, "for --url: skip TLS certificate verification") + // Phase 5 verification flags. + scanCmd.Flags().DurationVar(&flagVerifyTimeout, "verify-timeout", 10*time.Second, "per-key verification HTTP timeout (default 10s)") + scanCmd.Flags().IntVar(&flagVerifyWorkers, "verify-workers", 10, "parallel workers for key verification (default 10)") + _ = viper.BindPFlag("scan.workers", scanCmd.Flags().Lookup("workers")) }