mirror of
https://github.com/Vyntral/god-eye.git
synced 2026-05-16 13:39:10 +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.
135 lines
4.3 KiB
Go
135 lines
4.3 KiB
Go
// Package tui provides terminal-only live views of scan activity. No web
|
|
// UI by design. Fase 4 will expand this into a bubbletea-powered
|
|
// interactive TUI with panels; the current LivePrinter is the minimal
|
|
// terminal-only viewer that emits colorized event lines in real time.
|
|
package tui
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"god-eye/internal/eventbus"
|
|
"god-eye/internal/output"
|
|
)
|
|
|
|
// LivePrinter subscribes to every event on a bus and prints a one-line
|
|
// summary to stdout as they arrive. Safe to attach alongside the regular
|
|
// report module — this is purely an observability layer.
|
|
type LivePrinter struct {
|
|
bus *eventbus.Bus
|
|
sub *eventbus.Subscription
|
|
verbosity int // 0 = quiet (vulns only), 1 = normal (discovery+vulns), 2 = noisy
|
|
started time.Time
|
|
|
|
evCount atomic.Uint64
|
|
}
|
|
|
|
// NewLivePrinter attaches to bus and begins printing.
|
|
//
|
|
// verbosity levels:
|
|
//
|
|
// 0 — only vulnerabilities, takeovers, secrets, CVEs
|
|
// 1 — above + subdomain discovery + HTTP probe summaries
|
|
// 2 — everything, including module errors and phase markers
|
|
func NewLivePrinter(bus *eventbus.Bus, verbosity int) *LivePrinter {
|
|
p := &LivePrinter{bus: bus, verbosity: verbosity, started: time.Now()}
|
|
p.sub = bus.SubscribeAll(p.handle)
|
|
return p
|
|
}
|
|
|
|
// Close unsubscribes from the bus and prints a summary footer.
|
|
func (p *LivePrinter) Close() {
|
|
if p.sub != nil {
|
|
p.sub.Unsubscribe()
|
|
}
|
|
dur := time.Since(p.started).Round(time.Millisecond)
|
|
fmt.Printf("%s scan elapsed %s, %d events seen\n",
|
|
output.Dim("·"), output.BoldGreen(dur.String()), p.evCount.Load())
|
|
}
|
|
|
|
func (p *LivePrinter) handle(_ context.Context, e eventbus.Event) {
|
|
p.evCount.Add(1)
|
|
switch ev := e.(type) {
|
|
case eventbus.SubdomainDiscovered:
|
|
if p.verbosity >= 1 {
|
|
fmt.Printf("%s %s %s\n", output.Dim("↳"), output.Cyan(ev.Method), ev.Subdomain)
|
|
}
|
|
case eventbus.DNSResolved:
|
|
if p.verbosity >= 2 {
|
|
fmt.Printf("%s %s %s\n", output.Dim("⏚"), ev.Subdomain, output.Dim(joinIPs(ev.IPs)))
|
|
}
|
|
case eventbus.HTTPProbed:
|
|
if p.verbosity >= 1 {
|
|
color := statusColor(ev.StatusCode)
|
|
fmt.Printf("%s %s %s %s\n", color, ev.URL, output.Dim(fmt.Sprintf("[%d]", ev.StatusCode)), output.Dim(ev.Title))
|
|
}
|
|
case eventbus.VulnerabilityFound:
|
|
fmt.Printf("%s %s %s %s\n", sevBadge(ev.Severity), output.BoldWhite(ev.Title), output.Dim(ev.URL), output.Dim(ev.ID))
|
|
case eventbus.SecretFound:
|
|
fmt.Printf("%s %s %s %s\n", sevBadge(ev.Severity), output.BoldWhite("SECRET:"+ev.Kind), ev.Location, output.Dim(ev.Match))
|
|
case eventbus.TakeoverCandidate:
|
|
fmt.Printf("%s %s %s service=%s\n", sevBadge(eventbus.SeverityHigh), output.BoldYellow("TAKEOVER?"), ev.Subdomain, ev.Service)
|
|
case eventbus.TakeoverConfirmed:
|
|
fmt.Printf("%s %s %s service=%s\n", sevBadge(eventbus.SeverityCritical), output.BgRed(" TAKEOVER "), ev.Subdomain, ev.Service)
|
|
case eventbus.CVEMatch:
|
|
fmt.Printf("%s %s %s@%s → %s\n", sevBadge(ev.Severity), output.BoldWhite("CVE"), ev.Technology, ev.Version, ev.CVE)
|
|
case eventbus.AIFinding:
|
|
fmt.Printf("%s %s %s %s\n", sevBadge(ev.Severity), output.BoldMagenta("AI:"+ev.Agent), output.Dim(ev.Subject), ev.Title)
|
|
case eventbus.ModuleError:
|
|
if p.verbosity >= 2 {
|
|
fmt.Printf("%s %s %s\n", output.Red("⚠"), output.Dim(ev.Module), ev.Err)
|
|
}
|
|
case eventbus.PhaseStarted:
|
|
if p.verbosity >= 1 {
|
|
fmt.Printf("%s %s\n", output.Dim("▶"), output.BoldCyan("phase "+ev.Phase))
|
|
}
|
|
case eventbus.PhaseCompleted:
|
|
if p.verbosity >= 1 {
|
|
fmt.Printf("%s %s %s\n", output.Dim("▣"), output.Dim("phase "+ev.Phase), output.Dim(ev.Duration.Round(time.Millisecond).String()))
|
|
}
|
|
}
|
|
}
|
|
|
|
func sevBadge(s eventbus.Severity) string {
|
|
switch s {
|
|
case eventbus.SeverityCritical:
|
|
return output.BgRed(" CRIT ")
|
|
case eventbus.SeverityHigh:
|
|
return output.Red("[HIGH]")
|
|
case eventbus.SeverityMedium:
|
|
return output.Yellow("[MED]")
|
|
case eventbus.SeverityLow:
|
|
return output.Blue("[LOW]")
|
|
default:
|
|
return output.Dim("[INFO]")
|
|
}
|
|
}
|
|
|
|
func statusColor(code int) string {
|
|
switch {
|
|
case code >= 200 && code < 300:
|
|
return output.Green("●")
|
|
case code >= 300 && code < 400:
|
|
return output.Yellow("◐")
|
|
case code >= 400 && code < 500:
|
|
return output.Red("○")
|
|
case code >= 500:
|
|
return output.BoldRed("✕")
|
|
default:
|
|
return output.Dim("·")
|
|
}
|
|
}
|
|
|
|
func joinIPs(ips []string) string {
|
|
out := "["
|
|
for i, ip := range ips {
|
|
if i > 0 {
|
|
out += ","
|
|
}
|
|
out += ip
|
|
}
|
|
return out + "]"
|
|
}
|