diff --git a/internal/config/config.go b/internal/config/config.go index a426e395..fb68a10d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -18,6 +18,7 @@ import ( "encoding/json" "fmt" "os" + "sync" "time" "github.com/spf13/viper" @@ -84,6 +85,7 @@ type Interface interface { // the application. It uses `mapstructure` tags to facilitate loading from // configuration files (e.g., YAML, JSON) via the Viper library. type Config struct { + mu sync.RWMutex `mapstructure:"-" yaml:"-"` LoggerCfg LoggerConfig `mapstructure:"logger" yaml:"logger"` DatabaseCfg DatabaseConfig `mapstructure:"database" yaml:"database"` EngineCfg EngineConfig `mapstructure:"engine" yaml:"engine"` @@ -101,72 +103,188 @@ type Config struct { // -- Interface Method Implementations (Getters) -- -func (c *Config) Logger() LoggerConfig { return c.LoggerCfg } -func (c *Config) Database() DatabaseConfig { return c.DatabaseCfg } -func (c *Config) Engine() EngineConfig { return c.EngineCfg } -func (c *Config) Browser() BrowserConfig { return c.BrowserCfg } -func (c *Config) Network() NetworkConfig { return c.NetworkCfg } -func (c *Config) IAST() IASTConfig { return c.IASTCfg } -func (c *Config) Scanners() ScannersConfig { return c.ScannersCfg } -func (c *Config) JWT() JWTConfig { return c.ScannersCfg.Static.JWT } -func (c *Config) Agent() AgentConfig { return c.AgentCfg } -func (c *Config) Discovery() DiscoveryConfig { return c.DiscoveryCfg } -func (c *Config) Autofix() AutofixConfig { return c.AutofixCfg } -func (c *Config) Scan() ScanConfig { return c.ScanCfg } +func (c *Config) Logger() LoggerConfig { + c.mu.RLock() + defer c.mu.RUnlock() + return c.LoggerCfg +} +func (c *Config) Database() DatabaseConfig { + c.mu.RLock() + defer c.mu.RUnlock() + return c.DatabaseCfg +} +func (c *Config) Engine() EngineConfig { + c.mu.RLock() + defer c.mu.RUnlock() + return c.EngineCfg +} +func (c *Config) Browser() BrowserConfig { + c.mu.RLock() + defer c.mu.RUnlock() + return c.BrowserCfg +} +func (c *Config) Network() NetworkConfig { + c.mu.RLock() + defer c.mu.RUnlock() + return c.NetworkCfg +} +func (c *Config) IAST() IASTConfig { + c.mu.RLock() + defer c.mu.RUnlock() + return c.IASTCfg +} +func (c *Config) Scanners() ScannersConfig { + c.mu.RLock() + defer c.mu.RUnlock() + return c.ScannersCfg +} +func (c *Config) JWT() JWTConfig { + c.mu.RLock() + defer c.mu.RUnlock() + return c.ScannersCfg.Static.JWT +} +func (c *Config) Agent() AgentConfig { + c.mu.RLock() + defer c.mu.RUnlock() + return c.AgentCfg +} +func (c *Config) Discovery() DiscoveryConfig { + c.mu.RLock() + defer c.mu.RUnlock() + return c.DiscoveryCfg +} +func (c *Config) Autofix() AutofixConfig { + c.mu.RLock() + defer c.mu.RUnlock() + return c.AutofixCfg +} +func (c *Config) Scan() ScanConfig { + c.mu.RLock() + defer c.mu.RUnlock() + return c.ScanCfg +} // -- Interface Method Implementations (Setters) -- -func (c *Config) SetScanConfig(sc ScanConfig) { c.ScanCfg = sc } +func (c *Config) SetScanConfig(sc ScanConfig) { + c.mu.Lock() + defer c.mu.Unlock() + c.ScanCfg = sc +} // Discovery Setters -func (c *Config) SetDiscoveryMaxDepth(d int) { c.DiscoveryCfg.MaxDepth = d } +func (c *Config) SetDiscoveryMaxDepth(d int) { + c.mu.Lock() + defer c.mu.Unlock() + c.DiscoveryCfg.MaxDepth = d +} func (c *Config) SetDiscoveryIncludeSubdomains(b bool) { + c.mu.Lock() + defer c.mu.Unlock() c.DiscoveryCfg.IncludeSubdomains = b } // Engine Setters -func (c *Config) SetEngineWorkerConcurrency(w int) { c.EngineCfg.WorkerConcurrency = w } +func (c *Config) SetEngineWorkerConcurrency(w int) { + c.mu.Lock() + defer c.mu.Unlock() + c.EngineCfg.WorkerConcurrency = w +} // Browser Setters -func (c *Config) SetBrowserHeadless(b bool) { c.BrowserCfg.Headless = b } -func (c *Config) SetBrowserDisableCache(b bool) { c.BrowserCfg.DisableCache = b } -func (c *Config) SetBrowserDisableGPU(b bool) { c.BrowserCfg.DisableGPU = b } -func (c *Config) SetBrowserIgnoreTLSErrors(b bool) { c.BrowserCfg.IgnoreTLSErrors = b } -func (c *Config) SetBrowserDebug(b bool) { c.BrowserCfg.Debug = b } +func (c *Config) SetBrowserHeadless(b bool) { + c.mu.Lock() + defer c.mu.Unlock() + c.BrowserCfg.Headless = b +} +func (c *Config) SetBrowserDisableCache(b bool) { + c.mu.Lock() + defer c.mu.Unlock() + c.BrowserCfg.DisableCache = b +} +func (c *Config) SetBrowserDisableGPU(b bool) { + c.mu.Lock() + defer c.mu.Unlock() + c.BrowserCfg.DisableGPU = b +} +func (c *Config) SetBrowserIgnoreTLSErrors(b bool) { + c.mu.Lock() + defer c.mu.Unlock() + c.BrowserCfg.IgnoreTLSErrors = b +} +func (c *Config) SetBrowserDebug(b bool) { + c.mu.Lock() + defer c.mu.Unlock() + c.BrowserCfg.Debug = b +} // Humanoid Setters -func (c *Config) SetBrowserHumanoidEnabled(b bool) { c.BrowserCfg.Humanoid.Enabled = b } +func (c *Config) SetBrowserHumanoidEnabled(b bool) { + c.mu.Lock() + defer c.mu.Unlock() + c.BrowserCfg.Humanoid.Enabled = b +} func (c *Config) SetBrowserHumanoidClickHoldMinMs(ms int) { + c.mu.Lock() + defer c.mu.Unlock() c.BrowserCfg.Humanoid.ClickHoldMinMs = ms } func (c *Config) SetBrowserHumanoidClickHoldMaxMs(ms int) { + c.mu.Lock() + defer c.mu.Unlock() c.BrowserCfg.Humanoid.ClickHoldMaxMs = ms } func (c *Config) SetBrowserHumanoidKeyHoldMu(ms float64) { + c.mu.Lock() + defer c.mu.Unlock() c.BrowserCfg.Humanoid.KeyHoldMu = ms } // Network Setters func (c *Config) SetNetworkCaptureResponseBodies(b bool) { + c.mu.Lock() + defer c.mu.Unlock() c.NetworkCfg.CaptureResponseBodies = b } func (c *Config) SetNetworkNavigationTimeout(d time.Duration) { + c.mu.Lock() + defer c.mu.Unlock() c.NetworkCfg.NavigationTimeout = d } -func (c *Config) SetNetworkPostLoadWait(d time.Duration) { c.NetworkCfg.PostLoadWait = d } -func (c *Config) SetNetworkIgnoreTLSErrors(b bool) { c.NetworkCfg.IgnoreTLSErrors = b } +func (c *Config) SetNetworkPostLoadWait(d time.Duration) { + c.mu.Lock() + defer c.mu.Unlock() + c.NetworkCfg.PostLoadWait = d +} +func (c *Config) SetNetworkIgnoreTLSErrors(b bool) { + c.mu.Lock() + defer c.mu.Unlock() + c.NetworkCfg.IgnoreTLSErrors = b +} // IAST Setters -func (c *Config) SetIASTEnabled(b bool) { c.IASTCfg.Enabled = b } +func (c *Config) SetIASTEnabled(b bool) { + c.mu.Lock() + defer c.mu.Unlock() + c.IASTCfg.Enabled = b +} // JWT Setters -func (c *Config) SetJWTEnabled(b bool) { c.ScannersCfg.Static.JWT.Enabled = b } +func (c *Config) SetJWTEnabled(b bool) { + c.mu.Lock() + defer c.mu.Unlock() + c.ScannersCfg.Static.JWT.Enabled = b +} func (c *Config) SetJWTBruteForceEnabled(b bool) { + c.mu.Lock() + defer c.mu.Unlock() c.ScannersCfg.Static.JWT.BruteForceEnabled = b } // ATO Setter func (c *Config) SetATOConfig(atoCfg ATOConfig) { + c.mu.Lock() + defer c.mu.Unlock() c.ScannersCfg.Active.Auth.ATO = atoCfg } diff --git a/pkg/observability/logger.go b/pkg/observability/logger.go index 001f3e64..753cb294 100644 --- a/pkg/observability/logger.go +++ b/pkg/observability/logger.go @@ -18,7 +18,8 @@ var ( // globalLogger stores the global logger instance safely across goroutines. globalLogger atomic.Pointer[zap.Logger] // once ensures that initialization happens exactly once. - once sync.Once + // We use an atomic pointer to sync.Once to allow for safe resetting in tests. + oncePtr atomic.Pointer[sync.Once] ) // ANSI color codes for the terminal. @@ -46,11 +47,16 @@ var colorMap = map[string]string{ "white": colorWhite, } +func init() { + oncePtr.Store(new(sync.Once)) +} + // Initialize sets up the global Zap logger based on configuration and a specified output writer. // This is the core, flexible initializer. func Initialize(cfg config.LoggerConfig, consoleWriter zapcore.WriteSyncer) { - // Ensures initialization logic runs only once. - once.Do(func() { + // Load the current 'Once' instance + o := oncePtr.Load() + o.Do(func() { level := zap.NewAtomicLevel() if err := level.UnmarshalText([]byte(cfg.Level)); err != nil { level.SetLevel(zap.InfoLevel) @@ -100,7 +106,8 @@ func InitializeLogger(cfg config.LoggerConfig) { // This function should ONLY be used in tests to ensure isolation. func ResetForTest() { globalLogger.Store(nil) - once = sync.Once{} + // Atomically replace the old sync.Once with a fresh one + oncePtr.Store(new(sync.Once)) } // SetLogger is a test helper that forcibly replaces the global logger instance. @@ -152,6 +159,7 @@ func newColorizedLevelEncoder(colors config.ColorConfig) zapcore.LevelEncoder { func getEncoder(cfg config.LoggerConfig) zapcore.Encoder { // --- Base Configuration --- // Start with production-ready encoder settings. + // This includes EncodeCaller = ShortCallerEncoder by default. encoderConfig := zap.NewProductionEncoderConfig() // Use a more human-readable time format. encoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02T15:04:05.000Z07:00") @@ -162,6 +170,11 @@ func getEncoder(cfg config.LoggerConfig) zapcore.Encoder { // Enable colorized log levels for better visual distinction. encoderConfig.EncodeLevel = newColorizedLevelEncoder(cfg.Colors) + // Note: We DO NOT set EncodeCaller to nil here. + // If the Entry has a caller (e.g. from zap.AddCaller), we must have an encoder + // or jsonEncoder.EncodeEntry will panic. + // We rely on the ShortCallerEncoder inherited from NewProductionEncoderConfig. + // Customize the encoder to create a clean, single-line log message. // This avoids the multi-line, key-value output of the default console encoder. return newCustomConsoleEncoder(encoderConfig)