Files
Vyntral 8356eb573d 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.
2026-04-18 16:48:41 +02:00

338 lines
11 KiB
Go

// Package eventbus provides a typed, context-aware pub/sub bus that decouples
// discovery, probing, analysis, and reporting modules in God's Eye v2.
//
// Design choices:
// - Events are typed structs implementing Event; dispatch is keyed on EventType.
// - Subscribers run handlers on their own goroutine with a buffered channel,
// so a slow handler cannot stall the producer.
// - Publish is non-blocking: if a subscriber buffer is full, the event is
// dropped for that subscriber and Stats.Dropped is incremented. Subscribers
// that care about lossless delivery must size their buffer accordingly.
// - Close stops accepting new events and drains outstanding ones before
// returning.
package eventbus
import "time"
// EventType identifies the kind of an event.
type EventType string
// Canonical event types. Modules should always use these constants rather than
// string literals to avoid typos and to make the full event vocabulary greppable.
const (
EventSubdomainDiscovered EventType = "subdomain.discovered"
EventDNSResolved EventType = "dns.resolved"
EventHTTPProbed EventType = "http.probed"
EventTechDetected EventType = "tech.detected"
EventTLSAnalyzed EventType = "tls.analyzed"
EventTakeoverCandidate EventType = "takeover.candidate"
EventTakeoverConfirmed EventType = "takeover.confirmed"
EventVulnerability EventType = "vulnerability"
EventSecret EventType = "secret"
EventCVEMatch EventType = "cve.match"
EventCloudAsset EventType = "cloud.asset"
EventAPIFinding EventType = "api.finding"
EventJSFile EventType = "js.file"
EventAIFinding EventType = "ai.finding"
EventPhaseStarted EventType = "phase.started"
EventPhaseCompleted EventType = "phase.completed"
EventModuleError EventType = "module.error"
EventScanStarted EventType = "scan.started"
EventScanCompleted EventType = "scan.completed"
)
// Severity levels used across vulnerability, secret, AI and CVE events.
type Severity string
const (
SeverityInfo Severity = "info"
SeverityLow Severity = "low"
SeverityMedium Severity = "medium"
SeverityHigh Severity = "high"
SeverityCritical Severity = "critical"
)
// Event is implemented by every event struct.
type Event interface {
Type() EventType
Meta() EventMeta
}
// EventMeta is shared metadata embedded in every event.
type EventMeta struct {
At time.Time // when the event was created
Source string // originating module name (e.g. "sources.crtsh", "dns.resolver")
Target string // logical target (typically the subdomain or host the event pertains to)
}
// Meta returns the shared metadata; implemented by embedding EventMeta.
func (m EventMeta) Meta() EventMeta { return m }
// now returns the current time; indirected for testability.
var now = time.Now
// newMeta builds an EventMeta with a populated timestamp.
func newMeta(source, target string) EventMeta {
return EventMeta{At: now(), Source: source, Target: target}
}
// --- Concrete event types --------------------------------------------------
// SubdomainDiscovered fires whenever any source (passive, brute, recursive,
// CT, etc.) identifies a subdomain that passes the "ends in target domain"
// filter. Multiple sources may discover the same subdomain — the bus does not
// dedup; that's the store's job.
type SubdomainDiscovered struct {
EventMeta
Subdomain string
Method string // "passive:crt.sh", "brute", "recursive", "ct-stream", etc.
}
func (SubdomainDiscovered) Type() EventType { return EventSubdomainDiscovered }
func NewSubdomainDiscovered(source, subdomain, method string) SubdomainDiscovered {
return SubdomainDiscovered{
EventMeta: newMeta(source, subdomain),
Subdomain: subdomain,
Method: method,
}
}
// DNSResolved fires after a subdomain is resolved. Empty IPs field signals
// an intentionally negative result (NXDOMAIN); absence of the event means
// "not yet resolved".
type DNSResolved struct {
EventMeta
Subdomain string
IPs []string
CNAME string
PTR string
}
func (DNSResolved) Type() EventType { return EventDNSResolved }
// HTTPProbed fires once per successful HTTP probe, including server banner,
// title, and technology signals. Security checks emit their own events.
type HTTPProbed struct {
EventMeta
URL string
StatusCode int
ContentLength int64
Title string
Server string
Technologies []string
Headers map[string]string
ResponseMs int64
TLSVersion string
TLSSelfSigned bool
}
func (HTTPProbed) Type() EventType { return EventHTTPProbed }
// VulnerabilityFound is the canonical finding event for any detected issue.
// Scanner modules (security checks, smuggling, SSRF, GraphQL, etc.) all emit
// this so the reporter/aggregator has a single type to consume.
type VulnerabilityFound struct {
EventMeta
ID string // stable identifier, e.g. "open-redirect", "cors-wildcard-creds"
Title string // short human-readable title
Description string // longer context
Severity Severity
URL string // affected URL
Evidence string // raw evidence (truncated if too large)
Remediation string // how to fix
CVEs []string // referenced CVEs if any
OWASP string // OWASP category (e.g. "A03:2021-Injection")
CVSS float64 // 0.0 if not scored
}
func (VulnerabilityFound) Type() EventType { return EventVulnerability }
// SecretFound fires when a credential, API key, or token is detected (in JS,
// response bodies, commits, etc.).
type SecretFound struct {
EventMeta
Kind string // "aws_access_key", "jwt", "stripe_live", "generic_hex"
Match string // redacted or truncated match — full value in Value if validated
Value string // full value, populated only when validation succeeded
Location string // where it was found (URL, file path, commit sha)
Validated bool // true if we verified the secret is live against its service
Severity Severity
Description string
}
func (SecretFound) Type() EventType { return EventSecret }
// CVEMatch fires when a CVE is correlated to a detected technology/version.
type CVEMatch struct {
EventMeta
CVE string
Technology string
Version string
Severity Severity
CVSS float64
Description string
URL string
InKEV bool // true if in CISA Known Exploited Vulnerabilities catalog
}
func (CVEMatch) Type() EventType { return EventCVEMatch }
// TakeoverCandidate fires when a CNAME or fingerprint points at a service
// that could potentially be taken over. TakeoverConfirmed fires after active
// verification (service claim test) succeeds.
type TakeoverCandidate struct {
EventMeta
Subdomain string
Service string // "GitHub Pages", "S3", "Heroku", etc.
CNAME string
Evidence string
}
func (TakeoverCandidate) Type() EventType { return EventTakeoverCandidate }
type TakeoverConfirmed struct {
EventMeta
Subdomain string
Service string
CNAME string
PoC string // curl/HTTP reproducer
}
func (TakeoverConfirmed) Type() EventType { return EventTakeoverConfirmed }
// CloudAssetFound fires for exposed/accessible cloud assets (S3 buckets,
// GCS buckets, Azure blobs, Firebase projects, etc.).
type CloudAssetFound struct {
EventMeta
Provider string // "AWS", "GCP", "Azure", "Firebase"
Kind string // "s3-bucket", "gcs-bucket", "lambda-url"
Name string
URL string
Status string // "public-read", "listable", "writable", "exists"
Permissions []string // detailed permissions if known
}
func (CloudAssetFound) Type() EventType { return EventCloudAsset }
// APIFinding fires for discovered/enumerated API surfaces (GraphQL, Swagger,
// Postman, misconfigured REST) with associated issues.
type APIFinding struct {
EventMeta
Kind string // "graphql-introspection", "swagger-exposed", "rest-cors", etc.
URL string
Issue string
Severity Severity
Endpoints []string
}
func (APIFinding) Type() EventType { return EventAPIFinding }
// TechDetected fires when a technology (framework, server, CMS, language) is
// identified with a version, feeding CVE matching and AI analysis.
type TechDetected struct {
EventMeta
Host string
Technology string
Version string
Category string // "web-server", "framework", "cms", "language", "waf"
Confidence float64
}
func (TechDetected) Type() EventType { return EventTechDetected }
// TLSAnalyzed fires with TLS certificate details, including appliance
// fingerprint when identifiable.
type TLSAnalyzed struct {
EventMeta
Host string
Version string
Issuer string
Expiry time.Time
SelfSigned bool
AltNames []string
Vendor string // FortiGate, Palo Alto, etc. (empty if no fingerprint)
Product string
ApplianceKind string // "firewall", "vpn", "loadbalancer", "waf"
InternalHosts []string
}
func (TLSAnalyzed) Type() EventType { return EventTLSAnalyzed }
// JSFileDiscovered fires when a JavaScript file is discovered and prepared
// for analysis (secret scanning, endpoint extraction, AI review).
type JSFileDiscovered struct {
EventMeta
URL string
Size int64
Host string
}
func (JSFileDiscovered) Type() EventType { return EventJSFile }
// AIFinding is emitted by any AI/agent module (cascade or multi-agent).
type AIFinding struct {
EventMeta
Subject string // subdomain/URL the finding pertains to
Agent string // "triage", "deep", "xss", "sqli", etc.
Model string // LLM model id
Severity Severity
Title string
Description string
Evidence string
CVEs []string
OWASP string
Confidence float64
}
func (AIFinding) Type() EventType { return EventAIFinding }
// PhaseStarted / PhaseCompleted frame pipeline phases (passive, brute,
// resolve, probe, ai, etc.) so UIs and progress trackers can react.
type PhaseStarted struct {
EventMeta
Phase string
}
func (PhaseStarted) Type() EventType { return EventPhaseStarted }
type PhaseCompleted struct {
EventMeta
Phase string
Duration time.Duration
Stats map[string]int64
}
func (PhaseCompleted) Type() EventType { return EventPhaseCompleted }
// ModuleError fires when a module encounters a non-fatal error (source
// unavailable, rate-limited, timeout). Use this for observability; do not
// log errors in modules directly.
type ModuleError struct {
EventMeta
Module string
Err string // stringified error
Fatal bool // true only when the module cannot continue
Context map[string]string
}
func (ModuleError) Type() EventType { return EventModuleError }
// ScanStarted / ScanCompleted bookend the whole run.
type ScanStarted struct {
EventMeta
Target string
Profile string
}
func (ScanStarted) Type() EventType { return EventScanStarted }
type ScanCompleted struct {
EventMeta
Target string
Duration time.Duration
Stats map[string]int64
}
func (ScanCompleted) Type() EventType { return EventScanCompleted }