# 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. ```json { "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.