Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 114 additions & 47 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,41 +61,16 @@
return nil
}

func processDecisions(decisions *models.DecisionsStreamResponse, supportedActions []string) waf.Decisions {
d := waf.Decisions{
V4Add: make(map[string][]*string),
V6Add: make(map[string][]*string),
V4Del: make(map[string][]*string),
V6Del: make(map[string][]*string),
CountriesAdd: make(map[string][]wafv2types.CountryCode),
CountriesDel: make(map[string][]wafv2types.CountryCode),
}
func processDecisions(decisions *models.DecisionsStreamResponse, supportedActions []string) []cfg.ProcessedDecision {
processed := make([]cfg.ProcessedDecision, 0)

for _, decision := range decisions.New {
decisionType := strings.ToLower(*decision.Type)
if !slices.Contains(supportedActions, decisionType) {
decisionType = "fallback"
}

if strings.ToLower(*decision.Scope) == "ip" || strings.ToLower(*decision.Scope) == "range" {
if strings.Contains(*decision.Value, ":") {
if !strings.Contains(*decision.Value, "/") {
d.V6Add[decisionType] = append(d.V6Add[decisionType], aws.String(fmt.Sprintf("%s/128", *decision.Value)))
} else {
d.V6Add[decisionType] = append(d.V6Add[decisionType], decision.Value)
}
} else {
if !strings.Contains(*decision.Value, "/") {
d.V4Add[decisionType] = append(d.V4Add[decisionType], aws.String(fmt.Sprintf("%s/32", *decision.Value)))
} else {
d.V4Add[decisionType] = append(d.V4Add[decisionType], decision.Value)
}
}
} else if strings.ToLower(*decision.Scope) == "country" {
d.CountriesAdd[decisionType] = append(d.CountriesAdd[decisionType], wafv2types.CountryCode(*decision.Value))
} else {
log.Errorf("unsupported scope: %s", *decision.Scope)
}
processed = append(processed, buildProcessedDecision(decision, decisionType, "add")...)
}

for _, decision := range decisions.Deleted {
Expand All @@ -104,31 +79,117 @@
decisionType = "fallback"
}

if strings.ToLower(*decision.Scope) == "ip" || strings.ToLower(*decision.Scope) == "range" {
if strings.Contains(*decision.Value, ":") {
if !strings.Contains(*decision.Value, "/") {
d.V6Del[decisionType] = append(d.V6Del[decisionType], aws.String(fmt.Sprintf("%s/128", *decision.Value)))
} else {
d.V6Del[decisionType] = append(d.V6Del[decisionType], decision.Value)
}
} else {
if !strings.Contains(*decision.Value, "/") {
d.V4Del[decisionType] = append(d.V4Del[decisionType], aws.String(fmt.Sprintf("%s/32", *decision.Value)))
} else {
d.V4Del[decisionType] = append(d.V4Del[decisionType], decision.Value)
}
processed = append(processed, buildProcessedDecision(decision, decisionType, "del")...)
}

return processed
}

func buildProcessedDecision(decision *models.Decision, decisionType string, op string) []cfg.ProcessedDecision {
if decision == nil || decision.Scope == nil || decision.Value == nil {
return nil
}

scope := strings.ToLower(*decision.Scope)
value := *decision.Value
origin := ""
scenario := ""

if decision.Origin != nil {
origin = *decision.Origin
}

if decision.Scenario != nil {
scenario = *decision.Scenario
}

switch scope {
case "ip", "range":
if strings.Contains(value, ":") {
if !strings.Contains(value, "/") {
value = fmt.Sprintf("%s/128", value)
}
} else if strings.ToLower(*decision.Scope) == "country" {
d.CountriesDel[decisionType] = append(d.CountriesDel[decisionType], wafv2types.CountryCode(*decision.Value))
} else {
log.Errorf("unsupported scope: %s", *decision.Scope)

return []cfg.ProcessedDecision{{
Action: decisionType,
Target: "v6",
Value: value,
Op: op,
Origin: origin,
Scenario: scenario,
}}
}

if !strings.Contains(value, "/") {
value = fmt.Sprintf("%s/32", value)
}

return []cfg.ProcessedDecision{{
Action: decisionType,
Target: "v4",
Value: value,
Op: op,
Origin: origin,
Scenario: scenario,
}}
case "country":
return []cfg.ProcessedDecision{{
Action: decisionType,
Target: "country",
Value: value,
Op: op,
Origin: origin,
Scenario: scenario,
}}
default:
log.Errorf("unsupported scope: %s", scope)
}

return nil
}

func aggregateDecisions(processed []cfg.ProcessedDecision) waf.Decisions {
d := waf.Decisions{
V4Add: make(map[string][]*string),
V6Add: make(map[string][]*string),
V4Del: make(map[string][]*string),
V6Del: make(map[string][]*string),
CountriesAdd: make(map[string][]wafv2types.CountryCode),
CountriesDel: make(map[string][]wafv2types.CountryCode),
}

for _, p := range processed {
switch p.Target {
case "v4":
switch p.Op {
case "add":
d.V4Add[p.Action] = append(d.V4Add[p.Action], aws.String(p.Value))
case "del":
d.V4Del[p.Action] = append(d.V4Del[p.Action], aws.String(p.Value))
}
case "v6":
switch p.Op {
case "add":
d.V6Add[p.Action] = append(d.V6Add[p.Action], aws.String(p.Value))
case "del":
d.V6Del[p.Action] = append(d.V6Del[p.Action], aws.String(p.Value))
}
case "country":
switch p.Op {
case "add":
d.CountriesAdd[p.Action] = append(d.CountriesAdd[p.Action], wafv2types.CountryCode(p.Value))
case "del":
d.CountriesDel[p.Action] = append(d.CountriesDel[p.Action], wafv2types.CountryCode(p.Value))
}
default:
log.Errorf("unsupported target: %s", p.Target)
}
}

return d
}

func Execute() error {

Check failure on line 192 in cmd/root.go

View workflow job for this annotation

GitHub Actions / golangci-lint + codeql

function-length: maximum number of lines per function exceeded; max 153 but got 159 (revive)
configPath := flag.String("c", "", "path to crowdsec-aws-waf-bouncer.yaml")
bouncerVersion := flag.Bool("version", false, "display version and exit")
traceMode := flag.Bool("trace", false, "set trace mode")
Expand Down Expand Up @@ -265,9 +326,15 @@
continue
}

d := processDecisions(decisions, config.SupportedActions)
processed := processDecisions(decisions, config.SupportedActions)

for _, w := range wafInstances {
w.DecisionsChan <- d
filteredDecisions := w.GetDecisionsFilter().Apply(processed)
if len(filteredDecisions) == 0 {
continue
}

w.DecisionsChan <- aggregateDecisions(filteredDecisions)
}
}
}
Expand Down
114 changes: 101 additions & 13 deletions pkg/cfg/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,27 +30,107 @@
SupportedActions []string `yaml:"supported_actions"`
}

