### 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