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.
268 lines
6.1 KiB
Go
268 lines
6.1 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// MemoryStore is the default in-memory Store implementation. Thread-safe,
|
|
// suitable for single-process scans. Persistent backends (BoltDB for ASM /
|
|
// resume workflows) land in Fase 5; they will implement the same Store
|
|
// interface so callers need no changes.
|
|
type MemoryStore struct {
|
|
mu sync.RWMutex
|
|
hosts map[string]*Host
|
|
// perHostLocks serializes Upsert mutations per-host without blocking
|
|
// independent hosts. It's populated lazily and never cleared — the number
|
|
// of subdomains per scan is bounded (thousands, not millions).
|
|
perHostLocks map[string]*sync.Mutex
|
|
locksMu sync.Mutex
|
|
}
|
|
|
|
// NewMemoryStore creates an empty MemoryStore.
|
|
func NewMemoryStore() *MemoryStore {
|
|
return &MemoryStore{
|
|
hosts: make(map[string]*Host),
|
|
perHostLocks: make(map[string]*sync.Mutex),
|
|
}
|
|
}
|
|
|
|
// lockFor returns the mutex that protects mutations to subdomain, creating
|
|
// it lazily if needed.
|
|
func (s *MemoryStore) lockFor(subdomain string) *sync.Mutex {
|
|
s.locksMu.Lock()
|
|
defer s.locksMu.Unlock()
|
|
l, ok := s.perHostLocks[subdomain]
|
|
if !ok {
|
|
l = &sync.Mutex{}
|
|
s.perHostLocks[subdomain] = l
|
|
}
|
|
return l
|
|
}
|
|
|
|
// Upsert creates or updates the record for subdomain, invoking mutate under
|
|
// a per-host lock. Concurrent callers mutating different subdomains proceed
|
|
// in parallel; concurrent mutations of the same subdomain are serialized.
|
|
func (s *MemoryStore) Upsert(ctx context.Context, subdomain string, mutate func(*Host)) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if subdomain == "" {
|
|
return nil
|
|
}
|
|
|
|
hostLock := s.lockFor(subdomain)
|
|
hostLock.Lock()
|
|
defer hostLock.Unlock()
|
|
|
|
s.mu.Lock()
|
|
h, existed := s.hosts[subdomain]
|
|
if !existed {
|
|
h = &Host{
|
|
Subdomain: subdomain,
|
|
FirstSeen: time.Now(),
|
|
}
|
|
s.hosts[subdomain] = h
|
|
}
|
|
s.mu.Unlock()
|
|
|
|
if mutate != nil {
|
|
mutate(h)
|
|
}
|
|
h.LastUpdated = time.Now()
|
|
return nil
|
|
}
|
|
|
|
// Get returns a deep-enough copy of the record so the caller cannot
|
|
// accidentally mutate store state. Slice fields are copied; nested struct
|
|
// pointers (TLSFingerprint, Takeover) are shallow-copied — callers MUST treat
|
|
// the result as read-only.
|
|
func (s *MemoryStore) Get(ctx context.Context, subdomain string) (*Host, bool) {
|
|
s.mu.RLock()
|
|
h, ok := s.hosts[subdomain]
|
|
s.mu.RUnlock()
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
hostLock := s.lockFor(subdomain)
|
|
hostLock.Lock()
|
|
defer hostLock.Unlock()
|
|
return cloneHost(h), true
|
|
}
|
|
|
|
// All returns every host, sorted by subdomain. Each returned Host is a copy;
|
|
// mutations to the slice or its elements do not affect the store.
|
|
func (s *MemoryStore) All(ctx context.Context) []*Host {
|
|
s.mu.RLock()
|
|
names := make([]string, 0, len(s.hosts))
|
|
for name := range s.hosts {
|
|
names = append(names, name)
|
|
}
|
|
s.mu.RUnlock()
|
|
sort.Strings(names)
|
|
|
|
out := make([]*Host, 0, len(names))
|
|
for _, n := range names {
|
|
if h, ok := s.Get(ctx, n); ok {
|
|
out = append(out, h)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Count returns the number of hosts in the store.
|
|
func (s *MemoryStore) Count(ctx context.Context) int {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
return len(s.hosts)
|
|
}
|
|
|
|
// Close is a no-op for MemoryStore; implemented to satisfy Store interface.
|
|
func (s *MemoryStore) Close() error { return nil }
|
|
|
|
// cloneHost returns a deep-enough copy that slice/map fields are detached.
|
|
func cloneHost(h *Host) *Host {
|
|
if h == nil {
|
|
return nil
|
|
}
|
|
c := *h
|
|
c.IPs = cloneStrings(h.IPs)
|
|
c.Technologies = cloneStrings(h.Technologies)
|
|
c.TLSAltNames = cloneStrings(h.TLSAltNames)
|
|
c.DiscoveredVia = cloneStrings(h.DiscoveredVia)
|
|
c.Ports = cloneInts(h.Ports)
|
|
c.Headers = cloneStringMap(h.Headers)
|
|
|
|
if h.TLSFingerprint != nil {
|
|
fp := *h.TLSFingerprint
|
|
fp.InternalHosts = cloneStrings(h.TLSFingerprint.InternalHosts)
|
|
c.TLSFingerprint = &fp
|
|
}
|
|
if h.Takeover != nil {
|
|
t := *h.Takeover
|
|
c.Takeover = &t
|
|
}
|
|
|
|
c.Vulnerabilities = cloneVulns(h.Vulnerabilities)
|
|
c.Secrets = cloneSecrets(h.Secrets)
|
|
c.CVEs = cloneCVEs(h.CVEs)
|
|
c.AIFindings = cloneAIFindings(h.AIFindings)
|
|
return &c
|
|
}
|
|
|
|
func cloneStrings(in []string) []string {
|
|
if len(in) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]string, len(in))
|
|
copy(out, in)
|
|
return out
|
|
}
|
|
|
|
func cloneInts(in []int) []int {
|
|
if len(in) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]int, len(in))
|
|
copy(out, in)
|
|
return out
|
|
}
|
|
|
|
func cloneStringMap(in map[string]string) map[string]string {
|
|
if len(in) == 0 {
|
|
return nil
|
|
}
|
|
out := make(map[string]string, len(in))
|
|
for k, v := range in {
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneVulns(in []Vulnerability) []Vulnerability {
|
|
if len(in) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]Vulnerability, len(in))
|
|
for i, v := range in {
|
|
v.CVEs = cloneStrings(v.CVEs)
|
|
out[i] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneSecrets(in []Secret) []Secret {
|
|
if len(in) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]Secret, len(in))
|
|
copy(out, in)
|
|
return out
|
|
}
|
|
|
|
func cloneCVEs(in []CVE) []CVE {
|
|
if len(in) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]CVE, len(in))
|
|
copy(out, in)
|
|
return out
|
|
}
|
|
|
|
func cloneAIFindings(in []AIFinding) []AIFinding {
|
|
if len(in) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]AIFinding, len(in))
|
|
for i, f := range in {
|
|
f.CVEs = cloneStrings(f.CVEs)
|
|
out[i] = f
|
|
}
|
|
return out
|
|
}
|
|
|
|
// AppendUnique helpers — exported for modules that want to append slice
|
|
// fields without introducing duplicates. Keeps mutation semantics in one place.
|
|
|
|
// AddDiscoveryMethod appends method to h.DiscoveredVia if not already present.
|
|
func AddDiscoveryMethod(h *Host, method string) {
|
|
for _, m := range h.DiscoveredVia {
|
|
if m == method {
|
|
return
|
|
}
|
|
}
|
|
h.DiscoveredVia = append(h.DiscoveredVia, method)
|
|
}
|
|
|
|
// AddIPs appends new IPs (dedup, in-place).
|
|
func AddIPs(h *Host, ips []string) {
|
|
seen := make(map[string]bool, len(h.IPs))
|
|
for _, ip := range h.IPs {
|
|
seen[ip] = true
|
|
}
|
|
for _, ip := range ips {
|
|
if ip == "" || seen[ip] {
|
|
continue
|
|
}
|
|
seen[ip] = true
|
|
h.IPs = append(h.IPs, ip)
|
|
}
|
|
}
|
|
|
|
// AddTechnologies appends new technologies (dedup, in-place).
|
|
func AddTechnologies(h *Host, tech []string) {
|
|
seen := make(map[string]bool, len(h.Technologies))
|
|
for _, t := range h.Technologies {
|
|
seen[t] = true
|
|
}
|
|
for _, t := range tech {
|
|
if t == "" || seen[t] {
|
|
continue
|
|
}
|
|
seen[t] = true
|
|
h.Technologies = append(h.Technologies, t)
|
|
}
|
|
}
|