mirror of
https://github.com/Vyntral/god-eye.git
synced 2026-05-17 14:03:36 +02:00
3a4c230aa7
Complete architectural overhaul. Replaces the v0.1 monolithic scanner with an event-driven pipeline of auto-registered modules. Foundation (internal/): - eventbus: typed pub/sub, 20 event types, race-safe, drop counter - module: registry with phase-based selection - store: thread-safe host store with per-host locks + deep-copy reads - pipeline: coordinator with phase barriers + panic recovery - config: 5 scan profiles + 3 AI tiers + YAML loader + auto-discovery Modules (26 auto-registered across 6 phases): - Discovery: passive (26 sources), bruteforce, recursive, AXFR, GitHub dorks, CT streaming, permutation, reverse DNS, vhost, ASN, supply chain (npm + PyPI) - Enrichment: HTTP probe + tech fingerprint + TLS appliance ID, ports - Analysis: security checks, takeover (110+ sigs), cloud, JavaScript, GraphQL, JWT, headers (OWASP), HTTP smuggling, AI cascade, Nuclei - Reporting: TXT/JSON/CSV writer + AI scan brief AI layer (internal/ai/ + internal/modules/ai/): - Three profiles: lean (16 GB), balanced (32 GB MoE), heavy (64 GB) - Six event-driven handlers: CVE, JS file, HTTP response, secret filter, multi-agent vuln enrichment, anomaly + executive report - Content-hash cache dedups Ollama calls across hosts - Auto-pull of missing models via /api/pull with streaming progress - End-of-scan AI SCAN BRIEF in terminal with top chains + next actions Nuclei compat layer (internal/nucleitpl/): - Executes ~13k community templates (HTTP subset) - Auto-download of nuclei-templates ZIP to ~/.god-eye/nuclei-templates - Scope filter rejects off-host templates (eliminates OSINT FPs) Operations: - Interactive wizard (internal/wizard/) — zero-flag launch - LivePrinter (internal/tui/) — colorized event stream - Diff engine + scheduler (internal/diff, internal/scheduler) for continuous ASM monitoring with webhook alerts - Proxy support (internal/proxyconf/): http / https / socks5 / socks5h + basic auth Fixes #1 — native SOCKS5 / Tor compatibility via --proxy flag. 185 unit tests across 15 packages, all race-detector clean.
226 lines
5.7 KiB
Go
226 lines
5.7 KiB
Go
// 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 <domain> [--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).")
|
|
}
|
|
}
|