package config import ( "fmt" "os" "time" "gopkg.in/yaml.v3" ) // Config holds the full agent configuration loaded from config.yaml. type Config struct { Site string `yaml:"site"` // LUD or BAR LogLevel string `yaml:"log_level"` // debug | info | warn | error SetupComplete bool `yaml:"setup_complete"` // false → show setup wizard on first run DB struct { Path string `yaml:"path"` } `yaml:"db"` Web struct { Addr string `yaml:"addr"` Token string `yaml:"token"` } `yaml:"web"` Nexus struct { URL string `yaml:"url"` APIKey string `yaml:"api_key"` } `yaml:"nexus"` OfflineAfter time.Duration `yaml:"offline_after"` // mark hosts offline after this duration Alert AlertConfig `yaml:"alert"` Modules struct { ARPDiscovery ARPConfig `yaml:"arp_discovery"` ADSync ADConfig `yaml:"ad_sync"` SiteMonitoring SiteMonConfig `yaml:"site_monitoring"` PortScan PortScanConfig `yaml:"port_scan"` SNMP SNMPConfig `yaml:"snmp"` NexusReporter ReporterConfig `yaml:"nexus_reporter"` } `yaml:"modules"` } // ADConfig holds settings for the ad_sync module. type ADConfig struct { Enabled bool `yaml:"enabled"` Interval time.Duration `yaml:"interval"` Server string `yaml:"server"` // DC hostname or IP Port int `yaml:"port"` // default 389 BindDN string `yaml:"bind_dn"` // svc-scanner@winkel.local BindPassword string `yaml:"bind_password"` SearchBase string `yaml:"search_base"` // DC=winkel,DC=local TLS bool `yaml:"tls"` // use LDAPS } // SiteMonConfig holds settings for the site_monitoring module. type SiteMonConfig struct { Enabled bool `yaml:"enabled"` Interval time.Duration `yaml:"interval"` } // PortScanConfig holds settings for the port_scan module. type PortScanConfig struct { Enabled bool `yaml:"enabled"` Interval time.Duration `yaml:"interval"` Ports []int `yaml:"ports"` // empty = use defaults Timeout time.Duration `yaml:"timeout"` // per-port connect timeout } // SNMPConfig holds settings for the snmp module. type SNMPConfig struct { Enabled bool `yaml:"enabled"` Interval time.Duration `yaml:"interval"` Community string `yaml:"community"` // default community string } // ReporterConfig holds settings for the nexus_reporter module. type ReporterConfig struct { Enabled bool `yaml:"enabled"` Interval time.Duration `yaml:"interval"` } // AlertConfig holds notification settings. type AlertConfig struct { SMTP struct { Enabled bool `yaml:"enabled"` Host string `yaml:"host"` Port int `yaml:"port"` Username string `yaml:"username"` Password string `yaml:"password"` From string `yaml:"from"` To string `yaml:"to"` } `yaml:"smtp"` NexusEnabled bool `yaml:"nexus_enabled"` } // ARPConfig holds settings for the arp_discovery module. type ARPConfig struct { Enabled bool `yaml:"enabled"` Interval time.Duration `yaml:"interval"` Subnets []string `yaml:"subnets"` Interface string `yaml:"interface"` } // Load reads and validates the YAML config at path. func Load(path string) (*Config, error) { f, err := os.Open(path) if err != nil { return nil, fmt.Errorf("open config %q: %w", path, err) } defer f.Close() var cfg Config if err := yaml.NewDecoder(f).Decode(&cfg); err != nil { return nil, fmt.Errorf("decode config: %w", err) } if err := cfg.applyDefaults(); err != nil { return nil, fmt.Errorf("config validation: %w", err) } return &cfg, nil } // Save writes the config back to path in YAML format. func Save(path string, cfg *Config) error { f, err := os.Create(path) if err != nil { return fmt.Errorf("create config %q: %w", path, err) } defer f.Close() if err := yaml.NewEncoder(f).Encode(cfg); err != nil { return fmt.Errorf("encode config: %w", err) } return nil } func (c *Config) applyDefaults() error { if c.Site == "" { return fmt.Errorf("site is required (LUD or BAR)") } if c.Site != "LUD" && c.Site != "BAR" { return fmt.Errorf("site must be LUD or BAR, got %q", c.Site) } if c.LogLevel == "" { c.LogLevel = "info" } if c.DB.Path == "" { c.DB.Path = "/var/lib/nexus-scanner/scanner.db" } if c.Web.Addr == "" { c.Web.Addr = ":8080" } if c.Modules.ARPDiscovery.Interval == 0 { c.Modules.ARPDiscovery.Interval = 5 * time.Minute } if c.Modules.ARPDiscovery.Interface == "" { c.Modules.ARPDiscovery.Interface = "eth0" } if c.Modules.ADSync.Port == 0 { c.Modules.ADSync.Port = 389 } if c.Modules.ADSync.Interval == 0 { c.Modules.ADSync.Interval = 30 * time.Minute } if c.Modules.ADSync.SearchBase == "" && c.Modules.ADSync.Server != "" { c.Modules.ADSync.SearchBase = "DC=winkel,DC=local" } if c.Modules.SiteMonitoring.Interval == 0 { c.Modules.SiteMonitoring.Interval = time.Minute } if c.Modules.PortScan.Interval == 0 { c.Modules.PortScan.Interval = 15 * time.Minute } if c.Modules.PortScan.Timeout == 0 { c.Modules.PortScan.Timeout = 500 * time.Millisecond } if c.Modules.SNMP.Interval == 0 { c.Modules.SNMP.Interval = 5 * time.Minute } if c.Modules.SNMP.Community == "" { c.Modules.SNMP.Community = "public" } if c.Modules.NexusReporter.Interval == 0 { c.Modules.NexusReporter.Interval = 10 * time.Minute } if c.OfflineAfter == 0 { c.OfflineAfter = 15 * time.Minute } if c.Alert.SMTP.Port == 0 { c.Alert.SMTP.Port = 587 } return nil }