Skip to content
Open
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
2 changes: 2 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

Coraza is a Web Application Firewall (WAF) engine written in Go that implements Seclang directives and it is compatible with OWASP CRS. It provides protection against common web application attacks.

> For detailed architecture documentation, codebase navigation, and how-to guides, see [AGENTS.md](../AGENTS.md) in the repository root.

## Code Style and Conventions

### General Go Guidelines
Expand Down
295 changes: 295 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
# Coraza WAF - LLM Navigation Guide

This document helps LLMs (GitHub Copilot, Claude, Cursor, etc.) understand the Coraza codebase architecture, navigate the code, and perform common tasks.

## Project Overview

Coraza is a Web Application Firewall (WAF) engine written in Go. It implements the SecLang directive language (compatible with ModSecurity v2/v3), is fully compatible with OWASP CRS v4, and is an OWASP Production Project. Coraza also supports TinyGo compilation for environments with constrained runtimes (e.g. WASM).

## Repository Structure

| Directory | Purpose |
|---|---|
| `types/` | Public API interfaces: `Transaction`, `WAF`, `MatchData`, `Interruption`, `RuleMetadata`, variables |
| `collection/` | Public collection interfaces: `Collection`, `Single`, `Keyed`, `Map` |
| `debuglog/` | Debug logging interfaces and helpers |
| `http/` | HTTP integration helpers and end-to-end tests |
| `testing/` | Test utilities, test data, and CRS regression tests |
| `examples/` | Usage examples (e.g. `http-server`) |
| `experimental/plugins/` | Plugin registration system: `RegisterOperator()`, `RegisterTransformation()`, `RegisterAction()` |
| `experimental/plugins/plugintypes/` | Plugin interfaces: `Operator`, `Transformation`, `Action`, `TransactionState`, `RuleMetadata` |
| `experimental/plugins/macro/` | Macro expansion for rule messages and log data |
| `internal/corazawaf/` | Core WAF and Transaction implementation, RuleGroup evaluation |
| `internal/corazarules/` | Rule metadata and match data implementation |
| `internal/collections/` | Variable storage implementations: `Map`, `Named`, `Single`, `Sized` |
| `internal/operators/` | All operator implementations (~38 non-test files) |
| `internal/transformations/` | All transformation implementations (~33 non-test files) |
| `internal/actions/` | All action implementations (~33 non-test files) |
| `internal/seclang/` | SecLang rule and directive parser |
| `internal/auditlog/` | Audit logging: serial, concurrent, syslog, HTTPS writers; JSON and OCSF formatters |
| `internal/bodyprocessors/` | Body parsers: JSON, XML, multipart, urlencoded, raw |
| `internal/variables/` | Variable type system with generated maps |
| `internal/strings/`, `internal/url/`, `internal/cookies/` | Utility packages |
| `internal/sync/` | TinyGo-compatible sync primitives (`pool.go`, `pool_std.go`, `pool_tinygo.go`) |
| `internal/memoize/` | Memoization utilities for builders |
| `internal/environment/` | Build environment detection (FS access, etc.) |

## Architecture: Request Processing Pipeline

Every HTTP request/response flows through 5 phases. Each `Process*` method triggers `WAF.Rules.Eval(phase, tx)` and checks for interruptions.

### Phase 1 - Request Headers
```
ProcessConnection(clientIP, clientPort, serverIP, serverPort)
-> ProcessURI(uri, method, httpVersion)
-> AddRequestHeader(key, value) // repeat per header
-> ProcessRequestHeaders() -> *Interruption
```
Comment on lines +42 to +47
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add languages to fenced code blocks to satisfy markdown linting.

Several fenced blocks are missing a language hint (e.g., around Line 42 and Line 181). This triggers MD040 and reduces tooling compatibility.

💡 Proposed doc-only fix
-```
+```text
 ProcessConnection(clientIP, clientPort, serverIP, serverPort)
   -> ProcessURI(uri, method, httpVersion)
   -> AddRequestHeader(key, value)  // repeat per header
   -> ProcessRequestHeaders() -> *Interruption

Apply the same `text` (or other appropriate language) label to the other unlabeled fenced blocks.
</details>


Also applies to: 50-53, 57-60, 63-66, 69-71, 181-183

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.22.0)</summary>

[warning] 42-42: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @AGENTS.md around lines 42 - 47, Several fenced code blocks in AGENTS.md (for
example the block containing "ProcessConnection(clientIP, clientPort, serverIP,
serverPort) -> ProcessURI(uri, method, httpVersion) -> AddRequestHeader(key,
value) // repeat per header -> ProcessRequestHeaders() -> *Interruption" and
the other unlabeled blocks around the same area) lack a language hint; add a
language label (e.g., text) to each unlabeled fenced block to satisfy MD040 and markdown linters, updating the opening fences for the blocks that contain constructs like ProcessConnection(...), ProcessURI(...), AddRequestHeader(...), and similar sequences so they become text ... ``` (or another appropriate
language) while leaving the block contents unchanged.


</details>

<!-- fingerprinting:phantom:triton:hawk:edcc74e9-9abc-488e-8a5b-dc306d8c4d0b -->

<!-- This is an auto-generated comment by CodeRabbit -->


### Phase 2 - Request Body
```
WriteRequestBody([]byte) / ReadRequestBodyFrom(io.Reader)
-> ProcessRequestBody() -> (*Interruption, error)
```
Comment on lines +51 to +53
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The request body phase example lists WriteRequestBody([]byte) / ReadRequestBodyFrom(io.Reader) as if they don’t return values, but in the public types.Transaction interface both return (*Interruption, int, error) (interruption + bytes written + error). Update this snippet to reflect the real signatures and/or note that these helpers can surface an interruption directly (and may trigger body processing when limits are reached).

Copilot uses AI. Check for mistakes.
The body processor type (JSON, XML, multipart, urlencoded) is auto-detected from the `Content-Type` header. The processor parses the body into WAF variables (e.g. `ARGS_POST`, `REQUEST_BODY`, `FILES`).

### Phase 3 - Response Headers
```
AddResponseHeader(key, value) // repeat per header
-> ProcessResponseHeaders(statusCode, proto) -> *Interruption
```

### Phase 4 - Response Body
```
WriteResponseBody([]byte) / ReadResponseBodyFrom(io.Reader)
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, the response body phase example omits the actual return values: WriteResponseBody / ReadResponseBodyFrom return (*Interruption, int, error) in the types.Transaction interface. Adjust the snippet so it matches the API and makes it clear callers can receive an interruption/byte count directly from these methods.

Suggested change
WriteResponseBody([]byte) / ReadResponseBodyFrom(io.Reader)
WriteResponseBody([]byte) -> (*Interruption, int, error)
ReadResponseBodyFrom(io.Reader) -> (*Interruption, int, error)

