policy.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. package policy
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "io"
  7. "log/slog"
  8. "os"
  9. "sync/atomic"
  10. "time"
  11. "github.com/TecharoHQ/anubis/internal"
  12. "github.com/TecharoHQ/anubis/internal/dns"
  13. "github.com/TecharoHQ/anubis/lib/config"
  14. "github.com/TecharoHQ/anubis/lib/policy/checker"
  15. "github.com/TecharoHQ/anubis/lib/store"
  16. "github.com/TecharoHQ/anubis/lib/thoth"
  17. "github.com/fahedouch/go-logrotate"
  18. "github.com/prometheus/client_golang/prometheus"
  19. "github.com/prometheus/client_golang/prometheus/promauto"
  20. _ "github.com/TecharoHQ/anubis/lib/store/all"
  21. )
  22. var (
  23. Applications = promauto.NewCounterVec(prometheus.CounterOpts{
  24. Name: "anubis_policy_results",
  25. Help: "The results of each policy rule",
  26. }, []string{"rule", "action"})
  27. ErrChallengeRuleHasWrongAlgorithm = errors.New("config.Bot.ChallengeRules: algorithm is invalid")
  28. warnedAboutThresholds = &atomic.Bool{}
  29. )
  30. type ParsedConfig struct {
  31. Store store.Interface
  32. orig *config.Config
  33. Impressum *config.Impressum
  34. OpenGraph config.OpenGraph
  35. Bots []Bot
  36. Thresholds []*Threshold
  37. StatusCodes config.StatusCodes
  38. DefaultDifficulty int
  39. DNSBL bool
  40. DnsCache *dns.DnsCache
  41. Dns *dns.Dns
  42. Logger *slog.Logger
  43. }
  44. func newParsedConfig(orig *config.Config) *ParsedConfig {
  45. return &ParsedConfig{
  46. orig: orig,
  47. OpenGraph: orig.OpenGraph,
  48. StatusCodes: orig.StatusCodes,
  49. }
  50. }
  51. func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDifficulty int, logLevel string) (*ParsedConfig, error) {
  52. c, err := config.Load(fin, fname)
  53. if err != nil {
  54. return nil, err
  55. }
  56. var validationErrs []error
  57. tc, hasThothClient := thoth.FromContext(ctx)
  58. result := newParsedConfig(c)
  59. result.DefaultDifficulty = defaultDifficulty
  60. if c.Logging.Level != nil {
  61. logLevel = c.Logging.Level.String()
  62. }
  63. switch c.Logging.Sink {
  64. case config.LogSinkStdio:
  65. result.Logger = internal.InitSlog(logLevel, os.Stderr)
  66. case config.LogSinkFile:
  67. out := &logrotate.Logger{
  68. Filename: c.Logging.Parameters.Filename,
  69. FilenameTimeFormat: time.RFC3339,
  70. MaxBytes: c.Logging.Parameters.MaxBytes,
  71. MaxAge: c.Logging.Parameters.MaxAge,
  72. MaxBackups: c.Logging.Parameters.MaxBackups,
  73. LocalTime: c.Logging.Parameters.UseLocalTime,
  74. Compress: c.Logging.Parameters.Compress,
  75. }
  76. result.Logger = internal.InitSlog(logLevel, out)
  77. }
  78. lg := result.Logger.With("at", "config-validate")
  79. stFac, ok := store.Get(c.Store.Backend)
  80. switch ok {
  81. case true:
  82. store, err := stFac.Build(ctx, c.Store.Parameters)
  83. if err != nil {
  84. validationErrs = append(validationErrs, err)
  85. } else {
  86. result.Store = store
  87. }
  88. case false:
  89. validationErrs = append(validationErrs, config.ErrUnknownStoreBackend)
  90. }
  91. result.DnsCache = dns.NewDNSCache(result.orig.DNSTTL.Forward, result.orig.DNSTTL.Reverse, result.Store)
  92. result.Dns = dns.New(ctx, result.DnsCache)
  93. for _, b := range c.Bots {
  94. if berr := b.Valid(); berr != nil {
  95. validationErrs = append(validationErrs, berr)
  96. continue
  97. }
  98. parsedBot := Bot{
  99. Name: b.Name,
  100. Action: b.Action,
  101. }
  102. cl := checker.List{}
  103. if len(b.RemoteAddr) > 0 {
  104. c, err := NewRemoteAddrChecker(b.RemoteAddr)
  105. if err != nil {
  106. validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s remote addr set: %w", b.Name, err))
  107. } else {
  108. cl = append(cl, c)
  109. }
  110. }
  111. if b.UserAgentRegex != nil {
  112. c, err := NewUserAgentChecker(*b.UserAgentRegex)
  113. if err != nil {
  114. validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s user agent regex: %w", b.Name, err))
  115. } else {
  116. cl = append(cl, c)
  117. }
  118. }
  119. if b.PathRegex != nil {
  120. c, err := NewPathChecker(*b.PathRegex)
  121. if err != nil {
  122. validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s path regex: %w", b.Name, err))
  123. } else {
  124. cl = append(cl, c)
  125. }
  126. }
  127. if len(b.HeadersRegex) > 0 {
  128. c, err := NewHeadersChecker(b.HeadersRegex)
  129. if err != nil {
  130. validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s headers regex map: %w", b.Name, err))
  131. } else {
  132. cl = append(cl, c)
  133. }
  134. }
  135. if b.Expression != nil {
  136. c, err := NewCELChecker(b.Expression, result.Dns)
  137. if err != nil {
  138. validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s expressions: %w", b.Name, err))
  139. } else {
  140. cl = append(cl, c)
  141. }
  142. }
  143. if b.ASNs != nil {
  144. if !hasThothClient {
  145. lg.Warn("You have specified a Thoth specific check but you have no Thoth client configured. Please read https://anubis.techaro.lol/docs/admin/thoth for more information", "check", "asn", "settings", b.ASNs)
  146. continue
  147. }
  148. cl = append(cl, tc.ASNCheckerFor(b.ASNs.Match))
  149. }
  150. if b.GeoIP != nil {
  151. if !hasThothClient {
  152. lg.Warn("You have specified a Thoth specific check but you have no Thoth client configured. Please read https://anubis.techaro.lol/docs/admin/thoth for more information", "check", "geoip", "settings", b.GeoIP)
  153. continue
  154. }
  155. cl = append(cl, tc.GeoIPCheckerFor(b.GeoIP.Countries))
  156. }
  157. if b.Challenge == nil {
  158. parsedBot.Challenge = &config.ChallengeRules{
  159. Difficulty: defaultDifficulty,
  160. Algorithm: "fast",
  161. }
  162. } else {
  163. parsedBot.Challenge = b.Challenge
  164. if parsedBot.Challenge.Algorithm == "" {
  165. parsedBot.Challenge.Algorithm = config.DefaultAlgorithm
  166. }
  167. if parsedBot.Challenge.Algorithm == "slow" {
  168. lg.Warn("use of deprecated algorithm \"slow\" detected, please update this to \"fast\" when possible", "name", parsedBot.Name)
  169. }
  170. }
  171. if b.Weight != nil {
  172. parsedBot.Weight = b.Weight
  173. }
  174. result.Impressum = c.Impressum
  175. parsedBot.Rules = cl
  176. result.Bots = append(result.Bots, parsedBot)
  177. }
  178. for _, t := range c.Thresholds {
  179. if t.Challenge != nil && t.Challenge.Algorithm == "slow" {
  180. lg.Warn("use of deprecated algorithm \"slow\" detected, please update this to \"fast\" when possible", "name", t.Name)
  181. }
  182. if t.Challenge != nil && t.Challenge.ReportAs != 0 {
  183. lg.Warn("use of deprecated report_as setting detected, please remove this from your policy file when possible", "name", t.Name)
  184. }
  185. if t.Name == "legacy-anubis-behaviour" && t.Expression.String() == "true" {
  186. if !warnedAboutThresholds.Load() {
  187. 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/")
  188. warnedAboutThresholds.Store(true)
  189. }
  190. t.Challenge.Difficulty = defaultDifficulty
  191. }
  192. threshold, err := ParsedThresholdFromConfig(t)
  193. if err != nil {
  194. validationErrs = append(validationErrs, fmt.Errorf("can't compile threshold config for %s: %w", t.Name, err))
  195. continue
  196. }
  197. result.Thresholds = append(result.Thresholds, threshold)
  198. }
  199. if len(validationErrs) > 0 {
  200. return nil, fmt.Errorf("errors validating policy config JSON %s: %w", fname, errors.Join(validationErrs...))
  201. }
  202. result.DNSBL = c.DNSBL
  203. return result, nil
  204. }