// parity is a side-by-side comparison tool used to verify that the v2 // pipeline produces equivalent results to the v1 monolithic scanner for a // given target domain. It runs both paths, captures JSON output from each, // and diffs the subdomain lists + high-signal fields. // // Usage: // // go run ./tools/parity -d example.com // go run ./tools/parity -d example.com --no-brute // // Exit codes: // // 0 — outputs match or differ only in fields we expect to differ // 1 — meaningful divergence (subdomain set differs, status codes differ) // 2 — runtime error (network, binary missing) package main import ( "encoding/json" "flag" "fmt" "os" "os/exec" "sort" "strings" ) type scanResult struct { Mode string `json:"mode"` // "v1" or "v2" Target string `json:"target"` Subdomains map[string]hostInfo `json:"subdomains"` ErrorOutput string `json:"error,omitempty"` } type hostInfo struct { Subdomain string `json:"subdomain"` IPs []string `json:"ips,omitempty"` CNAME string `json:"cname,omitempty"` StatusCode int `json:"status_code,omitempty"` Tech []string `json:"technologies,omitempty"` CloudProvider string `json:"cloud_provider,omitempty"` } func main() { target := flag.String("d", "", "target domain") noBrute := flag.Bool("no-brute", false, "skip DNS brute-force (faster, less coverage)") binary := flag.String("bin", "./god-eye", "path to god-eye binary") flag.Parse() if *target == "" { fmt.Fprintln(os.Stderr, "usage: parity -d [--no-brute] [-bin ./god-eye]") os.Exit(2) } if _, err := os.Stat(*binary); err != nil { fmt.Fprintf(os.Stderr, "binary not found at %s: %v\n", *binary, err) os.Exit(2) } fmt.Printf(">>> Running v1 scanner on %s…\n", *target) v1, err := runScan(*binary, *target, *noBrute, false) if err != nil { fmt.Fprintf(os.Stderr, "v1 scan failed: %v\n", err) os.Exit(2) } fmt.Printf(">>> Running v2 pipeline on %s…\n", *target) v2, err := runScan(*binary, *target, *noBrute, true) if err != nil { fmt.Fprintf(os.Stderr, "v2 scan failed: %v\n", err) os.Exit(2) } diff := compare(v1, v2) printDiff(diff) if diff.Meaningful() { os.Exit(1) } } func runScan(binary, target string, noBrute, useV2 bool) (*scanResult, error) { args := []string{"-d", target, "--json", "--silent"} if noBrute { args = append(args, "--no-brute") } if useV2 { args = append(args, "--pipeline") } cmd := exec.Command(binary, args...) out, err := cmd.Output() if err != nil { return nil, fmt.Errorf("exec %v: %w (stderr: %s)", args, err, strings.TrimSpace(string(cmd.Stderr.(*os.File).Name()))) } r := &scanResult{Target: target} r.Subdomains = map[string]hostInfo{} if useV2 { r.Mode = "v2" } else { r.Mode = "v1" } // Both v1 and v2 emit JSON with a "subdomains" field whose shape differs // slightly — v1 is an array, v2 is a map. Try both. var shared struct { Subdomains []hostInfo `json:"subdomains"` } if err := json.Unmarshal(out, &shared); err == nil && len(shared.Subdomains) > 0 { for _, h := range shared.Subdomains { r.Subdomains[h.Subdomain] = h } return r, nil } var v2obj struct { Subdomains map[string]hostInfo `json:"subdomains"` } if err := json.Unmarshal(out, &v2obj); err == nil { r.Subdomains = v2obj.Subdomains return r, nil } return nil, fmt.Errorf("unable to parse JSON output (%d bytes): %s", len(out), strings.TrimSpace(string(out[:min(len(out), 200)]))) } func min(a, b int) int { if a < b { return a } return b } type diffReport struct { Target string OnlyInV1 []string OnlyInV2 []string Shared []string StatusDiff map[string][2]int // subdomain → [v1 status, v2 status] } func (d *diffReport) Meaningful() bool { // Ignore small diffs in discovery (sources can time out differently). if len(d.OnlyInV1) > 2 || len(d.OnlyInV2) > 2 { return true } if len(d.StatusDiff) > 0 { return true } return false } func compare(v1, v2 *scanResult) *diffReport { d := &diffReport{Target: v1.Target, StatusDiff: map[string][2]int{}} v1set := keySet(v1.Subdomains) v2set := keySet(v2.Subdomains) for k := range v1set { if _, ok := v2set[k]; !ok { d.OnlyInV1 = append(d.OnlyInV1, k) } else { d.Shared = append(d.Shared, k) } } for k := range v2set { if _, ok := v1set[k]; !ok { d.OnlyInV2 = append(d.OnlyInV2, k) } } sort.Strings(d.OnlyInV1) sort.Strings(d.OnlyInV2) sort.Strings(d.Shared) for _, k := range d.Shared { a := v1.Subdomains[k].StatusCode b := v2.Subdomains[k].StatusCode if a != b && a != 0 && b != 0 { d.StatusDiff[k] = [2]int{a, b} } } return d } func keySet(m map[string]hostInfo) map[string]struct{} { out := make(map[string]struct{}, len(m)) for k := range m { out[k] = struct{}{} } return out } func printDiff(d *diffReport) { fmt.Println() fmt.Printf("=== Parity report for %s ===\n", d.Target) fmt.Printf(" Shared subdomains: %d\n", len(d.Shared)) fmt.Printf(" Only in v1 : %d\n", len(d.OnlyInV1)) fmt.Printf(" Only in v2 : %d\n", len(d.OnlyInV2)) fmt.Printf(" Status code diff: %d\n", len(d.StatusDiff)) if len(d.OnlyInV1) > 0 { fmt.Println(" -- v1 only --") for _, s := range d.OnlyInV1 { fmt.Println(" ", s) } } if len(d.OnlyInV2) > 0 { fmt.Println(" -- v2 only --") for _, s := range d.OnlyInV2 { fmt.Println(" ", s) } } if len(d.StatusDiff) > 0 { fmt.Println(" -- status differences --") for k, pair := range d.StatusDiff { fmt.Printf(" %s: v1=%d v2=%d\n", k, pair[0], pair[1]) } } if d.Meaningful() { fmt.Println() fmt.Println("RESULT: meaningful divergence detected.") } else { fmt.Println() fmt.Println("RESULT: v1 and v2 agree (differences within acceptable noise).") } }