type DecisionsFilter struct {
IncludeOrigins []string `yaml:"include_origins"`
ExcludeOrigins []string `yaml:"exclude_origins"`
IncludeScenarios []string `yaml:"include_scenarios"`
ExcludeScenarios []string `yaml:"exclude_scenarios"`
}

type ProcessedDecision struct {
Action string
Target string // v4, v6, country
Value string
Op string // add or del
Origin string
Scenario string
}

func (d DecisionsFilter) isEmpty() bool {
return len(d.IncludeOrigins) == 0 &&
len(d.ExcludeOrigins) == 0 &&
len(d.IncludeScenarios) == 0 &&
len(d.ExcludeScenarios) == 0
}

func matchValue(value *string, include []string, exclude []string) bool {
if value == nil {
return false
}

for _, excluded := range exclude {
if strings.EqualFold(excluded, *value) {
return false
}
}

if len(include) == 0 {
return true
}

for _, allowed := range include {
if strings.EqualFold(allowed, *value) {
return true
}
}

return false
}

func (d DecisionsFilter) allow(decision ProcessedDecision) bool {
if d.isEmpty() {
return true
}

if !matchValue(&decision.Origin, d.IncludeOrigins, d.ExcludeOrigins) {
return false
}

if !matchValue(&decision.Scenario, d.IncludeScenarios, d.ExcludeScenarios) {
return false
}

return true
}