Copilot uses AI. Check for mistakes.
-> ProcessResponseBody() -> (*Interruption, error)
```

### Phase 5 - Logging
```
ProcessLogging()
```

**Key files:**
- `types/transaction.go` - Transaction interface (public API)
- `internal/corazawaf/transaction.go` - Transaction implementation
- `internal/corazawaf/rulegroup.go` - `RuleGroup.Eval(phase, tx)` iterates rules in syntactic order

## Architecture: Rule Evaluation

When `RuleGroup.Eval(phase, tx)` is called, each rule in the group is evaluated in order. Evaluation stops early if an interruption is triggered (except in the logging phase).

### Step-by-step evaluation (`Rule.Evaluate` / `Rule.doEvaluate`):

1. **Variable extraction**: Each rule's `variables` list is iterated. For each variable, `tx.GetField(v)` extracts values from the transaction's collections, applying key filtering (exact string or regex) and exceptions (`!VARIABLE:key`). The `&VARIABLE` syntax returns the count instead of the value.

2. **Transformation pipeline**: Each extracted value passes through the rule's ordered list of transformations. Results are cached per (variable, key, transformationID) to avoid redundant work. The TX variable is never cached. MultiMatch mode runs each transformation individually.

3. **Operator evaluation**: `Rule.executeOperator(transformedValue, tx)` calls `Operator.Evaluate(tx, value) -> bool`. If the rule has `Negation` set, the result is inverted.

4. **Action execution** (only on the parent rule, not chain children):
- **Flow actions** first (e.g. `skip`, `skipAfter`) - always evaluated
- **Disruptive actions** (e.g. `deny`, `drop`, `redirect`) - only when `RuleEngine` is `On` (not `DetectionOnly`)
- **Non-disruptive actions** (e.g. `log`, `setvar`, `capture`) - evaluated per match

5. **Chain processing**: If a rule has `Chain` set, the parent must match first. Then each chained rule is evaluated recursively via `doEvaluate`. All rules in the chain must match for the overall rule to match. If any chain child fails, the entire rule is considered unmatched.

**Key files:**
- `internal/corazawaf/rule.go` - `Rule` struct, `Evaluate()`, `doEvaluate()`, `executeOperator()`, transformation caching
- `internal/corazawaf/rulegroup.go` - `RuleGroup.Eval()` with phase filtering, skip/skipAfter, allow handling

## Plugin System

Coraza is extended through plugins. All registration happens via the `experimental/plugins` package.

### Operators

```go
// Interface (plugintypes/operator.go)
type Operator interface {
Evaluate(TransactionState, string) bool
}
type OperatorFactory func(options OperatorOptions) (Operator, error)

// Registration (plugins/operators.go)
plugins.RegisterOperator("myop", factory)
```

### Transformations

```go
// Type signature (plugintypes/transformation.go)
type Transformation = func(input string) (output string, changed bool, err error)

// Registration (plugins/transformations.go)
plugins.RegisterTransformation("mytrans", transformFunc)
```

### Actions

```go
// Interface (plugintypes/action.go)
type Action interface {
Init(RuleMetadata, string) error
Evaluate(RuleMetadata, TransactionState)
Type() ActionType
}
// ActionType: ActionTypeMetadata (1), ActionTypeDisruptive (2), ActionTypeData (3),
// ActionTypeNondisruptive (4), ActionTypeFlow (5)

// Registration (plugins/actions.go)
type ActionFactory = func() plugintypes.Action
plugins.RegisterAction("myaction", factory)
```

## Collection System

WAF variables are stored in typed collections within each transaction.

| Interface | Description | Example Variables |
|---|---|---|
| `collection.Single` | Single string value | `REQUEST_METHOD`, `RESPONSE_STATUS`, `REQUEST_URI` |
| `collection.Keyed` | Named lookups with key/regex support | `REQUEST_HEADERS`, `ARGS`, `TX` |
| `collection.Map` | Mutable keyed collection (extends `Keyed`) | `REQUEST_HEADERS`, `ARGS_POST`, `ARGS_GET` |

Collections are NOT concurrent-safe. Each transaction has its own isolated set.

**Key files:**
- `collection/collection.go` - Public interfaces: `Collection`, `Single`, `Keyed`, `Map`
- `internal/collections/map.go` - Map implementation with case-insensitive option
- `internal/collections/named.go` - Named collection implementation
- `internal/collections/single.go` - Single value collection
- `internal/collections/sized.go` - Size-tracking collection

## SecLang Parser

The parser compiles SecLang directives into WAF rules and configuration.

### Entry points
- `Parser.FromFile(path)` - Load from file (supports glob patterns with `*`)
- `Parser.FromString(data)` - Load from string

### Parsing flow
1. Lines are read with `bufio.Scanner`
2. Line continuations (`\` at end) and backtick multi-line blocks are handled
3. Comments (`#`) are skipped
4. Each complete line is split into directive name + options
5. The directive name is looked up in `directivesMap` (generated) and the corresponding function is called
6. `include` is handled specially with recursion protection (max 100 levels)

### Rule format
```
SecRule VARIABLES "OPERATOR" "ACTIONS"
```
- **Variable syntax**: `VARIABLE[:key]`, `VARIABLE:/regex/`, `&VARIABLE` (count), `!VARIABLE:key` (exception), `VARIABLE1|VARIABLE2` (multiple)
- **Operator syntax**: `@operatorName arguments` (e.g. `@rx pattern`, `@eq 0`)
- **Actions**: comma-separated list (e.g. `id:100,phase:1,deny,log,msg:'Blocked'`)

