anubis/lib/policy/config/threshold.go
Xe Iaso 226cf36bf7
feat(config): custom weight thresholds via CEL (#688)
* feat(config): add Thresholds to the top level config file

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore(config): make String() on ExpressionOrList join the component expressions

Signed-off-by: Xe Iaso <me@xeiaso.net>

* test(config): ensure unparseable json fails

Signed-off-by: Xe Iaso <me@xeiaso.net>

* fix(config): if no thresholds are set, use the default thresholds

Signed-off-by: Xe Iaso <me@xeiaso.net>

* feat(policy): half implement thresholds

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore(policy): continue wiring things up

Signed-off-by: Xe Iaso <me@xeiaso.net>

* feat(lib): wire up thresholds

Signed-off-by: Xe Iaso <me@xeiaso.net>

* test(lib): handle behavior from legacy configurations

Signed-off-by: Xe Iaso <me@xeiaso.net>

* docs: document thresholds

Signed-off-by: Xe Iaso <me@xeiaso.net>

* docs: update CHANGELOG, refer to threshold configuration

Signed-off-by: Xe Iaso <me@xeiaso.net>

* fix(lib): fix build

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore(lib): fix U1000

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
Signed-off-by: Jason Cameron <git@jasoncameron.dev>
Co-authored-by: Jason Cameron <git@jasoncameron.dev>
2025-06-18 16:58:31 -04:00

81 lines
2.0 KiB
Go

package config
import (
"errors"
"fmt"
"github.com/TecharoHQ/anubis"
)
var (
ErrNoThresholdRulesDefined = errors.New("config: no thresholds defined")
ErrThresholdMustHaveName = errors.New("config.Threshold: must set name")
ErrThresholdMustHaveExpression = errors.New("config.Threshold: must set expression")
ErrThresholdChallengeMustHaveChallenge = errors.New("config.Threshold: a threshold with the CHALLENGE action must have challenge set")
ErrThresholdCannotHaveWeighAction = errors.New("config.Threshold: a threshold cannot have the WEIGH action")
DefaultThresholds = []Threshold{
{
Name: "legacy-anubis-behaviour",
Expression: &ExpressionOrList{
Expression: "weight > 0",
},
Action: RuleChallenge,
Challenge: &ChallengeRules{
Algorithm: "fast",
Difficulty: anubis.DefaultDifficulty,
ReportAs: anubis.DefaultDifficulty,
},
},
}
)
type Threshold struct {
Name string `json:"name" yaml:"name"`
Expression *ExpressionOrList `json:"expression" yaml:"expression"`
Action Rule `json:"action" yaml:"action"`
Challenge *ChallengeRules `json:"challenge" yaml:"challenge"`
}
func (t Threshold) Valid() error {
var errs []error
if len(t.Name) == 0 {
errs = append(errs, ErrThresholdMustHaveName)
}
if t.Expression == nil {
errs = append(errs, ErrThresholdMustHaveExpression)
}
if t.Expression != nil {
if err := t.Expression.Valid(); err != nil {
errs = append(errs, err)
}
}
if err := t.Action.Valid(); err != nil {
errs = append(errs, err)
}
if t.Action == RuleWeigh {
errs = append(errs, ErrThresholdCannotHaveWeighAction)
}
if t.Action == RuleChallenge && t.Challenge == nil {
errs = append(errs, ErrThresholdChallengeMustHaveChallenge)
}
if t.Challenge != nil {
if err := t.Challenge.Valid(); err != nil {
errs = append(errs, err)
}
}
if len(errs) != 0 {
return fmt.Errorf("config: threshold entry for %q is not valid:\n%w", t.Name, errors.Join(errs...))
}
return nil
}