mirror of
https://github.com/Vyntral/god-eye.git
synced 2026-08-18 05:17:16 +02:00
feat: v2.0 full rewrite — event-driven pipeline, AI + Nuclei + proxy
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.
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
// 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).")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user