Files
edgeguard-owasp-feed/CVE-CHANNEL.md
T
AndreandClaude Opus 5 d8426a1d6c Document importing from a Nuclei template, and what the importer refuses
A CVE record describes a vulnerability, not a request, which is why this
channel is written by hand. A Nuclei template contains the request — path,
query, headers, body — because that is what it sends, so for thousands of CVEs
the shape a virtual patch needs is already written down.

What cannot come across is the template's matchers: they read the RESPONSE to
decide whether the target was actually vulnerable, and a WAF decides before a
response exists. So an imported rule is strictly weaker than the template it
came from — it recognises the attempt, not the exploitation. That is why they
land as action "log", which the engine now honours: observed, recorded as an
event, scoring nothing.

Also records what the importer skips rather than guesses: a CVE already
patched in compiled code, a request that is entirely template variables, a
literal under twelve characters, the site root. And what it corrects: one
leading path segment, because a template is written against a laboratory where
the application is mounted somewhere — /laravel52/vendor/phpunit/... would
have been published unable to match a real attack at /vendor/phpunit/...

Documentation only. cve.json is untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N5fpE6Gu4W6JoFMXyb7t7r
2026-11-04 02:34:54 +00:00

6.3 KiB

The CVE channel (cve.json)

This document carries virtual patches: rules written against a named vulnerability, published as data so that every edge node enforces them within the hour instead of waiting for an engine release.

It is deliberately a second document in this repository, not a second repository and not part of rulesets.json. Each has its own build_datetime, so each is skipped or applied on its own, and a mistake in one cannot take out the other.

Engine 3.0.47 and later fetch it hourly from:

https://git.artica.center/Andre/edgeguard-owasp-feed/raw/branch/main/cve.json

Publishing a patch

Add a rule to the single ruleset, bump build_datetime, commit, push. An unchanged build_datetime means every node skips the document without reading it, so the stamp is what makes a publish take effect.

{
  "build_datetime": "2026-10-22T13:00:00Z",
  "rulesets": [{
    "id": "edgeguard-cve",
    "name": "EdgeGuard CVE Virtual Patches",
    "version": "1",
    "source": "edgeguard-cve",
    "description": "Virtual patches for named vulnerabilities",
    "enabled": true,
    "priority": 1,
    "rules": [{
      "id": "CVE-2025-31161-01",
      "name": "CrushFTP authentication bypass via crafted Authorization header",
      "pattern": "(?i)AWS4-HMAC-SHA256.*Credential=[^/]*/",
      "targets": ["headers"],
      "action": "block",
      "severity": "critical",
      "category": "cve",
      "cve": "CVE-2025-31161",
      "enabled": true,
      "references": ["https://nvd.nist.gov/vuln/detail/CVE-2025-31161"]
    }]
  }]
}

That example is deliberately NOT the PHP upload-progress rule, which this document used to show. That CVE is patched in compiled code, so the guard below refuses it — the one example a reader was most likely to copy was the one example that could not be published. Anything already patched in code belongs in code; see the end of this page.

source must be edgeguard-cve. That is the source the channel owns and the only one it may clear, which is also what makes a rule withdrawable: remove it, bump the stamp, and it stops being enforced everywhere within the hour. Publishing an empty "rulesets": [] withdraws the lot.

Importing from a Nuclei template

A CVE record describes a vulnerability, not a request, which is why this channel is written by hand. A Nuclei template does contain the request — path, query, headers, body — because that is what it sends. So for thousands of CVEs the shape a virtual patch needs is already written down.

go run ./tools/nuclei-importer -cves CVE-2025-31161,CVE-2024-4577 \
    -merge edgeguard-rules/cve.json > /tmp/next.json
cp /tmp/next.json edgeguard-rules/cve.json
bash tools/publish-cve-feed.sh --check

Imported rules land as action: "log", and that is not timidity. A template decides whether a target was actually vulnerable by reading the RESPONSE — words in the body, the content type, the status. A WAF decides before a response exists, so the imported rule is strictly weaker than the template it came from: the template confirms exploitation, the rule only recognises the attempt. Watch what it would have caught in your own traffic, then change log to block deliberately, per rule.

Each imported rule carries an origin field saying a human did not write its pattern and what it came from. Do not remove it — it is the difference between a rule somebody reasoned about and one a converter guessed.

What it refuses to do

It skips rather than guesses, and prints why:

  • A CVE the engine already patches in code. Both rules would fire and the scores would add. Five are patched today: the two PHP2Shell ones, Log4Shell (two) and Spring4Shell.
  • A request with nothing literal in it. Templates are full of {{BaseURL}} and {{string_1}}; a pattern that is mostly .* matches ordinary traffic.
  • A request whose longest literal is under twelve characters, or is the site root. A rule on / matches everything.

What it corrects, and why you should check anyway

It strips one leading path segment when three remain, because a template is written against a laboratory where the application is mounted somewhere: the phpunit template requests /laravel52/vendor/phpunit/... and laravel52 is that lab's directory. Without stripping it the rule is published, enabled, and unable to match a real attack at /vendor/phpunit/....

It prefers a header value carrying an injection marker (${jndi:, <%, $(, ../) over a path, and ignores standard headers entirely whatever their value. An earlier version preferred headers over everything and produced the pattern Content-Type: text/html for a PHP RCE — a rule that says nothing about the attack and something about every request.

None of that makes the output right. Read every pattern before publishing.

Targets

The usual ones (uri, query, args, body, headers, user_agent, cookies, filename, all) plus:

  • param_names — the pattern is run against each parameter NAME on its own: query parameters, urlencoded body fields and multipart Content-Disposition names. An anchored pattern therefore anchors to one name, and a parameter whose value happens to be the string can never match. This is the shape CVE-2026-109618 needed and the reason it could not be written against args, where names and values arrive as one blob and ?note=PHP_SESSION_UPLOAD_PROGRESS would have been refused.

Write param_names patterns in lower case; both the original and the lowercased spelling of each name are tried.

Severity is the score

critical 5, high 4, medium 3, low 2, against a default anomaly threshold of 5. A patch that should refuse on its own is critical; one that should only contribute is medium or below. Nothing else sets the score.

Two things to get right

Do not publish a rule for a CVE the engine already patches in code. Both would fire on the same request, the anomaly score would be the sum, and the event would report one attack as two rules. TestPublishedFeedDoesNotDuplicateACompiledRule in the engine fails if this document names a CVE that ruleCVEReferences already covers.

A patch written in the hours after a disclosure is written against an advisory, not against traffic. There is no backtest. If one turns out to refuse something real, the remedy is to withdraw it — which is why withdrawal is a first-class operation here.