func (d DecisionsFilter) Apply(decisions []ProcessedDecision) []ProcessedDecision {
if d.isEmpty() {
return decisions
}

filtered := make([]ProcessedDecision, 0, len(decisions))

for _, decision := range decisions {
if d.allow(decision) {
filtered = append(filtered, decision)
}
}

return filtered
}

type AclConfig struct {
WebACLName string `yaml:"web_acl_name"`
RuleGroupName string `yaml:"rule_group_name"`
Region string `yaml:"region"`
Scope string `yaml:"scope"`
IpsetPrefix string `yaml:"ipset_prefix"`
FallbackAction string `yaml:"fallback_action"`
AWSProfile string `yaml:"aws_profile"`
IPHeader string `yaml:"ip_header"`
IPHeaderPosition string `yaml:"ip_header_position"`
Capacity int `yaml:"capacity"`
CloudWatchEnabled bool `yaml:"cloudwatch_enabled"`
CloudWatchMetricName string `yaml:"cloudwatch_metric_name"`
SampleRequests bool `yaml:"sample_requests"`
WebACLName string `yaml:"web_acl_name"`
RuleGroupName string `yaml:"rule_group_name"`
Region string `yaml:"region"`
Scope string `yaml:"scope"`
IpsetPrefix string `yaml:"ipset_prefix"`
FallbackAction string `yaml:"fallback_action"`
AWSProfile string `yaml:"aws_profile"`
IPHeader string `yaml:"ip_header"`
IPHeaderPosition string `yaml:"ip_header_position"`
Capacity int `yaml:"capacity"`
CloudWatchEnabled bool `yaml:"cloudwatch_enabled"`
CloudWatchMetricName string `yaml:"cloudwatch_metric_name"`
SampleRequests bool `yaml:"sample_requests"`
DecisionsFilter DecisionsFilter `yaml:"decisions_filter"`
}

var ValidActions = []string{"ban", "captcha", "count"}
var validScopes = []string{"REGIONAL", "CLOUDFRONT"}
var validIpHeaderPosition = []string{"FIRST", "LAST", "ANY"}

func getConfigFromEnv(config *bouncerConfig) {

Check failure on line 133 in pkg/cfg/config.go

View workflow job for this annotation

GitHub Actions / golangci-lint + codeql

cyclomatic: function getConfigFromEnv has cyclomatic complexity 50 (> max enabled 46) (revive)
var (
key string
value string
Expand Down Expand Up @@ -124,6 +204,14 @@
log.Warnf("Invalid value for %s: %s, defaulting to false", key, value)
acl.SampleRequests = false
}
case "DECISIONS_FILTER_INCLUDE_ORIGINS":
acl.DecisionsFilter.IncludeOrigins = strings.Split(value, ",")
case "DECISIONS_FILTER_EXCLUDE_ORIGINS":
acl.DecisionsFilter.ExcludeOrigins = strings.Split(value, ",")
case "DECISIONS_FILTER_INCLUDE_SCENARIOS":
acl.DecisionsFilter.IncludeScenarios = strings.Split(value, ",")
case "DECISIONS_FILTER_EXCLUDE_SCENARIOS":
acl.DecisionsFilter.ExcludeScenarios = strings.Split(value, ",")
}
} else {
switch key {
Expand Down
8 changes: 8 additions & 0 deletions pkg/waf/waf.go
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,14 @@ func (w *WAF) UpdateGeoSet(ctx context.Context, d Decisions) error {
return nil
}

func (w *WAF) GetDecisionsFilter() cfg.DecisionsFilter {
if w.config == nil {
return cfg.DecisionsFilter{}
}

return w.config.DecisionsFilter
}

func (w *WAF) Process(ctx context.Context) error {
for {
select {
Expand Down
Loading