**Key files:**
- `internal/seclang/parser.go` - `Parser` struct, `FromFile()`, `FromString()`, line parsing
- `internal/seclang/rule_parser.go` - `RuleParser`, `ParseVariables()`, variable/operator/action parsing
- `internal/seclang/directives.go` - Directive implementations
- `internal/seclang/directivesmap.gen.go` - Generated directive name -> function map

## Build System and TinyGo

### Mage tasks
```bash
go run mage.go test # Run all tests (including memoize_builders, multiphase, CRS)
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go run mage.go test currently runs go test ./..., then reruns tests with -tags=coraza.no_memoize, plus additional suites (examples/http-server with race, and testing/coreruleset with multiple tags). The comment mentioning "memoize_builders" doesn’t match any build tag or what Mage runs today; consider updating this line to reference the actual tag (coraza.no_memoize) and the additional tagged suites (multiphase, no_regex_multiline, case_sensitive_args_keys).

Suggested change
go run mage.go test # Run all tests (including memoize_builders, multiphase, CRS)
go run mage.go test # Run all tests (go test ./..., then with -tags=coraza.no_memoize, plus examples/http-server (race) and testing/coreruleset with multiphase/no_regex_multiline/case_sensitive_args_keys)

Copilot uses AI. Check for mistakes.
go run mage.go lint # Lint (generates code, checks formatting, runs golangci-lint)
go run mage.go coverage # Tests with coverage and race detector
go run mage.go format # Format code (go generate, goimports, addlicense)
go run mage.go fuzz # Run fuzz tests
```

### Build tags
| Tag | Effect |
|---|---|
| `coraza.disabled_operators.<name>` | Exclude a specific operator from compilation |
| `coraza.rule.multiphase_evaluation` | Evaluate rule variables in phases they become ready |
| `coraza.rule.case_sensitive_args_keys` | Case-sensitive ARGS key matching (RFC 3986) |
| `coraza.rule.no_regex_multiline` | Disable default multiline mode in `@rx` operator |
| `coraza.rule.mandatory_rule_id_check` | Require `id` action for all SecRule/SecAction |
| `tinygo` | TinyGo-compatible build (affects sync primitives, FS access) |
| `memoize_builders` | Enable memoization of operator/transformation builders |
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The build tags table appears inaccurate/incomplete compared to the repository’s documented/used tags: there is no memoize_builders build tag in the codebase, and coraza.no_memoize (used in Mage/CI and documented in README) is missing. Suggest removing memoize_builders, adding coraza.no_memoize, and aligning the list with README.md’s “Build tags” section to avoid confusing integrators/LLMs.

Suggested change
| `memoize_builders` | Enable memoization of operator/transformation builders |
| `coraza.no_memoize` | Disable memoization of operator/transformation builders |

Copilot uses AI. Check for mistakes.
| `no_fs_access` | Disable filesystem access |

Comment on lines +206 to +216
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Surround the build-tags table with blank lines.

The table starting at Line 206 should be separated by blank lines to satisfy MD058.

💡 Proposed doc-only fix
 ### Build tags
+
 | Tag | Effect |
 |---|---|
 | `coraza.disabled_operators.<name>` | Exclude a specific operator from compilation |
 | `coraza.rule.multiphase_evaluation` | Evaluate rule variables in phases they become ready |
 | `coraza.rule.case_sensitive_args_keys` | Case-sensitive ARGS key matching (RFC 3986) |
 | `coraza.rule.no_regex_multiline` | Disable default multiline mode in `@rx` operator |
 | `coraza.rule.mandatory_rule_id_check` | Require `id` action for all SecRule/SecAction |
 | `tinygo` | TinyGo-compatible build (affects sync primitives, FS access) |
 | `memoize_builders` | Enable memoization of operator/transformation builders |
 | `no_fs_access` | Disable filesystem access |
