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
This commit is contained in:
71
cmd/scan.go
71
cmd/scan.go
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/salvacybersec/keyhunter/pkg/output"
|
"github.com/salvacybersec/keyhunter/pkg/output"
|
||||||
"github.com/salvacybersec/keyhunter/pkg/providers"
|
"github.com/salvacybersec/keyhunter/pkg/providers"
|
||||||
"github.com/salvacybersec/keyhunter/pkg/storage"
|
"github.com/salvacybersec/keyhunter/pkg/storage"
|
||||||
|
"github.com/salvacybersec/keyhunter/pkg/verify"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
@@ -30,8 +31,10 @@ var (
|
|||||||
flagURL string
|
flagURL string
|
||||||
flagClipboard bool
|
flagClipboard bool
|
||||||
flagSince string
|
flagSince string
|
||||||
flagMaxFileSize int64
|
flagMaxFileSize int64
|
||||||
flagInsecure bool
|
flagInsecure bool
|
||||||
|
flagVerifyTimeout time.Duration
|
||||||
|
flagVerifyWorkers int
|
||||||
)
|
)
|
||||||
|
|
||||||
var scanCmd = &cobra.Command{
|
var scanCmd = &cobra.Command{
|
||||||
@@ -107,18 +110,62 @@ var scanCmd = &cobra.Command{
|
|||||||
return fmt.Errorf("starting scan: %w", err)
|
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
|
var findings []engine.Finding
|
||||||
for f := range ch {
|
for f := range ch {
|
||||||
findings = append(findings, f)
|
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{
|
storeFinding := storage.Finding{
|
||||||
ProviderName: f.ProviderName,
|
ProviderName: f.ProviderName,
|
||||||
KeyValue: f.KeyValue,
|
KeyValue: f.KeyValue,
|
||||||
KeyMasked: f.KeyMasked,
|
KeyMasked: f.KeyMasked,
|
||||||
Confidence: f.Confidence,
|
Confidence: f.Confidence,
|
||||||
SourcePath: f.Source,
|
SourcePath: f.Source,
|
||||||
SourceType: f.SourceType,
|
SourceType: f.SourceType,
|
||||||
LineNumber: f.LineNumber,
|
LineNumber: f.LineNumber,
|
||||||
|
Verified: f.Verified,
|
||||||
|
VerifyStatus: f.VerifyStatus,
|
||||||
|
VerifyHTTPCode: f.VerifyHTTPCode,
|
||||||
|
VerifyMetadata: f.VerifyMetadata,
|
||||||
}
|
}
|
||||||
if _, err := db.SaveFinding(storeFinding, encKey); err != nil {
|
if _, err := db.SaveFinding(storeFinding, encKey); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "warning: failed to save finding: %v\n", err)
|
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().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")
|
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"))
|
_ = viper.BindPFlag("scan.workers", scanCmd.Flags().Lookup("workers"))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user