Skip to content

fix(rules): match web-attack rules on the CRS attack_type the daemon emits - #230

Merged
ralyodio merged 1 commit into
masterfrom
fix/rules-match-daemon-events
Sep 25, 2026
Merged

ralyodio merged 1 commit into
masterfrom
fix/rules-match-daemon-events

Conversation

@ralyodio

@ralyodio ralyodio commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

What

The shipped web-attack rules (web-sqli-attack, web-path-traversal, web-xss-attack, exploit-probe-pattern) matched the message text Attack detected [X]. Only threatcrush monitor prints that; the daemon's log-watcher writes Attack [X]. None of these rules has ever fired on a daemon event.

Change

  • The four rules now match the structured details.attack_type from the CRS verdict (feat(cli): score web requests with the OWASP Core Rule Set (PL1) #222), which the engine already resolves as a bare field name, so the engine needed no change. Message wording no longer matters to any web rule, so monitor and log-watcher keep their current text.
  • Sub-threshold requests also carry attack_type (as a low Suspicious … or Client error … event). Each rule therefore also needs severity to be high or critical, which log-watcher assigns only at or above the anomaly threshold.
  • exploit-probe-pattern listed CMD_INJECTION and XXE, which CRS never reports (it files command injection under rce, and an access log has no request body for XXE). It now matches rce|ssrf|rfi|php_injection|ssti.
  • web-sqli-attack and web-path-traversal go from critical to high. A critical rule would take a single-CRS-rule high event past min_severity = "critical", which the docs say requires two matching rules.
  • threatcrush rules show <id> now prints and/or sub-conditions. Without that it would show attack_type equals "sqli" and leave out the severity gate.
  • One paragraph added to docs/auto-defence.md.

I checked every other default rule for the same bug

  • ssh-brute-force, ssh-success-after-failures, ssh-root-login, ssh-user-enumeration: their text matches log-watcher's auth output. OK.
  • sudo-abuse and system-critical-error match journal-watcher's [ident] message and severity. OK.
  • port-scan-indicator matches network-monitor's Port scan detected: … (case-insensitive contains). OK.
  • web-scanner-detection, paywall-hammering, paywall-scrape-distributed match log-watcher's Client error NNN:. OK.
  • Not fixed here, listed for follow-up:
    • No rule covers the CRS types scanner and injection, or a null type. Module severity still bans them.
    • remediation.ttl_seconds on rules is shown by rules show but ignored by RemediationManager, which always uses the Fibonacci ladder.
    • On journald-only hosts (no /var/log/auth.log), sshd events come from journal-watcher as user-journal/system, and the ssh rules neither select nor match them.

Measurement (on top of #228, real traffic, 15 rotated nginx access logs, ~238k lines → ~89k events)

A throwaway script replayed each line through LogWatcher.process and then RuleEngine, with the clock set to each line's timestamp. It is not committed.

origin/master this PR
web-path-traversal firings 0 876
exploit-probe-pattern firings 0 54
web-xss-attack firings 0 0
web-sqli-attack firings 0 0 (no SQLi at threshold in these logs)
web-scanner-detection firings 247 247
unique IPs a block rule bans (min_severity=high) 0 360 (path-traversal 350, exploit-probe 41; overlapping)
unique IPs banned by module severity (min_severity=high) 487 487
rule bans not already banned by module severity 0 0
rule bans at min_severity=critical 0 0 (rules are high)

Event mix is the same on both sides: 49,271 high, 964 critical, 38,719 low. None of the low events carried an attack_type at the default threshold. For the sub-threshold case, see the test below.

The set of banned IPs does not change. Rules now fire and are recorded as detections. Because the rule engine runs before remediation on the bus, a ban is now attributed to the rule (rule_id, [DETECTION] … reason) rather than the raw event.

Hand-check of newly firing request shapes, grouped with digits normalised. Every group is an attack probe:

  • path-traversal: dotfile and .git probes, cgi-bin/.%2e/…/bin/sh, literal ../
  • exploit-probe: php-cgi allow_url_include / auto_prepend_file, Mozi/router wget …;sh RCE, cloud-metadata SSRF/RFI, vault.yml fetches
  • xss: before fix(cli): leave CRS 941130 off by default so *.xhtml pages are not banned #228 the only hit was a .xhtml page flagged by 941130. With 941130 off by default there are none.

Tests

apps/cli/src/daemon/__tests__/web-attack-rules.test.ts sends real access-log lines through the daemon's LogWatcher and then the default rules:

  • One case each for sqli, path traversal, xss and RCE asserts that the right rule fires. All four fail on origin/master (checked by restoring master's default-rules.ts).
  • At anomaly_threshold = 10, a 5-point /.env request becomes a low event with attack_type = path_traversal, at status 200 and at 404. The test asserts that no web-attack rule fires. These two fail if the severity gate is removed (checked).

vitest run src/daemon src/core src/__tests__/doc-claims.test.ts: 150 passed.

@ralyodio
ralyodio force-pushed the fix/rules-match-daemon-events branch from 2e64bd2 to 8990f6e Compare September 25, 2026 16:36
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

12 finding(s)

HIGH/CRITICAL: 1 | MEDIUM: 6 | LOW: 5

Severity Rule Location
HIGH secret-aws-access-key prd/0003-detect-hardcoded-secrets-before-they-are-committed-or-served.md:126
MEDIUM js-open-redirect apps/web/src/app/auth/login/page.tsx:50
MEDIUM js-unescaped-html-sink apps/web/src/app/hire/page.tsx:104
MEDIUM js-unescaped-html-sink apps/web/src/app/hire/page.tsx:108
MEDIUM js-open-redirect apps/web/src/components/funding/FundingClient.tsx:97
MEDIUM js-unescaped-html-sink apps/web/src/components/GuideReader.tsx:265
MEDIUM js-uninitialized-buffer packages/scan/src/node-rules.ts:456
LOW secret-generic-credential PRD.md:269
LOW tls-verification-disabled prd/0004-find-dangerous-code-patterns-without-pretending-to-be-a-compiler.md:121
LOW tls-verification-disabled prd/0004-find-dangerous-code-patterns-without-pretending-to-be-a-compiler.md:122
LOW sh-remote-script-execution scripts/smoke-test.sh:47
LOW secret-aws-access-key scripts/smoke-test.sh:112

Snippets are redacted; ThreatCrush never prints matched credential material.

…emits

The web-sqli-attack, web-path-traversal, web-xss-attack and
exploit-probe-pattern rules matched the message text 'Attack detected [X]',
which only 'threatcrush monitor' prints; the daemon's log-watcher writes
'Attack [X]', so none of them ever fired on a daemon event.

Match the structured details.attack_type instead, gated on high/critical
severity so a sub-threshold request (which also carries attack_type) never
trips them. exploit-probe-pattern listed CMD_INJECTION and XXE, which CRS
never reports; it now covers rce, ssrf, rfi, php_injection and ssti.
web-sqli-attack and web-path-traversal drop from critical to high so a
rule never lifts a one-CRS-rule hit past min_severity = critical.

'threatcrush rules show' now prints and/or sub-conditions.
@ralyodio
ralyodio force-pushed the fix/rules-match-daemon-events branch from 8990f6e to d091495 Compare September 25, 2026 16:39
@ralyodio
ralyodio merged commit 9c4c916 into master Sep 25, 2026
11 checks passed
@ralyodio
ralyodio deleted the fix/rules-match-daemon-events branch September 25, 2026 16:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant