mirror of
https://github.com/Vyntral/god-eye.git
synced 2026-05-18 14:24:49 +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.
182 lines
4.7 KiB
Go
182 lines
4.7 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// YAMLConfig is the schema persisted on disk. Fields are intentionally a
|
|
// subset of Config — YAML is for declarative, long-lived settings
|
|
// (profile, module toggles, resolver lists, AI model names); ephemeral
|
|
// flags (--silent, --verbose, --domain) remain CLI-only.
|
|
type YAMLConfig struct {
|
|
Profile string `yaml:"profile,omitempty"`
|
|
Concurrency int `yaml:"concurrency,omitempty"`
|
|
Timeout int `yaml:"timeout,omitempty"`
|
|
Stealth string `yaml:"stealth,omitempty"`
|
|
Resolvers []string `yaml:"resolvers,omitempty"`
|
|
Wordlist string `yaml:"wordlist,omitempty"`
|
|
Modules map[string]bool `yaml:"modules,omitempty"`
|
|
AI *YAMLAIConfig `yaml:"ai,omitempty"`
|
|
Output *YAMLOutputConfig `yaml:"output,omitempty"`
|
|
}
|
|
|
|
// YAMLAIConfig groups AI-related YAML fields.
|
|
type YAMLAIConfig struct {
|
|
Enabled bool `yaml:"enabled,omitempty"`
|
|
URL string `yaml:"url,omitempty"`
|
|
FastModel string `yaml:"fast_model,omitempty"`
|
|
DeepModel string `yaml:"deep_model,omitempty"`
|
|
Cascade *bool `yaml:"cascade,omitempty"`
|
|
Deep bool `yaml:"deep,omitempty"`
|
|
MultiAgent bool `yaml:"multi_agent,omitempty"`
|
|
}
|
|
|
|
// YAMLOutputConfig groups output-related YAML fields.
|
|
type YAMLOutputConfig struct {
|
|
Path string `yaml:"path,omitempty"`
|
|
Format string `yaml:"format,omitempty"`
|
|
JSON bool `yaml:"json,omitempty"`
|
|
}
|
|
|
|
// LoadYAML reads a YAML config file from path and returns the parsed config.
|
|
// Returns (nil, nil) when the file does not exist — callers should treat this
|
|
// as "no config file, use defaults". Returns an error for any other I/O or
|
|
// parse failure.
|
|
func LoadYAML(path string) (*YAMLConfig, error) {
|
|
if path == "" {
|
|
return nil, nil
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, fmt.Errorf("read config %q: %w", path, err)
|
|
}
|
|
|
|
var y YAMLConfig
|
|
if err := yaml.Unmarshal(data, &y); err != nil {
|
|
return nil, fmt.Errorf("parse config %q: %w", path, err)
|
|
}
|
|
return &y, nil
|
|
}
|
|
|
|
// ApplyYAML merges a parsed YAML config into cfg. CLI flags win: YAML only
|
|
// fills fields that are still at their zero value on cfg. The profile named
|
|
// in YAML is applied only if cfg.Profile is empty.
|
|
func ApplyYAML(cfg *Config, y *YAMLConfig) {
|
|
if cfg == nil || y == nil {
|
|
return
|
|
}
|
|
|
|
if cfg.Profile == "" && y.Profile != "" {
|
|
cfg.Profile = y.Profile
|
|
}
|
|
if cfg.Concurrency == 0 && y.Concurrency > 0 {
|
|
cfg.Concurrency = y.Concurrency
|
|
}
|
|
if cfg.Timeout == 0 && y.Timeout > 0 {
|
|
cfg.Timeout = y.Timeout
|
|
}
|
|
if cfg.StealthMode == "" && y.Stealth != "" {
|
|
cfg.StealthMode = y.Stealth
|
|
}
|
|
if cfg.Resolvers == "" && len(y.Resolvers) > 0 {
|
|
cfg.Resolvers = joinComma(y.Resolvers)
|
|
}
|
|
if cfg.Wordlist == "" && y.Wordlist != "" {
|
|
cfg.Wordlist = y.Wordlist
|
|
}
|
|
|
|
if len(y.Modules) > 0 {
|
|
if cfg.ModuleSettings == nil {
|
|
cfg.ModuleSettings = make(map[string]bool)
|
|
}
|
|
for name, enabled := range y.Modules {
|
|
if _, already := cfg.ModuleSettings[name]; !already {
|
|
cfg.ModuleSettings[name] = enabled
|
|
}
|
|
}
|
|
}
|
|
|
|
if y.AI != nil {
|
|
if y.AI.Enabled && !cfg.EnableAI {
|
|
cfg.EnableAI = true
|
|
}
|
|
if cfg.AIUrl == "" && y.AI.URL != "" {
|
|
cfg.AIUrl = y.AI.URL
|
|
}
|
|
if cfg.AIFastModel == "" && y.AI.FastModel != "" {
|
|
cfg.AIFastModel = y.AI.FastModel
|
|
}
|
|
if cfg.AIDeepModel == "" && y.AI.DeepModel != "" {
|
|
cfg.AIDeepModel = y.AI.DeepModel
|
|
}
|
|
if y.AI.Cascade != nil && !cfg.AICascade {
|
|
cfg.AICascade = *y.AI.Cascade
|
|
}
|
|
if y.AI.Deep && !cfg.AIDeepAnalysis {
|
|
cfg.AIDeepAnalysis = true
|
|
}
|
|
if y.AI.MultiAgent && !cfg.MultiAgent {
|
|
cfg.MultiAgent = true
|
|
}
|
|
}
|
|
|
|
if y.Output != nil {
|
|
if cfg.Output == "" && y.Output.Path != "" {
|
|
cfg.Output = y.Output.Path
|
|
}
|
|
if cfg.Format == "" && y.Output.Format != "" {
|
|
cfg.Format = y.Output.Format
|
|
}
|
|
if y.Output.JSON && !cfg.JsonOutput {
|
|
cfg.JsonOutput = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// DefaultConfigPaths returns the ordered list of paths LoadYAML scans by
|
|
// default when no --config is provided. The first existing file wins.
|
|
func DefaultConfigPaths() []string {
|
|
home, err := os.UserHomeDir()
|
|
var homeCfg string
|
|
if err == nil {
|
|
homeCfg = filepath.Join(home, ".god-eye", "config.yaml")
|
|
}
|
|
return []string{
|
|
"god-eye.yaml",
|
|
".god-eye.yaml",
|
|
homeCfg,
|
|
}
|
|
}
|
|
|
|
// FindConfigFile returns the first existing file in DefaultConfigPaths, or
|
|
// "" if none is found.
|
|
func FindConfigFile() string {
|
|
for _, p := range DefaultConfigPaths() {
|
|
if p == "" {
|
|
continue
|
|
}
|
|
if _, err := os.Stat(p); err == nil {
|
|
return p
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func joinComma(ss []string) string {
|
|
out := ""
|
|
for i, s := range ss {
|
|
if i > 0 {
|
|
out += ","
|
|
}
|
|
out += s
|
|
}
|
|
return out
|
|
}
|