diff --git a/README.md b/README.md index e21bd05..3a36849 100644 --- a/README.md +++ b/README.md @@ -12,35 +12,35 @@ maps the regular CRS IDs from 900K for each rule to the range 9,900,000 - 9,999, | *Plugin Name* | *Rule ID Range* | *Repository* | *Type* | *Status* | *CI* | *License* | |---|---|---|---|---|---|---| -| template | 9,500,000 - 9,500,999 | [coreruleset/template-plugin](https://github.com/coreruleset/template-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/template-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | +| template | 9,500,000 - 9,500,999 | [coreruleset/template-plugin](https://github.com/coreruleset/template-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | | auto-decoding | 9,501,000 - 9,501,999 | [coreruleset/auto-decoding-plugin](https://github.com/coreruleset/auto-decoding-plugin) | official | untested | | Apache-2.0 | | antivirus | 9,502,000 - 9,502,999 | [coreruleset/antivirus-plugin](https://github.com/coreruleset/antivirus-plugin) | official | being tested | | Apache-2.0 | -| body-decompress | 9,503,000 - 9,503,999 | [coreruleset/body-decompress-plugin](https://github.com/coreruleset/body-decompress-plugin) | official | being tested | ![Integration tests](https://github.com/coreruleset/body-decompress-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| fake-bot | 9,504,000 - 9,504,999 | [coreruleset/fake-bot-plugin](https://github.com/coreruleset/fake-bot-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/fake-bot-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| google-oauth2 | 9,505,000 - 9,505,999 | [coreruleset/google-oauth2-plugin](https://github.com/coreruleset/google-oauth2-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/google-oauth2-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| drupal-rule-exclusions | 9,506,000 - 9,506,999 | [coreruleset/drupal-rule-exclusions-plugin](https://github.com/coreruleset/drupal-rule-exclusions-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/drupal-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| wordpress-rule-exclusions | 9,507,000 - 9,507,999 | [coreruleset/wordpress-rule-exclusions-plugin](https://github.com/coreruleset/wordpress-rule-exclusions-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/wordpress-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| nextcloud-rule-exclusions | 9,508,000 - 9,508,999 | [coreruleset/nextcloud-rule-exclusions-plugin](https://github.com/coreruleset/nextcloud-rule-exclusions-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/nextcloud-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| dokuwiki-rule-exclusions | 9,509,000 - 9,509,999 | [coreruleset/dokuwiki-rule-exclusions-plugin](https://github.com/coreruleset/dokuwiki-rule-exclusions-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/dokuwiki-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| cpanel-rule-exclusions | 9,510,000 - 9,510,999 | [coreruleset/cpanel-rule-exclusions-plugin](https://github.com/coreruleset/cpanel-rule-exclusions-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/cpanel-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| xenforo-rule-exclusions | 9,511,000 - 9,511,999 | [coreruleset/xenforo-rule-exclusions-plugin](https://github.com/coreruleset/xenforo-rule-exclusions-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/xenforo-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| phpbb-rule-exclusions | 9,512,000 - 9,512,999 | [coreruleset/phpbb-rule-exclusions-plugin](https://github.com/coreruleset/phpbb-rule-exclusions-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/phpbb-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| phpmyadmin-rule-exclusions | 9,513,000 - 9,513,999 | [coreruleset/phpmyadmin-rule-exclusions-plugin](https://github.com/coreruleset/phpmyadmin-rule-exclusions-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/phpmyadmin-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | +| body-decompress | 9,503,000 - 9,503,999 | [coreruleset/body-decompress-plugin](https://github.com/coreruleset/body-decompress-plugin) | official | being tested | Integration tests | Apache-2.0 | +| fake-bot | 9,504,000 - 9,504,999 | [coreruleset/fake-bot-plugin](https://github.com/coreruleset/fake-bot-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | +| google-oauth2 | 9,505,000 - 9,505,999 | [coreruleset/google-oauth2-plugin](https://github.com/coreruleset/google-oauth2-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | +| drupal-rule-exclusions | 9,506,000 - 9,506,999 | [coreruleset/drupal-rule-exclusions-plugin](https://github.com/coreruleset/drupal-rule-exclusions-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | +| wordpress-rule-exclusions | 9,507,000 - 9,507,999 | [coreruleset/wordpress-rule-exclusions-plugin](https://github.com/coreruleset/wordpress-rule-exclusions-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | +| nextcloud-rule-exclusions | 9,508,000 - 9,508,999 | [coreruleset/nextcloud-rule-exclusions-plugin](https://github.com/coreruleset/nextcloud-rule-exclusions-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | +| dokuwiki-rule-exclusions | 9,509,000 - 9,509,999 | [coreruleset/dokuwiki-rule-exclusions-plugin](https://github.com/coreruleset/dokuwiki-rule-exclusions-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | +| cpanel-rule-exclusions | 9,510,000 - 9,510,999 | [coreruleset/cpanel-rule-exclusions-plugin](https://github.com/coreruleset/cpanel-rule-exclusions-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | +| xenforo-rule-exclusions | 9,511,000 - 9,511,999 | [coreruleset/xenforo-rule-exclusions-plugin](https://github.com/coreruleset/xenforo-rule-exclusions-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | +| phpbb-rule-exclusions | 9,512,000 - 9,512,999 | [coreruleset/phpbb-rule-exclusions-plugin](https://github.com/coreruleset/phpbb-rule-exclusions-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | +| phpmyadmin-rule-exclusions | 9,513,000 - 9,513,999 | [coreruleset/phpmyadmin-rule-exclusions-plugin](https://github.com/coreruleset/phpmyadmin-rule-exclusions-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | | dos-protection-modsecurity | 9,514,000 - 9,514,999 | [coreruleset/dos-protection-plugin-modsecurity](https://github.com/coreruleset/dos-protection-plugin-modsecurity) | official | untested | | Apache-2.0 | -| machine-learning-integration-plugin | 9,516,000 - 9,516,999 | [coreruleset/machine-learning-integration-plugin](https://github.com/coreruleset/machine-learning-integration-plugin) | official | draft | ![Integration tests](https://github.com/coreruleset/machine-learning-integration-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| performance-plugin | 9,517,000 - 9,517,999 | [coreruleset/performance-plugin](https://github.com/coreruleset/performance-plugin) | official | draft (Private) | ![Integration tests](https://github.com/coreruleset/performance-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| ghost-rule-exclusions | 9,518,000 - 9,518,999 | [coreruleset/ghost-rule-exclusions-plugin](https://github.com/coreruleset/ghost-rule-exclusions-plugin) | official | draft (Private) | ![Integration tests](https://github.com/coreruleset/ghost-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| roundcube-rule-exclusions-plugin | 9,519,000 - 9,519,999 | [EsadCetiner/roundcube-rule-exclusions-plugin](https://github.com/EsadCetiner/roundcube-rule-exclusions-plugin) | 3rd-party | ✅ tested | ![Integration tests](https://github.com/EsadCetiner/roundcube-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | GPL-2.0 | -| sogo-rule-exclusions-plugin | 9,520,000 - 9,520,999 | [EsadCetiner/sogo-rule-exclusions-plugin](https://github.com/EsadCetiner/sogo-rule-exclusions-plugin) | 3rd-party | ✅ tested | ![Integration tests](https://github.com/EsadCetiner/sogo-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | GPL-2.0 | -| iredadmin-rule-exclusions-plugin | 9,521,000 - 9,521,999 | [EsadCetiner/iredadmin-rule-exclusions-plugin](https://github.com/EsadCetiner/iredadmin-rule-exclusions-plugin) | 3rd-party | ✅ tested | ![Integration tests](https://github.com/EsadCetiner/iredadmin-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | GPL-2.0 | -| wordpress-hardening-plugin | 9,522,000 - 9,522,999 | [eilandert/wordpress-hardening-plugin](https://github.com/eilandert/wordpress-hardening-plugin) | 3rd-party | ✅ tested | ![Integration tests](https://github.com/eilandert/wordpress-hardening-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | +| machine-learning-integration-plugin | 9,516,000 - 9,516,999 | [coreruleset/machine-learning-integration-plugin](https://github.com/coreruleset/machine-learning-integration-plugin) | official | draft | Integration tests | Apache-2.0 | +| performance-plugin | 9,517,000 - 9,517,999 | [coreruleset/performance-plugin](https://github.com/coreruleset/performance-plugin) | official | draft (Private) | Integration tests | Apache-2.0 | +| ghost-rule-exclusions | 9,518,000 - 9,518,999 | [coreruleset/ghost-rule-exclusions-plugin](https://github.com/coreruleset/ghost-rule-exclusions-plugin) | official | draft (Private) | Integration tests | Apache-2.0 | +| roundcube-rule-exclusions-plugin | 9,519,000 - 9,519,999 | [EsadCetiner/roundcube-rule-exclusions-plugin](https://github.com/EsadCetiner/roundcube-rule-exclusions-plugin) | 3rd-party | ✅ tested | Integration tests | GPL-2.0 | +| sogo-rule-exclusions-plugin | 9,520,000 - 9,520,999 | [EsadCetiner/sogo-rule-exclusions-plugin](https://github.com/EsadCetiner/sogo-rule-exclusions-plugin) | 3rd-party | ✅ tested | Integration tests | GPL-2.0 | +| iredadmin-rule-exclusions-plugin | 9,521,000 - 9,521,999 | [EsadCetiner/iredadmin-rule-exclusions-plugin](https://github.com/EsadCetiner/iredadmin-rule-exclusions-plugin) | 3rd-party | ✅ tested | Integration tests | GPL-2.0 | +| wordpress-hardening-plugin | 9,522,000 - 9,522,999 | [eilandert/wordpress-hardening-plugin](https://github.com/eilandert/wordpress-hardening-plugin) | 3rd-party | ✅ tested | Integration tests | Apache-2.0 | | database-logging-plugin | 9,523,000 - 9,523,999 | [coreruleset/database-logging-plugin](https://github.com/coreruleset/database-logging-plugin) | official | untested | | Apache-2.0 | -| referer-hardening-plugin | 9,524,000 - 9,524,999 | [coreruleset/referer-hardening-plugin](https://github.com/coreruleset/referer-hardening-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/referer-hardening-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| false-positive-report-plugin | 9,525,000 - 9,525,999 | [coreruleset/false-positive-report-plugin](https://github.com/coreruleset/false-positive-report-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/false-positive-report-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| traffic-observation-plugin | 9,526,000 - 9,526,999 | [coreruleset/traffic-observation-plugin](https://github.com/coreruleset/traffic-observation-plugin) | official | untested | ![Integration tests](https://github.com/coreruleset/traffic-observation-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | +| referer-hardening-plugin | 9,524,000 - 9,524,999 | [coreruleset/referer-hardening-plugin](https://github.com/coreruleset/referer-hardening-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | +| false-positive-report-plugin | 9,525,000 - 9,525,999 | [coreruleset/false-positive-report-plugin](https://github.com/coreruleset/false-positive-report-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | +| traffic-observation-plugin | 9,526,000 - 9,526,999 | [coreruleset/traffic-observation-plugin](https://github.com/coreruleset/traffic-observation-plugin) | official | untested | Integration tests | Apache-2.0 | | netnea-crs-upgrading-plugin | 9,527,000 - 9,527,999 | [netnea/netnea-crs-upgrading-plugin](https://github.com/netnea/netnea-crs-upgrading-plugin) | 3rd-party | untested | | GPL-3.0 | -| plausible-rule-exclusions-plugin | 9,528,000 - 9,528,999 | [EsadCetiner/plausible-rule-exclusions-plugin](https://github.com/EsadCetiner/plausible-rule-exclusions-plugin) | 3rd-party | ✅ tested | ![Integration tests](https://github.com/EsadCetiner/plausible-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | GPL-2.0 | -| vimbadmin-crs-plugin | 9,529,000 - 9,529,999 | [eilandert/vimbadmin-crs-plugin](https://github.com/eilandert/vimbadmin-crs-plugin) | 3rd-party | ✅ tested | ![Integration tests](https://github.com/eilandert/vimbadmin-crs-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | +| plausible-rule-exclusions-plugin | 9,528,000 - 9,528,999 | [EsadCetiner/plausible-rule-exclusions-plugin](https://github.com/EsadCetiner/plausible-rule-exclusions-plugin) | 3rd-party | ✅ tested | Integration tests | GPL-2.0 | +| vimbadmin-crs-plugin | 9,529,000 - 9,529,999 | [eilandert/vimbadmin-crs-plugin](https://github.com/eilandert/vimbadmin-crs-plugin) | 3rd-party | ✅ tested | Integration tests | Apache-2.0 | | incubator | 9,900,000 - 9,999,999 | [coreruleset/incubator-plugin](https://github.com/coreruleset/incubator-plugin) | official | being tested | | Apache-2.0 | diff --git a/registry-schema.json b/registry-schema.json index 1517f0b..9fd120b 100644 --- a/registry-schema.json +++ b/registry-schema.json @@ -53,8 +53,8 @@ }, "repository": { "type": "string", - "pattern": "^https://github\\.com/[^/]+/[^/]+/?$", - "description": "URL of the plugin source repository (currently only GitHub repositories are supported)" + "pattern": "^https://github\\.com/[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*/[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?/?$", + "description": "URL of the plugin source repository (currently only GitHub repositories are supported); the owner segment follows GitHub's username/org charset (alphanumerics and single hyphens, no dots or underscores) and the repo segment follows GitHub's repo charset (alphanumerics, '.', '_', '-'), so this value can never break out of the markdown/HTML it's rendered into" }, "type": { "type": "string", diff --git a/scripts/generate_registry.py b/scripts/generate_registry.py index bc2ae20..00234d9 100755 --- a/scripts/generate_registry.py +++ b/scripts/generate_registry.py @@ -12,6 +12,7 @@ import json import re import sys +from html import escape from pathlib import Path import yaml @@ -36,7 +37,7 @@ def format_range(rule_id_range: dict) -> str: def format_repository(repository: str) -> str: slug = repository.removeprefix("https://github.com/").rstrip("/") - return f"[{slug}]({repository})" + return f"[{escape(slug, quote=True)}]({escape(repository, quote=True)})" def format_status(plugin: dict) -> str: @@ -50,7 +51,8 @@ def format_status(plugin: dict) -> str: def format_ci(plugin: dict) -> str: if not plugin.get("ci"): return "" - return f"![Integration tests]({plugin['repository'].rstrip('/')}/actions/workflows/integration.yml/badge.svg)" + url = f"{plugin['repository'].rstrip('/')}/actions/workflows/integration.yml/badge.svg" + return f'Integration tests' def render_row(plugin: dict) -> str: @@ -105,7 +107,7 @@ def render_readme(registry: dict) -> None: pattern = re.compile(re.escape(BEGIN_MARKER) + r".*?" + re.escape(END_MARKER), re.DOTALL) if not pattern.search(readme): raise SystemExit(f"README.md is missing {BEGIN_MARKER} / {END_MARKER} markers") - README.write_text(pattern.sub(table, readme)) + README.write_text(pattern.sub(lambda _match: table, readme)) def render_json(registry: dict) -> None: diff --git a/tests/registry/invalid/bad-repository-owner-punctuation.yaml b/tests/registry/invalid/bad-repository-owner-punctuation.yaml new file mode 100644 index 0000000..16b979f --- /dev/null +++ b/tests/registry/invalid/bad-repository-owner-punctuation.yaml @@ -0,0 +1,8 @@ +schema_version: 1 +plugins: + - name: example + rule_id_range: {start: 9990000, end: 9990999} + repository: https://github.com/foo.bar/example-plugin + type: official + status: tested + license: Apache-2.0 diff --git a/tests/registry/invalid/bad-repository-special-chars.yaml b/tests/registry/invalid/bad-repository-special-chars.yaml new file mode 100644 index 0000000..0a34c36 --- /dev/null +++ b/tests/registry/invalid/bad-repository-special-chars.yaml @@ -0,0 +1,8 @@ +schema_version: 1 +plugins: + - name: example + rule_id_range: {start: 9990000, end: 9990999} + repository: 'https://github.com/foo/bar">' + type: official + status: tested + license: Apache-2.0 diff --git a/tests/registry/test_validate.py b/tests/registry/test_validate.py index 604e53b..99ef08e 100755 --- a/tests/registry/test_validate.py +++ b/tests/registry/test_validate.py @@ -3,15 +3,18 @@ # requires-python = ">=3.11" # dependencies = ["pyyaml==6.0.3"] # /// -"""Self-check for the semantic validation in scripts/generate_registry.py. +"""Self-check for scripts/generate_registry.py: semantic validation and rendering safety. check-jsonschema only catches structural errors; duplicate names and overlapping rule ID ranges are cross-item constraints it cannot express, -so they are asserted here instead. +so they are asserted here instead. This also covers rendering-safety +regressions (HTML/markdown escaping, regex-replacement backreferences) +that a schema alone can't guarantee against. """ import importlib.util -import sys +import os +import tempfile from pathlib import Path ROOT = Path(__file__).resolve().parent.parent.parent @@ -26,7 +29,7 @@ def load(path: Path) -> dict: return yaml.safe_load(path.read_text()) -def main() -> None: +def check_semantic_validation() -> None: registry = load(ROOT / "registry.yaml") assert generate_registry.validate(registry) == [], "registry.yaml must be semantically valid" @@ -38,6 +41,67 @@ def main() -> None: errors = generate_registry.validate(overlapping) assert any("overlapping rule ID ranges" in e for e in errors), errors + +def check_ci_badge_escaping() -> None: + # registry-schema.json's pattern rules this repository value out today, but + # the escaping is defense in depth, independent of the schema. + malicious = {"ci": True, "repository": 'https://github.com/foo/bar" onerror="alert(1)'} + badge = generate_registry.format_ci(malicious) + # The tag has exactly three literal attributes (alt=".." src=".." height="..") -> 6 quotes. + # A leaked, unescaped quote from the malicious value would add 2 more. + assert badge.count('"') == 6, badge + assert """ in badge, badge + + +def check_repository_link_escaping() -> None: + malicious = "https://github.com/foo/bar" + rendered = generate_registry.format_repository(malicious) + assert "