### Run Coraza HTTP Server Source: https://github.com/corazawaf/coraza/blob/main/examples/http-server/README.md Starts the server using the default configuration. ```bash go run . ``` -------------------------------- ### Install pre-commit hook Source: https://github.com/corazawaf/coraza/blob/main/CONTRIBUTING.md Install the pre-commit git hook using the mage build tool. This hook helps ensure code quality before committing. ```sh go run mage.go precommit ``` -------------------------------- ### Run specific Go tests Source: https://github.com/corazawaf/coraza/blob/main/CONTRIBUTING.md Run tests within a specific package and filter them by name. This example runs tests in the loggers package that match 'TestDefaultWriters'. ```go go test -run TestDefaultWriters -v ./loggers ``` -------------------------------- ### Initialize Coraza WAF Source: https://github.com/corazawaf/coraza/blob/main/README.md Demonstrates creating a WAF instance, parsing rules, and processing a request transaction. ```go package main import ( "fmt" "github.com/corazawaf/coraza/v3" ) func main() { // First we initialize our waf and our seclang parser waf, err := coraza.NewWAF(coraza.NewWAFConfig(). WithDirectives(`SecRule REMOTE_ADDR "@rx .*" "id:1,phase:1,deny,status:403"`)) // Now we parse our rules if err != nil { fmt.Println(err) } // Then we create a transaction and assign some variables tx := waf.NewTransaction() defer func() { tx.ProcessLogging() tx.Close() }() tx.ProcessConnection("127.0.0.1", 8080, "127.0.0.1", 12345) // Finally we process the request headers phase, which may return an interruption if it := tx.ProcessRequestHeaders(); it != nil { fmt.Printf("Transaction was interrupted with status %d\n", it.Status) } } ``` -------------------------------- ### Implement Rule Observer for Rule Loading Monitoring in Go Source: https://context7.com/corazawaf/coraza/llms.txt Use the experimental rule observer to track rule loading for configuration validation, caching, and deployment verification. This Go code demonstrates how to set up an observer function that counts rules by phase and severity. ```go package main import ( "fmt" "log" "github.com/corazawaf/coraza/v3" "github.com/corazawaf/coraza/v3/experimental" "github.com/corazawaf/coraza/v3/types" ) func main() { ruleCount := 0 rulesByPhase := make(map[types.RulePhase]int) rulesBySeverity := make(map[types.RuleSeverity]int) observer := func(rule types.RuleMetadata) { ruleCount++ rulesByPhase[rule.Phase()]++ rulesBySeverity[rule.Severity()]++ fmt.Printf("Loaded rule %d: phase=%d, severity=%s, tags=%v\n", rule.ID(), rule.Phase(), rule.Severity(), rule.Tags()) } cfg := coraza.NewWAFConfig(). WithDirectives(` SecRuleEngine On SecRule REQUEST_URI "@contains /admin" "id:1,phase:1,deny,severity:critical,tag:'admin'" SecRule REQUEST_URI "@contains /api" "id:2,phase:1,log,severity:notice,tag:'api'" SecRule ARGS "@rx ", "GET", "HTTP/1.1") tx.AddRequestHeader("User-Agent", "TestBot/1.0") tx.ProcessRequestHeaders() tx.ProcessRequestBody() // Iterate matched rules for _, mr := range tx.MatchedRules() { rule := mr.Rule() fmt.Printf("Rule %d matched:\n", rule.ID()) fmt.Printf(" Message: %s\n", mr.Message()) fmt.Printf(" Severity: %s\n", rule.Severity()) fmt.Printf(" Phase: %d\n", rule.Phase()) fmt.Printf(" Tags: %v\n", rule.Tags()) fmt.Printf(" Disruptive: %t\n", mr.Disruptive()) // Access matched data for _, md := range mr.MatchedDatas() { fmt.Printf(" Matched %s:%s = %s\n", md.Variable(), md.Key(), md.Value()) } // Get formatted logs fmt.Printf(" ErrorLog: %s\n", mr.ErrorLog()) fmt.Printf(" AuditLog: %s\n", mr.AuditLog()) fmt.Println() } // Export as JSON for SIEM integration type RuleMatch struct { ID int `json:"id"` Message string `json:"message"` Severity string `json:"severity"` Tags []string `json:"tags"` } var matches []RuleMatch for _, mr := range tx.MatchedRules() { matches = append(matches, RuleMatch{ ID: mr.Rule().ID(), Message: mr.Message(), Severity: mr.Rule().Severity().String(), Tags: mr.Rule().Tags(), }) } jsonData, _ := json.MarshalIndent(matches, "", " ") fmt.Printf("JSON Export:\n%s\n", jsonData) } ``` -------------------------------- ### Implement Custom Debug Logger in Go Source: https://context7.com/corazawaf/coraza/llms.txt Defines a custom logger structure and event handler to integrate with Coraza's WAF operations. ```go package main import ( "fmt" "io" "log" "os" "github.com/corazawaf/coraza/v3" "github.com/corazawaf/coraza/v3/debuglog" ) // Custom logger implementation type customLogger struct { output io.Writer level debuglog.Level fields []debuglog.ContextField } type customEvent struct { logger *customLogger level string fields map[string]interface{} enabled bool } func (e *customEvent) Msg(msg string) { if !e.enabled { return } fmt.Fprintf(e.logger.output, "[%s] %s %v\n", e.level, msg, e.fields) } func (e *customEvent) Str(key, val string) debuglog.Event { e.fields[key] = val return e } func (e *customEvent) Err(err error) debuglog.Event { if err != nil { e.fields["error"] = err.Error() } return e } func (e *customEvent) Bool(key string, b bool) debuglog.Event { e.fields[key] = b return e } func (e *customEvent) Int(key string, i int) debuglog.Event { e.fields[key] = i return e } func (e *customEvent) Uint(key string, i uint) debuglog.Event { e.fields[key] = i return e } func (e *customEvent) Stringer(key string, val fmt.Stringer) debuglog.Event { if val != nil { e.fields[key] = val.String() } return e } func (e *customEvent) IsEnabled() bool { return e.enabled } func (l *customLogger) newEvent(level string, lvl debuglog.Level) debuglog.Event { return &customEvent{ logger: l, level: level, fields: make(map[string]interface{}), enabled: lvl <= l.level, } } func (l *customLogger) Trace() debuglog.Event { return l.newEvent("TRACE", debuglog.LevelTrace) } func (l *customLogger) Debug() debuglog.Event { return l.newEvent("DEBUG", debuglog.LevelDebug) } func (l *customLogger) Info() debuglog.Event { return l.newEvent("INFO", debuglog.LevelInfo) } func (l *customLogger) Warn() debuglog.Event { return l.newEvent("WARN", debuglog.LevelWarn) } func (l *customLogger) Error() debuglog.Event { return l.newEvent("ERROR", debuglog.LevelError) } func (l *customLogger) WithOutput(w io.Writer) debuglog.Logger { return &customLogger{output: w, level: l.level, fields: l.fields} } func (l *customLogger) WithLevel(lvl debuglog.Level) debuglog.Logger { return &customLogger{output: l.output, level: lvl, fields: l.fields} } func (l *customLogger) With(fields ...debuglog.ContextField) debuglog.Logger { return &customLogger{output: l.output, level: l.level, fields: append(l.fields, fields...)} } func main() { logger := &customLogger{ output: os.Stdout, level: debuglog.LevelDebug, } waf, err := coraza.NewWAF( coraza.NewWAFConfig(). WithDebugLogger(logger). WithDirectives(` SecRuleEngine On SecDebugLogLevel 4 SecRule REQUEST_URI "@unconditionalMatch" "id:1,phase:1,pass,log,msg:'Request logged'" `), ) if err != nil { log.Fatal(err) } tx := waf.NewTransaction() tx.ProcessURI("/test", "GET", "HTTP/1.1") tx.ProcessRequestHeaders() tx.ProcessLogging() tx.Close() } ``` -------------------------------- ### Define and query a SecDataset Source: https://github.com/corazawaf/coraza/blob/main/CHANGELOG.md Use SecDataset to define in-memory data and query it using the @pmFromDataset operator. ```apache SecDataset restricted-files-1 ` .my.cnf .mysql_history ` SecRule REQUEST_FILENAME "@pmFromDataset restricted-files-1" \ "...msg:'Match sample_dataset'" ``` -------------------------------- ### Handle WAF Interruptions Source: https://context7.com/corazawaf/coraza/llms.txt Check for interruptions using ProcessRequestHeaders or IsInterrupted to determine if a request should be blocked, redirected, or dropped. ```go package main import ( "fmt" "log" "net/http" "github.com/corazawaf/coraza/v3" "github.com/corazawaf/coraza/v3/types" ) func handleRequest(waf coraza.WAF, w http.ResponseWriter, r *http.Request) { tx := waf.NewTransaction() defer func() { tx.ProcessLogging() tx.Close() }() tx.ProcessConnection(r.RemoteAddr, 0, "", 0) tx.ProcessURI(r.URL.String(), r.Method, r.Proto) for k, vv := range r.Header { for _, v := range vv { tx.AddRequestHeader(k, v) } } // Check for interruption after each phase if it := tx.ProcessRequestHeaders(); it != nil { handleInterruption(w, it) return } // Alternative: check IsInterrupted() at any point if tx.IsInterrupted() { it := tx.Interruption() handleInterruption(w, it) return } w.Write([]byte("OK")) } func handleInterruption(w http.ResponseWriter, it *types.Interruption) { status := it.Status if status == 0 { status = http.StatusForbidden } log.Printf("Request blocked: action=%s, status=%d, rule=%d, data=%s", it.Action, status, it.RuleID, it.Data) switch it.Action { case "deny": w.WriteHeader(status) w.Write([]byte("Access Denied")) case "redirect": http.Redirect(w, nil, it.Data, http.StatusFound) case "drop": // Close connection immediately (implementation specific) hj, ok := w.(http.Hijacker) if ok { conn, _, _ := hj.Hijack() conn.Close() } } } func main() { waf, _ := coraza.NewWAF( coraza.NewWAFConfig(). WithDirectives(` SecRuleEngine On SecRule REQUEST_URI "@contains /blocked" "id:1,phase:1,deny,status:403" SecRule REQUEST_URI "@contains /redirect" "id:2,phase:1,redirect:https://example.com" `), ) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { handleRequest(waf, w, r) }) log.Fatal(http.ListenAndServe(":8080", nil)) } ``` -------------------------------- ### Register Custom Metrics Action in Go Source: https://context7.com/corazawaf/coraza/llms.txt Implements and registers a custom 'metric' action. This action prints a message indicating a metric is being incremented, simulating metric collection for rule hits. ```go // Custom action for metrics type metricsAction struct { metricName string } func (m *metricsAction) Init(r plugintypes.RuleMetadata, data string) error { m.metricName = data if m.metricName == "" { m.metricName = fmt.Sprintf("waf_rule_%d_hits", r.ID()) } return nil } func (m *metricsAction) Evaluate(r plugintypes.RuleMetadata, tx plugintypes.TransactionState) { // In production, increment Prometheus counter or similar fmt.Printf("[METRIC] Incrementing %s\n", m.metricName) } func (m *metricsAction) Type() plugintypes.ActionType { return plugintypes.ActionTypeNondisruptive } func init() { plugins.RegisterAction("metric", func() plugintypes.Action { return &metricsAction{} }) } ``` -------------------------------- ### Test Coraza Server with Curl Source: https://github.com/corazawaf/coraza/blob/main/examples/http-server/README.md Sends requests to the server to verify WAF behavior for true positive and true negative cases. ```bash # True positive request (403 Forbidden) curl -i 'localhost:8090/hello?id=0' # True negative request (200 OK) curl -i 'localhost:8090/hello' ``` -------------------------------- ### Register Custom Alert Action in Go Source: https://context7.com/corazawaf/coraza/llms.txt Implements and registers a custom 'alert' action. This action increments a counter and prints a message indicating an alert has been triggered, simulating sending alerts to a specific destination. ```go package main import ( "fmt" "log" "sync/atomic" "github.com/corazawaf/coraza/v3" "github.com/corazawaf/coraza/v3/experimental/plugins" "github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes" ) var alertCount int64 // Custom action that sends alerts type alertAction struct { destination string } func (a *alertAction) Init(r plugintypes.RuleMetadata, data string) error { a.destination = data if a.destination == "" { a.destination = "default-channel" } return nil } func (a *alertAction) Evaluate(r plugintypes.RuleMetadata, tx plugintypes.TransactionState) { atomic.AddInt64(&alertCount, 1) // In production, send to alerting system fmt.Printf("[ALERT] Rule %d triggered, sending to %s (total alerts: %d)\n", r.ID(), a.destination, atomic.LoadInt64(&alertCount)) } func (a *alertAction) Type() plugintypes.ActionType { return plugintypes.ActionTypeNondisruptive } func init() { plugins.RegisterAction("alert", func() plugintypes.Action { return &alertAction{} }) } ``` -------------------------------- ### Registering Custom Operators in Go Source: https://context7.com/corazawaf/coraza/llms.txt Implements custom operators for JWT validation and keyword detection, then registers them for use in WAF directives. ```go package main import ( "fmt" "log" "regexp" "strings" "github.com/corazawaf/coraza/v3" "github.com/corazawaf/coraza/v3/experimental/plugins" "github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes" ) // Custom operator that checks if input matches a JWT pattern type jwtOperator struct { pattern *regexp.Regexp } func (o *jwtOperator) Evaluate(tx plugintypes.TransactionState, input string) bool { return o.pattern.MatchString(input) } func init() { plugins.RegisterOperator("isJWT", func(options plugintypes.OperatorOptions) (plugintypes.Operator, error) { // JWT pattern: header.payload.signature (base64url encoded parts) pattern := regexp.MustCompile(`^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$`) return &jwtOperator{pattern: pattern}, nil }) } // Custom operator with arguments type containsAnyOperator struct { words []string } func (o *containsAnyOperator) Evaluate(tx plugintypes.TransactionState, input string) bool { lower := strings.ToLower(input) for _, word := range o.words { if strings.Contains(lower, word) { return true } } return false } func init() { plugins.RegisterOperator("containsAny", func(options plugintypes.OperatorOptions) (plugintypes.Operator, error) { words := strings.Split(options.Arguments, ",") for i := range words { words[i] = strings.TrimSpace(strings.ToLower(words[i])) } return &containsAnyOperator{words: words}, nil }) } func main() { waf, err := coraza.NewWAF( coraza.NewWAFConfig(). WithDirectives(` SecRuleEngine On SecRule ARGS:token "@isJWT" "id:1,phase:1,deny,status:400,msg:'JWT not allowed in query params'" SecRule REQUEST_BODY "@containsAny admin,root,system" "id:2,phase:2,deny,status:403,msg:'Forbidden keyword in body'" `), ) if err != nil { log.Fatal(err) } tx := waf.NewTransaction() tx.ProcessURI("/?token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0.signature", "GET", "HTTP/1.1") if it := tx.ProcessRequestHeaders(); it != nil { fmt.Printf("Blocked: Rule %d - %s\n", it.RuleID, "JWT detected in query params") } tx.ProcessLogging() tx.Close() } ``` -------------------------------- ### Evaluate URL paths with @restpath Source: https://github.com/corazawaf/coraza/blob/main/CHANGELOG.md Use the @restpath operator to match URL patterns and extract variables into ARGS_PATH. ```apache SecRule REQUEST_URI "@restpath /some/random/url/{id}/{name}" "…..chain" SecRule ARGS_PATH:id "!@eq %{user:session_id}" "deny" ``` -------------------------------- ### Test Response Body Matching Source: https://github.com/corazawaf/coraza/blob/main/examples/http-server/README.md Verifies that the server returns a 403 Forbidden status when the response body matches configured rules. ```bash # True positive request (403 Forbidden) due to matching response body curl -i 'localhost:8090/hello' ``` -------------------------------- ### Manual Coraza Transaction Processing Source: https://context7.com/corazawaf/coraza/llms.txt Manually process HTTP requests through Coraza WAF phases for fine-grained control. This involves creating a transaction, processing each phase (connection, URI, headers, body), and handling responses. Ensure to call `ProcessLogging` and `Close` on the transaction. ```go package main import ( "fmt" "log" "strings" "github.com/corazawaf/coraza/v3" "github.com/corazawaf/coraza/v3/types" ) func main() { waf, _ := coraza.NewWAF( coraza.NewWAFConfig(). WithDirectives(` SecRuleEngine On SecRequestBodyAccess On SecRule REQUEST_URI "@contains /admin" "id:1,phase:1,deny,status:403" SecRule REQUEST_HEADERS:User-Agent "@contains sqlmap" "id:2,phase:1,deny,status:403" SecRule ARGS:cmd "@rx (;|\\||&)" "id:3,phase:2,deny,status:400,msg:'Command injection attempt'" `), ) tx := waf.NewTransaction() defer func() { tx.ProcessLogging() tx.Close() }() // Phase 0: Process connection tx.ProcessConnection("192.168.1.100", 54321, "10.0.0.1", 80) // Phase 1: Process URI and headers tx.ProcessURI("/api/users?cmd=ls|cat", "POST", "HTTP/1.1") tx.SetServerName("example.com") tx.AddRequestHeader("Host", "example.com") tx.AddRequestHeader("User-Agent", "Mozilla/5.0") tx.AddRequestHeader("Content-Type", "application/x-www-form-urlencoded") if it := tx.ProcessRequestHeaders(); it != nil { log.Printf("Blocked at headers: status=%d, action=%s", it.Status, it.Action) return } // Phase 2: Process request body body := "cmd=ls|cat /etc/passwd" if it, n, err := tx.WriteRequestBody([]byte(body)); err != nil { log.Fatal(err) } else if it != nil { log.Printf("Blocked during body write: status=%d", it.Status) return } else { fmt.Printf("Wrote %d bytes to request body buffer\n", n) } if it, err := tx.ProcessRequestBody(); err != nil { log.Fatal(err) } else if it != nil { log.Printf("Blocked at request body: status=%d, rule=%d", it.Status, it.RuleID) return } // Phase 3: Process response headers tx.AddResponseHeader("Content-Type", "application/json") if it := tx.ProcessResponseHeaders(200, "HTTP/1.1"); it != nil { log.Printf("Blocked at response headers: status=%d", it.Status) return } // Phase 4: Process response body responseBody := `{"status": "ok"}` tx.WriteResponseBody([]byte(responseBody)) if it, err := tx.ProcessResponseBody(); err != nil { log.Fatal(err) } else if it != nil { log.Printf("Blocked at response body: status=%d", it.Status) return } // Check matched rules for _, mr := range tx.MatchedRules() { fmt.Printf("Matched Rule ID: %d, Msg: %s\n", mr.Rule().ID(), mr.Message()) } fmt.Println("Request processed successfully") } ``` -------------------------------- ### Register Custom Transformations in Go Source: https://context7.com/corazawaf/coraza/llms.txt Register custom transformation functions for use in Coraza WAF rules. These functions modify input data before evaluation. Ensure transformations handle potential errors gracefully. ```go package main import ( "encoding/base64" "fmt" "log" "strings" "github.com/corazawaf/coraza/v3" "github.com/corazawaf/coraza/v3/experimental/plugins" ) func init() { // Custom transformation: reverse a string plugins.RegisterTransformation("reverse", func(input string) (string, bool, error) { runes := []rune(input) for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { runes[i], runes[j] = runes[j], runes[i] } return string(runes), true, nil }) // Custom transformation: normalize whitespace plugins.RegisterTransformation("normalizeWS", func(input string) (string, bool, error) { fields := strings.Fields(input) result := strings.Join(fields, " ") return result, result != input, nil }) // Custom transformation: decode custom encoding plugins.RegisterTransformation("customB64", func(input string) (string, bool, error) { // Handle URL-safe base64 without padding input = strings.ReplaceAll(input, "-", "+") input = strings.ReplaceAll(input, "_", "/") // Add padding if necessary switch len(input) % 4 { case 2: input += "==" case 3: input += "=" } decoded, err := base64.StdEncoding.DecodeString(input) if err != nil { return input, false, nil // Return original on error } return string(decoded), true, nil }) } func main() { waf, err := coraza.NewWAF( coraza.NewWAFConfig(). WithDirectives(` SecRuleEngine On SecRule ARGS:data "t:reverse,@contains tpircs" "id:1,phase:1,deny,msg:'Reversed script detected'" SecRule ARGS:encoded "t:customB64,@contains