diff --git a/internal/config/config.go b/internal/config/config.go index 47d6d6c..746e65d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -65,6 +65,9 @@ type InvestigationConfig struct { BackgroundPollInterval Duration `yaml:"background_poll_interval"` BackgroundLookback Duration `yaml:"background_lookback"` BackgroundBatchSize int `yaml:"background_batch_size"` + + enabledDefined bool + spamhausEnabledDefined bool } type OPNsenseConfig struct { @@ -79,6 +82,42 @@ type OPNsenseConfig struct { EnsureAlias bool `yaml:"ensure_alias"` Alias AliasConfig `yaml:"alias"` APIPaths APIPathsConfig `yaml:"api_paths"` + + ensureAliasDefined bool +} + +func (c *InvestigationConfig) UnmarshalYAML(node *yaml.Node) error { + type raw InvestigationConfig + var decoded raw + if err := node.Decode(&decoded); err != nil { + return err + } + *c = InvestigationConfig(decoded) + for index := 0; index+1 < len(node.Content); index += 2 { + switch node.Content[index].Value { + case "enabled": + c.enabledDefined = true + case "spamhaus_enabled": + c.spamhausEnabledDefined = true + } + } + return nil +} + +func (c *OPNsenseConfig) UnmarshalYAML(node *yaml.Node) error { + type raw OPNsenseConfig + var decoded raw + if err := node.Decode(&decoded); err != nil { + return err + } + *c = OPNsenseConfig(decoded) + for index := 0; index+1 < len(node.Content); index += 2 { + if node.Content[index].Value == "ensure_alias" { + c.ensureAliasDefined = true + break + } + } + return nil } type AliasConfig struct { @@ -170,7 +209,7 @@ func (c *Config) applyDefaults() error { if c.Storage.Path == "" { c.Storage.Path = "./data/caddy-opnsense-blocker.db" } - if !c.Investigation.Enabled { + if !c.Investigation.enabledDefined { c.Investigation.Enabled = true } if c.Investigation.RefreshAfter.Duration == 0 { @@ -182,7 +221,7 @@ func (c *Config) applyDefaults() error { if strings.TrimSpace(c.Investigation.UserAgent) == "" { c.Investigation.UserAgent = "caddy-opnsense-blocker/0.2" } - if !c.Investigation.SpamhausEnabled { + if !c.Investigation.spamhausEnabledDefined { c.Investigation.SpamhausEnabled = true } if c.Investigation.BackgroundWorkers == 0 { @@ -225,7 +264,7 @@ func (c *Config) applyDefaults() error { if c.OPNsense.APIPaths.AliasUtilDelete == "" { c.OPNsense.APIPaths.AliasUtilDelete = "/api/firewall/alias_util/delete/{alias}" } - if !c.OPNsense.EnsureAlias { + if !c.OPNsense.ensureAliasDefined { c.OPNsense.EnsureAlias = true } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index a624c5b..ffaba37 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -76,6 +76,15 @@ sources: if got := cfg.Investigation.BackgroundLookback.Duration; got != 0 { t.Fatalf("unexpected background lookback default: got %s want 0", got) } + if !cfg.Investigation.Enabled { + t.Fatalf("expected investigation to default to enabled") + } + if !cfg.Investigation.SpamhausEnabled { + t.Fatalf("expected spamhaus checks to default to enabled") + } + if !cfg.OPNsense.EnsureAlias { + t.Fatalf("expected ensure_alias to default to enabled") + } profile := cfg.Profiles["main"] if !profile.IsAllowedPostPath("/search") { t.Fatalf("expected /search to be normalized as an allowed POST path") @@ -107,3 +116,41 @@ sources: t.Fatalf("expected invalid initial_position to be rejected") } } + +func TestLoadPreservesExplicitFalseForTrueDefaults(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + configPath := filepath.Join(tempDir, "config.yaml") + payload := fmt.Sprintf(`investigation: + enabled: false + spamhaus_enabled: false +opnsense: + enabled: false + ensure_alias: false +profiles: + main: + auto_block: true +sources: + - name: main + path: %s/access.json + profile: main +`, tempDir) + if err := os.WriteFile(configPath, []byte(payload), 0o600); err != nil { + t.Fatalf("write config file: %v", err) + } + + cfg, err := Load(configPath) + if err != nil { + t.Fatalf("load config: %v", err) + } + if cfg.Investigation.Enabled { + t.Fatalf("expected investigation.enabled=false to be preserved") + } + if cfg.Investigation.SpamhausEnabled { + t.Fatalf("expected investigation.spamhaus_enabled=false to be preserved") + } + if cfg.OPNsense.EnsureAlias { + t.Fatalf("expected opnsense.ensure_alias=false to be preserved") + } +}