package policy import ( "context" "errors" "fmt" "io" "log/slog" "os" "sync/atomic" "time" "git.sad.ovh/sophie/nuke/internal" "git.sad.ovh/sophie/nuke/internal/dns" "git.sad.ovh/sophie/nuke/lib/config" "git.sad.ovh/sophie/nuke/lib/policy/checker" "git.sad.ovh/sophie/nuke/lib/store" "github.com/fahedouch/go-logrotate" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" _ "git.sad.ovh/sophie/nuke/lib/store/all" ) var ( Applications = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "nuke_policy_results", Help: "The results of each policy rule", }, []string{"rule", "action"}) ErrChallengeRuleHasWrongAlgorithm = errors.New("config.Bot.ChallengeRules: algorithm is invalid") warnedAboutThresholds = &atomic.Bool{} ) type ParsedConfig struct { Store store.Interface orig *config.Config Impressum *config.Impressum OpenGraph config.OpenGraph Bots []Bot Thresholds []*Threshold StatusCodes config.StatusCodes DefaultDifficulty int DNSBL bool DnsCache *dns.DnsCache Dns *dns.Dns Logger *slog.Logger } func newParsedConfig(orig *config.Config) *ParsedConfig { return &ParsedConfig{ orig: orig, OpenGraph: orig.OpenGraph, StatusCodes: orig.StatusCodes, } } func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDifficulty int, logLevel string) (*ParsedConfig, error) { c, err := config.Load(fin, fname) if err != nil { return nil, err } var validationErrs []error result := newParsedConfig(c) result.DefaultDifficulty = defaultDifficulty if c.Logging.Level != nil { logLevel = c.Logging.Level.String() } switch c.Logging.Sink { case config.LogSinkStdio: result.Logger = internal.InitSlog(logLevel, os.Stderr) case config.LogSinkFile: out := &logrotate.Logger{ Filename: c.Logging.Parameters.Filename, FilenameTimeFormat: time.RFC3339, MaxBytes: c.Logging.Parameters.MaxBytes, MaxAge: c.Logging.Parameters.MaxAge, MaxBackups: c.Logging.Parameters.MaxBackups, LocalTime: c.Logging.Parameters.UseLocalTime, Compress: c.Logging.Parameters.Compress, } result.Logger = internal.InitSlog(logLevel, out) } lg := result.Logger.With("at", "config-validate") stFac, ok := store.Get(c.Store.Backend) switch ok { case true: store, err := stFac.Build(ctx, c.Store.Parameters) if err != nil { validationErrs = append(validationErrs, err) } else { result.Store = store } case false: validationErrs = append(validationErrs, config.ErrUnknownStoreBackend) } result.DnsCache = dns.NewDNSCache(result.orig.DNSTTL.Forward, result.orig.DNSTTL.Reverse, result.Store) result.Dns = dns.New(ctx, result.DnsCache) for _, b := range c.Bots { if berr := b.Valid(); berr != nil { validationErrs = append(validationErrs, berr) continue } parsedBot := Bot{ Name: b.Name, Action: b.Action, } cl := checker.List{} if len(b.RemoteAddr) > 0 { c, err := NewRemoteAddrChecker(b.RemoteAddr) if err != nil { validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s remote addr set: %w", b.Name, err)) } else { cl = append(cl, c) } } if b.UserAgentRegex != nil { c, err := NewUserAgentChecker(*b.UserAgentRegex) if err != nil { validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s user agent regex: %w", b.Name, err)) } else { cl = append(cl, c) } } if b.PathRegex != nil { c, err := NewPathChecker(*b.PathRegex) if err != nil { validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s path regex: %w", b.Name, err)) } else { cl = append(cl, c) } } if len(b.HeadersRegex) > 0 { c, err := NewHeadersChecker(b.HeadersRegex) if err != nil { validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s headers regex map: %w", b.Name, err)) } else { cl = append(cl, c) } } if b.Expression != nil { c, err := NewCELChecker(b.Expression, result.Dns) if err != nil { validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s expressions: %w", b.Name, err)) } else { cl = append(cl, c) } } if b.Challenge == nil { parsedBot.Challenge = &config.ChallengeRules{ Difficulty: defaultDifficulty, Algorithm: "fast", } } else { parsedBot.Challenge = b.Challenge if parsedBot.Challenge.Algorithm == "" { parsedBot.Challenge.Algorithm = config.DefaultAlgorithm } if parsedBot.Challenge.Algorithm == "slow" { lg.Warn("use of deprecated algorithm \"slow\" detected, please update this to \"fast\" when possible", "name", parsedBot.Name) } } if b.Weight != nil { parsedBot.Weight = b.Weight } result.Impressum = c.Impressum parsedBot.Rules = cl result.Bots = append(result.Bots, parsedBot) } for _, t := range c.Thresholds { if t.Challenge != nil && t.Challenge.Algorithm == "slow" { lg.Warn("use of deprecated algorithm \"slow\" detected, please update this to \"fast\" when possible", "name", t.Name) } if t.Challenge != nil && t.Challenge.ReportAs != 0 { lg.Warn("use of deprecated report_as setting detected, please remove this from your policy file when possible", "name", t.Name) } if t.Name == "legacy-nuke-behaviour" && t.Expression.String() == "true" { if !warnedAboutThresholds.Load() { lg.Warn("configuration file does not contain thresholds, see docs for details on how to upgrade", "fname", fname, "docs_url", "https://anubis.techaro.lol/docs/admin/configuration/thresholds/") warnedAboutThresholds.Store(true) } t.Challenge.Difficulty = defaultDifficulty } threshold, err := ParsedThresholdFromConfig(t) if err != nil { validationErrs = append(validationErrs, fmt.Errorf("can't compile threshold config for %s: %w", t.Name, err)) continue } result.Thresholds = append(result.Thresholds, threshold) } if len(validationErrs) > 0 { return nil, fmt.Errorf("errors validating policy config JSON %s: %w", fname, errors.Join(validationErrs...)) } result.DNSBL = c.DNSBL return result, nil }