+
 ### Generated code
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| Tag | Effect |
|---|---|
| `coraza.disabled_operators.<name>` | Exclude a specific operator from compilation |
| `coraza.rule.multiphase_evaluation` | Evaluate rule variables in phases they become ready |
| `coraza.rule.case_sensitive_args_keys` | Case-sensitive ARGS key matching (RFC 3986) |
| `coraza.rule.no_regex_multiline` | Disable default multiline mode in `@rx` operator |
| `coraza.rule.mandatory_rule_id_check` | Require `id` action for all SecRule/SecAction |
| `tinygo` | TinyGo-compatible build (affects sync primitives, FS access) |
| `memoize_builders` | Enable memoization of operator/transformation builders |
| `no_fs_access` | Disable filesystem access |
### Build tags
| Tag | Effect |
|---|---|
| `coraza.disabled_operators.<name>` | Exclude a specific operator from compilation |
| `coraza.rule.multiphase_evaluation` | Evaluate rule variables in phases they become ready |
| `coraza.rule.case_sensitive_args_keys` | Case-sensitive ARGS key matching (RFC 3986) |
| `coraza.rule.no_regex_multiline` | Disable default multiline mode in `@rx` operator |
| `coraza.rule.mandatory_rule_id_check` | Require `id` action for all SecRule/SecAction |
| `tinygo` | TinyGo-compatible build (affects sync primitives, FS access) |
| `memoize_builders` | Enable memoization of operator/transformation builders |
| `no_fs_access` | Disable filesystem access |
### Generated code
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 206-206: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@AGENTS.md` around lines 206 - 216, The Markdown table that begins with the
header "| Tag | Effect |" and lists tags like
`coraza.disabled_operators.<name>`, `coraza.rule.multiphase_evaluation`,
`tinygo`, etc., needs blank lines inserted immediately before and after the
table to satisfy MD058; update the AGENTS.md content to add a single empty line
above the table header and a single empty line after the final `| no_fs_access |
Disable filesystem access |` row.

### Generated code
- `internal/seclang/directivesmap.gen.go` - Generated from `internal/seclang/generator/`
- `internal/variables/variablesmap.gen.go` - Generated from `internal/variables/generator/`

Run `go generate ./...` to regenerate (also done by `go run mage.go format` and `go run mage.go lint`).

### TinyGo
TinyGo support affects concurrency primitives. The `internal/sync/` package provides pool implementations:
- `pool_std.go` - Standard Go `sync.Pool`
- `pool_tinygo.go` - TinyGo-compatible alternative

## Common Tasks

### Adding a new operator

1. Create `internal/operators/my_operator.go`:
```go
type myOperator struct {
data string
}
func (o *myOperator) Evaluate(tx plugintypes.TransactionState, value string) bool {
// implementation
}
```
2. Register in `internal/operators/` init or via `experimental/plugins/operators.go`:
```go
plugins.RegisterOperator("myOperator", func(options plugintypes.OperatorOptions) (plugintypes.Operator, error) {
return &myOperator{data: options.Arguments}, nil
})
```
3. Add tests in `internal/operators/my_operator_test.go`
Comment on lines +232 to +247
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “Adding a new operator” steps mix two different workflows and the example as written would create an import cycle if followed inside internal/operators/ (because experimental/plugins imports internal/operators). If the intent is adding a built-in operator, the registration should be done via internal/operators.Register(...) from within the operators package; if the intent is an external plugin, show plugins.RegisterOperator(...) from an integrator’s package (not from internal/operators).

Suggested change
1. Create `internal/operators/my_operator.go`:
```go
type myOperator struct {
data string
}
func (o *myOperator) Evaluate(tx plugintypes.TransactionState, value string) bool {
// implementation
}
```
2. Register in `internal/operators/` init or via `experimental/plugins/operators.go`:
```go
plugins.RegisterOperator("myOperator", func(options plugintypes.OperatorOptions) (plugintypes.Operator, error) {
return &myOperator{data: options.Arguments}, nil
})
```
3. Add tests in `internal/operators/my_operator_test.go`
There are two ways to add an operator:
- As a **built-in operator** in the core `internal/operators` package.
- As an **external plugin operator** registered via `experimental/plugins` from your own package.
#### Built-in operator (core)
1. Create `internal/operators/my_operator.go`:
```go
package operators
import "github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes"
type myOperator struct {
data string
}
func (o *myOperator) Evaluate(tx plugintypes.TransactionState, value string) bool {
// implementation
return false
}
func init() {
Register("myOperator", func(options plugintypes.OperatorOptions) (plugintypes.Operator, error) {
return &myOperator{data: options.Arguments}, nil
})
}
  1. Add tests in internal/operators/my_operator_test.go.

