Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

eg-coraza-analyzer

Interactive TUI for analyzing Envoy + Coraza (OWASP CRS) WAF logs and generating seclang rule exceptions.

It correlates each Envoy access-log line with the Coraza rule-trigger lines that share its request id (order-independent), lets you drill down vhost → path → request → violation, mark false positives, track which requests you've already analyzed, and generate copy-pasteable SecRule ... ctl:ruleRemoveTargetById=... exceptions per vhost.

CRS aggregation rules (949*, 959*, 980*) are hidden from the violation lists but their score is shown.

On a wide terminal the screen splits: the left pane is the navigable view and the right pane shows two vertical bar charts (inbound and outbound anomaly score → number of requests). The charts follow the left context (all traffic on the vhost list, the selected vhost once entered) and the active IP filter. They exclude score 0, gap-fill every score from the lowest up to 50 (extending to 100 when higher scores exist) with a final 100+ bucket, label the x-axis every 5, show a y-axis scale, and cap the y-axis so a single spike doesn't flatten the other bars (very wide score ranges fall back to 5-point bins). The charts auto-hide when the terminal is too narrow. The inbound/outbound anomaly scores are read from rule 980170 (the CRS reporting_level correlation rule, logged for every request — its detection value), falling back to 949110/959* for logs captured without reporting_level. The by-score and request lists show both IN and OUT columns.

Build & run

go build -o eg-coraza-analyzer .
./eg-coraza-analyzer ../k8s-demo-cluster/eg-coraza.log     # or any Envoy/Coraza log
./eg-coraza-analyzer --start-id 2000 mylog.log             # first generated SecRule id

# Read straight from a pod (piped stdin is auto-detected):
kubectl logs <envoy-pod> | ./eg-coraza-analyzer
./eg-coraza-analyzer - < mylog.log                         # "-" forces stdin

With no argument and an interactive terminal it reads eg-coraza.log in the current directory. When stdin is piped (or - is given) it reads the log from stdin and takes keyboard input from the controlling terminal (/dev/tty).

Navigation

Vhosts ─enter→ Section ─enter→ Requests ─enter→ Request detail
                  │ (by path)                       (one request,
                  │                                  its violations)
                  └ by score: Section ─enter→ Request detail (directly)
Screen Keys
All ↑/↓ j/k move · enter/ open · esc/ back · / filter · g export · q quit
Vhosts columns: FLAGGED / CLEAN / TOTAL requests (flagged = has a rule violation or anomaly score)
Section s toggle by pathby anomaly score
Request list one row per request; columns SCORE / METHOD / URI / VIOL / RESP
Request detail per-request fields + one row per violation (rule, PL, category, match, data)
Export ↑/↓ scroll · esc back · q quit

False-positive marking (f/F/A) — drives the generated exceptions:

  • f — on the request-detail screen marks the focused violation; on list screens it marks the whole focused request.
  • F — mark the whole focused request.
  • A — mark every violation on the current path.

Analyzed tracking (space / m) — independent of FP marks; just records what you've reviewed so you can work through a noisy log methodically. Marks a request (or a whole path, from the path list) analyzed. Analyzed rows show a and dim; paths show an n/m analyzed progress counter. Row status prefix: analyzed, * fully FP-marked (· = partial). Footer shows live FP and Analyzed counts.

Both FP and analyzed marks are in-memory only — generate the export (g) before quitting. On exit, the generated exceptions are also printed to stdout so they land in your terminal scrollback for copy-paste. (Analyzed marks do not affect the export.)

Client-IP filter (/) — type a single IP (172.18.0.1) or a CIDR (172.18.0.0/16), enter to apply, esc to cancel. The filter is global: it rebuilds the dataset so every screen (vhost counts, paths, scores, requests) reflects only matching requests. An active filter shows a banner under the title (Filter: client <ip/cidr> (matched/total requests)); submit an empty value to clear it. X-Forwarded-For lists match if any address matches.

Layout

  • internal/parse — log → correlated []Request
  • internal/model — domain types, vhost/path aggregation, FP mark set (fpset.go), analyzed/review set (review.go), client-IP filter (ipfilter.go), 920420 body-handling remediation (remediation.go)
  • internal/export — false-positive marks → seclang exceptions
  • internal/tui — Bubble Tea screens & key handling

The export directive de-duplication mirrors the logic in ../k8s-demo-cluster/waf-analyze2.py.

Special-cased rules

A violation Coraza could not attribute to a variable (no match name, or UNKNOWN) has no target to remove, so the rule itself is dropped for the path with ctl:ruleRemoveById=<id>; that also supersedes any target-specific directive for the same rule.

Some rules need a different exception than disabling a target:

  • 911100 (method enforcement) — instead of ctl:ruleRemoveTargetById, the offending HTTP method(s) are appended to the CRS defaults in a single setvar:'tx.allowed_methods=GET HEAD POST OPTIONS <extra…>' directive (offending methods read from the violation's match.value).

  • 920420 (content-type not allowed) — disabling the target is wrong; the rule is disabled and the body is handled explicitly. Marking a 920420 false positive opens a body-handling picker (j JSON · x XML · u URLENCODED · m MULTIPART · o body-access-off), stored per path; press b to change it. The export emits a chained rule per offending content-type:

    SecRule REQUEST_HEADERS:Content-Type "@beginsWith text/plain" "id:N,phase:1,pass,t:none,nolog,chain"
        SecRule REQUEST_URI "@beginsWith /path" "t:none,ctl:ruleRemoveById=920420,ctl:requestBodyProcessor=JSON"
    

    (the last ctl is ctl:requestBodyProcessor=<PROC> or ctl:requestBodyAccess=off).

  • 920450 (PL1) & 920451 (PL2) (HTTP header restricted by policy) — removing the target would disable the check for all restricted headers, so the rule is removed only for requests that actually carry the offending header (taken from the violation's match.key). The export emits a chained rule per offending header; if both PL1 and PL2 fired for the same header, both ids are removed in the same chain:

    SecRule &REQUEST_HEADERS:x-http-method-override "@ge 1" "id:N,phase:1,pass,t:none,nolog,chain"
        SecRule REQUEST_URI "@beginsWith /path" "t:none,ctl:ruleRemoveById=920450"
    

    No extra input is needed — marking the violation false-positive is enough.

About

Analyser for OWASP Coraza Violations in Envoy Gateway

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages