You've already forked caddy-opnsense-blocker
Build initial caddy-opnsense-blocker daemon
This commit is contained in:
153
internal/engine/evaluator_test.go
Normal file
153
internal/engine/evaluator_test.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.dern.ovh/infrastructure/caddy-opnsense-blocker/internal/config"
|
||||
"git.dern.ovh/infrastructure/caddy-opnsense-blocker/internal/model"
|
||||
)
|
||||
|
||||
func TestEvaluatorManualOverridesTakePriority(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
profile := loadProfile(t, `
|
||||
auto_block: true
|
||||
block_unexpected_posts: true
|
||||
block_php_paths: true
|
||||
suspicious_path_prefixes:
|
||||
- /wp-admin
|
||||
`)
|
||||
evaluator := NewEvaluator()
|
||||
record := model.AccessLogRecord{ClientIP: "203.0.113.10", Status: 404, Method: "GET", Path: "/wp-admin/install.php", UserAgent: "curl/8.0"}
|
||||
|
||||
if decision := evaluator.Evaluate(record, profile, model.ManualOverrideForceAllow); decision.Action != model.DecisionActionAllow {
|
||||
t.Fatalf("expected manual allow to win, got %+v", decision)
|
||||
}
|
||||
if decision := evaluator.Evaluate(record, profile, model.ManualOverrideForceBlock); decision.Action != model.DecisionActionBlock {
|
||||
t.Fatalf("expected manual block to win, got %+v", decision)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluatorBlocksSuspiciousRequests(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
profile := loadProfile(t, `
|
||||
auto_block: true
|
||||
block_unexpected_posts: true
|
||||
block_php_paths: true
|
||||
allowed_post_paths:
|
||||
- /search
|
||||
suspicious_path_prefixes:
|
||||
- /wp-admin
|
||||
`)
|
||||
evaluator := NewEvaluator()
|
||||
record := model.AccessLogRecord{ClientIP: "203.0.113.11", Status: 404, Method: "POST", Path: "/wp-admin/install.php", UserAgent: "curl/8.0"}
|
||||
|
||||
decision := evaluator.Evaluate(record, profile, model.ManualOverrideNone)
|
||||
if decision.Action != model.DecisionActionBlock {
|
||||
t.Fatalf("expected block decision, got %+v", decision)
|
||||
}
|
||||
if len(decision.Reasons) < 2 {
|
||||
t.Fatalf("expected multiple blocking reasons, got %+v", decision)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluatorAllowsExcludedCIDRAndKnownAgents(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
profile := loadProfile(t, `
|
||||
auto_block: true
|
||||
excluded_cidrs:
|
||||
- 10.0.0.0/8
|
||||
known_agents:
|
||||
- name: friendly-bot
|
||||
decision: allow
|
||||
user_agent_prefixes:
|
||||
- FriendlyBot/
|
||||
`)
|
||||
evaluator := NewEvaluator()
|
||||
|
||||
excluded := model.AccessLogRecord{ClientIP: "10.0.0.5", Status: 404, Method: "GET", Path: "/wp-login.php", UserAgent: "curl/8.0"}
|
||||
if decision := evaluator.Evaluate(excluded, profile, model.ManualOverrideNone); decision.Action != model.DecisionActionAllow {
|
||||
t.Fatalf("expected excluded cidr to be allowed, got %+v", decision)
|
||||
}
|
||||
|
||||
knownAgent := model.AccessLogRecord{ClientIP: "203.0.113.12", Status: 404, Method: "GET", Path: "/wp-login.php", UserAgent: "FriendlyBot/2.0"}
|
||||
if decision := evaluator.Evaluate(knownAgent, profile, model.ManualOverrideNone); decision.Action != model.DecisionActionAllow {
|
||||
t.Fatalf("expected known agent to be allowed, got %+v", decision)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluatorReturnsReviewWhenAutoBlockIsDisabled(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
profile := loadProfile(t, `
|
||||
auto_block: false
|
||||
block_unexpected_posts: true
|
||||
suspicious_path_prefixes:
|
||||
- /admin
|
||||
`)
|
||||
evaluator := NewEvaluator()
|
||||
record := model.AccessLogRecord{ClientIP: "203.0.113.13", Status: 404, Method: "POST", Path: "/admin", UserAgent: "curl/8.0"}
|
||||
|
||||
decision := evaluator.Evaluate(record, profile, model.ManualOverrideNone)
|
||||
if decision.Action != model.DecisionActionReview {
|
||||
t.Fatalf("expected review decision, got %+v", decision)
|
||||
}
|
||||
}
|
||||
|
||||
func loadProfile(t *testing.T, profileSnippet string) config.ProfileConfig {
|
||||
t.Helper()
|
||||
tempDir := t.TempDir()
|
||||
configPath := filepath.Join(tempDir, "config.yaml")
|
||||
payload := fmt.Sprintf(`profiles:
|
||||
main:%s
|
||||
sources:
|
||||
- name: main
|
||||
path: %s/access.json
|
||||
profile: main
|
||||
`, indent(profileSnippet, 4), tempDir)
|
||||
if err := os.WriteFile(configPath, []byte(payload), 0o600); err != nil {
|
||||
t.Fatalf("write config file: %v", err)
|
||||
}
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("load config: %v", err)
|
||||
}
|
||||
return cfg.Profiles["main"]
|
||||
}
|
||||
|
||||
func indent(value string, spaces int) string {
|
||||
padding := ""
|
||||
for range spaces {
|
||||
padding += " "
|
||||
}
|
||||
lines := []byte(value)
|
||||
_ = lines
|
||||
var output string
|
||||
for _, line := range splitLines(value) {
|
||||
trimmed := line
|
||||
if trimmed == "" {
|
||||
output += "\n"
|
||||
continue
|
||||
}
|
||||
output += "\n" + padding + trimmed
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func splitLines(value string) []string {
|
||||
var lines []string
|
||||
start := 0
|
||||
for index, character := range value {
|
||||
if character == '\n' {
|
||||
lines = append(lines, value[start:index])
|
||||
start = index + 1
|
||||
}
|
||||
}
|
||||
lines = append(lines, value[start:])
|
||||
return lines
|
||||
}
|
||||
Reference in New Issue
Block a user