External operator plugin (integrator)

  1. In your own package (not in internal/operators), create e.g. my_operator_plugin.go:
    package mywaf
    
    import (
        "github.com/corazawaf/coraza/v3/experimental/plugins"
        "github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes"
    )
    
    type myOperator struct {
        data string
    }
    
    func (o *myOperator) Evaluate(tx plugintypes.TransactionState, value string) bool {
        // implementation
        return false
    }
    
    func init() {
        plugins.RegisterOperator("myOperator", func(options plugintypes.OperatorOptions) (plugintypes.Operator, error) {
            return &myOperator{data: options.Arguments}, nil
        })
    }
  2. Add tests in your package to cover the new operator.

Copilot uses AI. Check for mistakes.

### Adding a new transformation

1. Create `internal/transformations/my_transform.go`:
```go
func myTransform(input string) (string, bool, error) {
// return (result, changed, nil)
}
```
2. Register via `experimental/plugins/transformations.go`:
```go
plugins.RegisterTransformation("myTransform", myTransform)
```
Comment on lines +257 to +260
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transformation section has the same workflow mix-up as operators: registering via experimental/plugins/transformations.go is something an integrator would do from outside the internal/transformations package. If you’re documenting how to add a built-in transformation under internal/transformations/, the example should use that package’s internal registration mechanism (to avoid an import cycle and match existing built-ins).

Copilot uses AI. Check for mistakes.
3. Add tests in `internal/transformations/my_transform_test.go`

### Adding a new action

1. Create `internal/actions/my_action.go` implementing `plugintypes.Action`:
```go
type myAction struct {}
func (a *myAction) Init(metadata plugintypes.RuleMetadata, data string) error { return nil }
func (a *myAction) Evaluate(metadata plugintypes.RuleMetadata, tx plugintypes.TransactionState) { }
func (a *myAction) Type() plugintypes.ActionType { return plugintypes.ActionTypeNondisruptive }
```
2. Register via `experimental/plugins/actions.go`:
```go
plugins.RegisterAction("myAction", func() plugintypes.Action { return &myAction{} })
```
Comment on lines +272 to +275
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as operators/transformations: the “Adding a new action” section shows plugins.RegisterAction(...) but places it in a workflow that starts by creating internal/actions/my_action.go. Using experimental/plugins from internal/actions would introduce an import cycle; for built-in actions the registration should be done via the internal actions registry, while plugins.RegisterAction should be shown as code that lives in an integrator/plugin package.

Copilot uses AI. Check for mistakes.
3. Add tests in `internal/actions/my_action_test.go`

### Adding a new directive

1. Add the directive function in `internal/seclang/directives.go`:
```go
func directiveMyDirective(options *DirectiveOptions) error { ... }
```
2. Run `go generate ./internal/seclang/...` to regenerate the directives map
3. If the directive sets WAF-level config, add the field to `internal/corazawaf/waf.go`

## Testing Patterns

- **Table-driven tests** with `t.Run()` for logical grouping
- **Operator tests** follow SpiderLabs secrules-language-tests format with JSON test data in `testdata/` directories
- **Full test suite**: `go run mage.go test`
- **Coverage with race detector**: `go run mage.go coverage`
- **CRS regression tests**: Located in `testing/coreruleset/`
- **End-to-end HTTP tests**: Located in `http/` package
- **Fuzz tests**: `go run mage.go fuzz` (operators: SQLi/XSS, transformations: base64/cmdline)
Loading