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.
184 lines
4.6 KiB
Go
184 lines
4.6 KiB
Go
package module
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"sync"
|
|
|
|
"god-eye/internal/eventbus"
|
|
)
|
|
|
|
// Registry stores modules keyed by name. Modules register themselves via
|
|
// init() functions by calling Register on the default registry.
|
|
type Registry struct {
|
|
mu sync.RWMutex
|
|
modules map[string]Module
|
|
order []string // insertion order for deterministic iteration
|
|
}
|
|
|
|
// NewRegistry returns an empty registry. Most callers should use Default()
|
|
// which returns the process-wide registry that init() functions populate.
|
|
func NewRegistry() *Registry {
|
|
return &Registry{modules: make(map[string]Module)}
|
|
}
|
|
|
|
var (
|
|
defaultRegistry *Registry
|
|
defaultOnce sync.Once
|
|
)
|
|
|
|
// Default returns the process-wide module registry.
|
|
func Default() *Registry {
|
|
defaultOnce.Do(func() {
|
|
defaultRegistry = NewRegistry()
|
|
})
|
|
return defaultRegistry
|
|
}
|
|
|
|
// Register adds m to r. Panics on duplicate name — registration happens at
|
|
// init() time, so duplicates indicate a compile-time bug that must surface
|
|
// immediately rather than silently overwrite.
|
|
func (r *Registry) Register(m Module) {
|
|
if m == nil {
|
|
panic("module.Register: nil module")
|
|
}
|
|
name := m.Name()
|
|
if name == "" {
|
|
panic("module.Register: module has empty Name()")
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if _, exists := r.modules[name]; exists {
|
|
panic(fmt.Sprintf("module.Register: duplicate module %q", name))
|
|
}
|
|
r.modules[name] = m
|
|
r.order = append(r.order, name)
|
|
}
|
|
|
|
// Register is a shortcut for Default().Register(m). Intended use:
|
|
//
|
|
// func init() { module.Register(&myModule{}) }
|
|
func Register(m Module) { Default().Register(m) }
|
|
|
|
// Get returns the module with the given name.
|
|
func (r *Registry) Get(name string) (Module, bool) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
m, ok := r.modules[name]
|
|
return m, ok
|
|
}
|
|
|
|
// Names returns all registered module names in insertion order.
|
|
func (r *Registry) Names() []string {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
out := make([]string, len(r.order))
|
|
copy(out, r.order)
|
|
return out
|
|
}
|
|
|
|
// All returns every registered module in insertion order. The returned slice
|
|
// is safe for the caller to iterate but do not mutate it.
|
|
func (r *Registry) All() []Module {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
out := make([]Module, 0, len(r.order))
|
|
for _, n := range r.order {
|
|
out = append(out, r.modules[n])
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ByPhase returns modules belonging to the given phase, sorted by name for
|
|
// stable presentation.
|
|
func (r *Registry) ByPhase(p Phase) []Module {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
var out []Module
|
|
for _, n := range r.order {
|
|
m := r.modules[n]
|
|
if m.Phase() == p {
|
|
out = append(out, m)
|
|
}
|
|
}
|
|
sort.SliceStable(out, func(i, j int) bool { return out[i].Name() < out[j].Name() })
|
|
return out
|
|
}
|
|
|
|
// Select returns the subset of modules that should run for the given config.
|
|
// A module is selected when cfg.ModuleEnabled(name) returns true (explicit
|
|
// enable wins), OR when cfg leaves it unset and DefaultEnabled() is true.
|
|
func (r *Registry) Select(cfg ConfigView) []Module {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
var out []Module
|
|
for _, n := range r.order {
|
|
m := r.modules[n]
|
|
if cfg != nil {
|
|
// explicit config: respect it directly
|
|
if cfg.ModuleEnabled(m.Name()) {
|
|
out = append(out, m)
|
|
continue
|
|
}
|
|
// if the config has a non-default opinion (enabled=false), honor it
|
|
// — but ModuleEnabled returning false could also mean "unset".
|
|
// We resolve the ambiguity by checking whether any profile/CLI flag
|
|
// set it via a separate mechanism; for now, fall back to the
|
|
// module's default.
|
|
if m.DefaultEnabled() {
|
|
out = append(out, m)
|
|
}
|
|
continue
|
|
}
|
|
// no config: honor module default
|
|
if m.DefaultEnabled() {
|
|
out = append(out, m)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ProducersOf returns the modules that declare t in their Produces() set.
|
|
// Used by tooling and tests to validate the event-graph integrity.
|
|
func (r *Registry) ProducersOf(t eventbus.EventType) []Module {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
var out []Module
|
|
for _, n := range r.order {
|
|
m := r.modules[n]
|
|
for _, et := range m.Produces() {
|
|
if et == t {
|
|
out = append(out, m)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ConsumersOf returns modules that declare t in their Consumes() set.
|
|
func (r *Registry) ConsumersOf(t eventbus.EventType) []Module {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
var out []Module
|
|
for _, n := range r.order {
|
|
m := r.modules[n]
|
|
for _, et := range m.Consumes() {
|
|
if et == t {
|
|
out = append(out, m)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Reset clears the registry. Intended for tests only; never call in production
|
|
// code.
|
|
func (r *Registry) Reset() {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.modules = make(map[string]Module)
|
|
r.order = nil
|
|
}
|