From 1b3ccdaf1153e1c5b21dc2327f82f4a1e4901e83 Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Mon, 7 Sep 2026 08:44:39 -0300 Subject: [PATCH] feat: add machine-readable registry.yaml, generate README table Adds registry.yaml as the authoritative, machine-readable plugin index (rule ID range, repository, type, status, license), validated against registry-schema.json in CI. README.md's table and registry.json are generated from it by scripts/generate_registry.py; CI regenerates and fails on drift instead of committing from the workflow. Migrating the table into registry.yaml also fixes the drift found in the audit: swapped CI badges between nextcloud/dokuwiki (now derived from each plugin's own repository field, making the swap structurally impossible), the missing License cell on false-positive-report-plugin, dokuwiki's link-text typo, missing CI badges on body-decompress-plugin and traffic-observation-plugin despite having integration tests, and documents the undocumented 9,515,000-9,515,999 hole as a reserved range. Also corrects vimbadmin-crs-plugin's license, which the table listed as GPL 2.0 but is actually Apache-2.0. Refs #21 Co-Authored-By: Claude Sonnet 5 --- .github/workflows/validate-registry.yml | 43 ++ .gitignore | 1 + README.md | 75 ++-- docs/plugin-descriptor-schema.md | 3 +- docs/registry-schema.md | 65 +++ registry-schema.json | 134 +++++++ registry.json | 370 ++++++++++++++++++ registry.yaml | 250 ++++++++++++ scripts/generate_registry.py | 127 ++++++ .../invalid-semantic/duplicate-name.yaml | 14 + .../invalid-semantic/overlapping-ranges.yaml | 14 + .../invalid/bad-license-not-spdx-shaped.yaml | 8 + .../invalid/bad-range-out-of-bounds.yaml | 8 + .../invalid/bad-repository-not-github.yaml | 8 + .../registry/invalid/bad-status-unknown.yaml | 8 + tests/registry/test_validate.py | 45 +++ tests/registry/valid/minimal.yaml | 12 + 17 files changed, 1151 insertions(+), 34 deletions(-) create mode 100644 .github/workflows/validate-registry.yml create mode 100644 .gitignore create mode 100644 docs/registry-schema.md create mode 100644 registry-schema.json create mode 100644 registry.json create mode 100644 registry.yaml create mode 100755 scripts/generate_registry.py create mode 100644 tests/registry/invalid-semantic/duplicate-name.yaml create mode 100644 tests/registry/invalid-semantic/overlapping-ranges.yaml create mode 100644 tests/registry/invalid/bad-license-not-spdx-shaped.yaml create mode 100644 tests/registry/invalid/bad-range-out-of-bounds.yaml create mode 100644 tests/registry/invalid/bad-repository-not-github.yaml create mode 100644 tests/registry/invalid/bad-status-unknown.yaml create mode 100755 tests/registry/test_validate.py create mode 100644 tests/registry/valid/minimal.yaml diff --git a/.github/workflows/validate-registry.yml b/.github/workflows/validate-registry.yml new file mode 100644 index 0000000..0ff899a --- /dev/null +++ b/.github/workflows/validate-registry.yml @@ -0,0 +1,43 @@ +name: Validate registry + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + + - name: registry.yaml matches the schema + run: | + uvx check-jsonschema==0.38.0 --schemafile registry-schema.json --force-filetype yaml \ + registry.yaml tests/registry/valid/*.yaml + + - name: Invalid registry fixtures are rejected + run: | + status=0 + for f in tests/registry/invalid/*.yaml; do + if uvx check-jsonschema==0.38.0 --schemafile registry-schema.json --force-filetype yaml "$f" >/dev/null 2>&1; then + echo "::error file=$f::expected schema validation to fail, but it passed" + status=1 + fi + done + exit "$status" + + - name: Semantic validation (uniqueness, overlaps) + run: uv run tests/registry/test_validate.py + + - name: README.md and registry.json are up to date with registry.yaml + run: | + uv run scripts/generate_registry.py + git diff --exit-code -- README.md registry.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/README.md b/README.md index 34c1dab..e21bd05 100644 --- a/README.md +++ b/README.md @@ -9,35 +9,46 @@ The rule ID range from 9,500,000 - 9,999,999 is reserved for CRS plugins. Plugins usually get a range of 1,000 IDs with the notable exception of the incubator plugin that maps the regular CRS IDs from 900K for each rule to the range 9,900,000 - 9,999,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 | -| 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 | | 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/dokuwiki-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | Apache 2.0 | -| dokuwiki-rule-exclusions | 9,509,000 - 9,509,999 | [coreruleset/dokuwki-rule-exclusions-plugin](https://github.com/coreruleset/dokuwiki-rule-exclusions-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/nextcloud-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 | -| 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 | | Apache 2.0 | -| performance-plugin | 9,517,000 - 9,517,999 | [coreruleset/performance-plugin](https://github.com/coreruleset/performance-plugin) | official | draft (Private) | | 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) | | 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 | ![Integrations tests](https://github.com/eilandert/wordpress-hardening-plugin/actions/workflows/integration.yml/badge.svg) | 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) | -| traffic-observation-plugin | 9,526,000 - 9,526,999 | [coreruleset/traffic-observation-plugin](https://github.com/coreruleset/traffic-observation-plugin) | official | untested | | 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) | GPL 2.0 | -| incubator | 9,900,000 - 9,999,999 | [coreruleset/incubator-plugin](https://github.com/coreruleset/incubator-plugin) | official | - | | Apache 2.0 | + +| *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 | +| 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 | +| 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 | +| 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 | +| 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 | +| incubator | 9,900,000 - 9,999,999 | [coreruleset/incubator-plugin](https://github.com/coreruleset/incubator-plugin) | official | being tested | | Apache-2.0 | + + +This table is generated from [`registry.yaml`](registry.yaml); do not edit it by hand. +A machine-readable [`registry.json`](registry.json) is published alongside it for tooling +that wants to discover and install plugins without parsing this file. +See [`docs/registry-schema.md`](docs/registry-schema.md) for what the registry attests +(and what it does not), and [`docs/plugin-descriptor-schema.md`](docs/plugin-descriptor-schema.md) +for the `plugin.yaml` descriptor that each plugin repository provides. + +To register a new plugin, open a PR adding it to `registry.yaml`. diff --git a/docs/plugin-descriptor-schema.md b/docs/plugin-descriptor-schema.md index b3999a7..ad09828 100644 --- a/docs/plugin-descriptor-schema.md +++ b/docs/plugin-descriptor-schema.md @@ -4,13 +4,12 @@ The plugin descriptor schema defines a `plugin.yaml` file that lives in the root of each CRS plugin repository. The file must be named `plugin.yaml` and must sit at the repository root: tooling locates it by appending that path to the `repository` URL, so any other name or location makes the plugin undiscoverable. It provides machine-readable metadata about the plugin, its configuration variables, and compatibility requirements. -The plugin registry aggregates these descriptors to generate the registry table, and downstream tooling (such as a CRS configurator) can parse them to build preconfigured CRS deployments based on plugin selection. +The registry table itself is generated from [`registry.yaml`](../registry.yaml), not from these descriptors — see [`registry-schema.md`](registry-schema.md) for why the two files have different owners. Downstream tooling (such as a CRS configurator) parses `plugin.yaml` to build preconfigured CRS deployments based on plugin selection. ## Goals - Allow each plugin repository to be the single source of truth for its own metadata. - Enable automated tooling to discover, validate, and configure plugins. -- Replace manual registry table maintenance with generated output. - Provide enough information for a configurator to present a UI for plugin selection and variable tuning. ## Schema Structure diff --git a/docs/registry-schema.md b/docs/registry-schema.md new file mode 100644 index 0000000..a42dd79 --- /dev/null +++ b/docs/registry-schema.md @@ -0,0 +1,65 @@ +# CRS Plugin Registry Schema + +## Overview + +`registry.yaml`, at the root of this repository, is the authoritative index of registered CRS +plugins. It is defined by [JSON Schema (2020-12)](https://json-schema.org/draft/2020-12/json-schema-core) +in [`registry-schema.json`](../registry-schema.json). [`README.md`](../README.md)'s table and +[`registry.json`](../registry.json) are both generated from it by +[`scripts/generate_registry.py`](../scripts/generate_registry.py); do not edit either by hand. + +This is a separate file with a separate owner from the per-plugin +[`plugin.yaml`](plugin-descriptor-schema.md) descriptor: + +- **`registry.yaml`, in this repo** — authoritative for rule ID range allocation and for the + vetting signal (`type`, `status`). Changed only through reviewed PRs. +- **`plugin.yaml`, in each plugin repository** — authoritative for the plugin's description and + its configuration variables. + +The index stays in this repo rather than being assembled by fetching every plugin repository's +`plugin.yaml`, for three reasons: rule ID ranges have to be allocated centrally to prevent +collisions between plugins that don't know about each other; some registered plugin repositories +are private and cannot be fetched at all; and a plugin repository declaring itself +`status: tested` in its own descriptor attests nothing, while the same field here, set through +review, does. + +## What the registry attests, and what it does not + +Registering a plugin here means a rule ID range is allocated to it and that CRS maintainers +reviewed the registration (the `type` and `status` fields). That is the extent of it. + +**The registry is not a code audit and not a supply-chain guarantee.** Registration does not mean +the plugin's code was reviewed line by line, that its releases are signed, or that its dependencies +were checked. `status: tested` means integration tests exist and pass, not that the plugin is free +of bugs or safe against a malicious release. Installers should resolve to a release tag and record +what they installed; signing or checksums may attach to release tags in a later iteration, but are +out of scope today. + +## Fields + +Each entry under `plugins` has: + +| Field | Required | Description | +|-----------------|----------|-------------| +| `name` | yes | Plugin name as shown in the registry. | +| `rule_id_range` | yes | `{start, end}` object; both within 9,500,000 - 9,999,999, and not overlapping any other plugin's range or a `reserved` range. | +| `repository` | yes | GitHub repository URL. The CI badge and the repository link are both derived from this field, so a swapped or mistyped badge is no longer possible. | +| `type` | yes | `official` (coreruleset-maintained) or `3rd-party`. | +| `status` | yes | `tested`, `being-tested`, `untested`, or `draft` — the same maturity scale as `plugin.yaml`, but attested here through review rather than self-declared. | +| `ci` | no | Whether the repository has a `.github/workflows/integration.yml` workflow. Defaults to `false`; drives whether the generated CI badge is shown. | +| `private` | no | Whether the repository is private. Defaults to `false`; shown as `(Private)` next to the status. | +| `license` | yes | SPDX license identifier (e.g., `Apache-2.0`, `GPL-2.0`). | + +Top-level `reserved` documents rule ID ranges that are intentionally not allocated, each with a +`rule_id_range` and a `note` explaining why (for example, a range vacated by a retired plugin) so +the hole isn't mistaken for a bug and silently reallocated. + +## Validation + +`registry-schema.json` covers structure: required fields, enums, the rule ID bounds, and the +GitHub repository URL shape. Two constraints it cannot express — unique plugin names, and no +overlapping rule ID ranges across plugins and reserved ranges — are checked separately by +[`scripts/generate_registry.py`](../scripts/generate_registry.py), which fails the same way schema +validation does when it finds a violation. CI runs both, then regenerates `README.md` and +`registry.json` and fails on any drift (`git diff --exit-code`), so the generated files can never +go stale relative to `registry.yaml`. diff --git a/registry-schema.json b/registry-schema.json new file mode 100644 index 0000000..1517f0b --- /dev/null +++ b/registry-schema.json @@ -0,0 +1,134 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/coreruleset/plugin-registry/main/registry-schema.json", + "title": "CRS Plugin Registry", + "description": "Schema for the machine-readable plugin registry (registry.yaml)", + "type": "object", + "required": [ + "schema_version", + "plugins" + ], + "additionalProperties": false, + "properties": { + "schema_version": { + "type": "integer", + "const": 1, + "description": "Version of the registry schema" + }, + "plugins": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/plugin" + } + }, + "reserved": { + "type": "array", + "description": "Rule ID ranges that are intentionally not allocated to any plugin, with the reason", + "items": { + "$ref": "#/$defs/reserved_range" + } + } + }, + "$defs": { + "plugin": { + "type": "object", + "required": [ + "name", + "rule_id_range", + "repository", + "type", + "status", + "license" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", + "description": "Plugin name as shown in the registry" + }, + "rule_id_range": { + "$ref": "#/$defs/rule_id_range" + }, + "repository": { + "type": "string", + "pattern": "^https://github\\.com/[^/]+/[^/]+/?$", + "description": "URL of the plugin source repository (currently only GitHub repositories are supported)" + }, + "type": { + "type": "string", + "enum": [ + "official", + "3rd-party" + ], + "description": "Whether the plugin is maintained by the CRS team or a third party" + }, + "status": { + "type": "string", + "enum": [ + "tested", + "being-tested", + "untested", + "draft" + ], + "description": "Maturity level, attested by CRS maintainers through review of this file" + }, + "ci": { + "type": "boolean", + "default": false, + "description": "Whether the repository has a .github/workflows/integration.yml workflow; used to derive the CI badge" + }, + "private": { + "type": "boolean", + "default": false, + "description": "Whether the repository is private" + }, + "license": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9.+-]*$", + "description": "SPDX license identifier (e.g., Apache-2.0, GPL-2.0)" + } + } + }, + "rule_id_range": { + "type": "object", + "required": [ + "start", + "end" + ], + "additionalProperties": false, + "properties": { + "start": { + "type": "integer", + "minimum": 9500000, + "maximum": 9999999, + "description": "First rule ID in the allocated range" + }, + "end": { + "type": "integer", + "minimum": 9500000, + "maximum": 9999999, + "description": "Last rule ID in the allocated range" + } + } + }, + "reserved_range": { + "type": "object", + "required": [ + "rule_id_range", + "note" + ], + "additionalProperties": false, + "properties": { + "rule_id_range": { + "$ref": "#/$defs/rule_id_range" + }, + "note": { + "type": "string", + "description": "Why this range is not allocated" + } + } + } + } +} diff --git a/registry.json b/registry.json new file mode 100644 index 0000000..c2db8f1 --- /dev/null +++ b/registry.json @@ -0,0 +1,370 @@ +{ + "plugins": [ + { + "ci": true, + "license": "Apache-2.0", + "name": "template", + "repository": "https://github.com/coreruleset/template-plugin", + "rule_id_range": { + "end": 9500999, + "start": 9500000 + }, + "status": "tested", + "type": "official" + }, + { + "license": "Apache-2.0", + "name": "auto-decoding", + "repository": "https://github.com/coreruleset/auto-decoding-plugin", + "rule_id_range": { + "end": 9501999, + "start": 9501000 + }, + "status": "untested", + "type": "official" + }, + { + "license": "Apache-2.0", + "name": "antivirus", + "repository": "https://github.com/coreruleset/antivirus-plugin", + "rule_id_range": { + "end": 9502999, + "start": 9502000 + }, + "status": "being-tested", + "type": "official" + }, + { + "ci": true, + "license": "Apache-2.0", + "name": "body-decompress", + "repository": "https://github.com/coreruleset/body-decompress-plugin", + "rule_id_range": { + "end": 9503999, + "start": 9503000 + }, + "status": "being-tested", + "type": "official" + }, + { + "ci": true, + "license": "Apache-2.0", + "name": "fake-bot", + "repository": "https://github.com/coreruleset/fake-bot-plugin", + "rule_id_range": { + "end": 9504999, + "start": 9504000 + }, + "status": "tested", + "type": "official" + }, + { + "ci": true, + "license": "Apache-2.0", + "name": "google-oauth2", + "repository": "https://github.com/coreruleset/google-oauth2-plugin", + "rule_id_range": { + "end": 9505999, + "start": 9505000 + }, + "status": "tested", + "type": "official" + }, + { + "ci": true, + "license": "Apache-2.0", + "name": "drupal-rule-exclusions", + "repository": "https://github.com/coreruleset/drupal-rule-exclusions-plugin", + "rule_id_range": { + "end": 9506999, + "start": 9506000 + }, + "status": "tested", + "type": "official" + }, + { + "ci": true, + "license": "Apache-2.0", + "name": "wordpress-rule-exclusions", + "repository": "https://github.com/coreruleset/wordpress-rule-exclusions-plugin", + "rule_id_range": { + "end": 9507999, + "start": 9507000 + }, + "status": "tested", + "type": "official" + }, + { + "ci": true, + "license": "Apache-2.0", + "name": "nextcloud-rule-exclusions", + "repository": "https://github.com/coreruleset/nextcloud-rule-exclusions-plugin", + "rule_id_range": { + "end": 9508999, + "start": 9508000 + }, + "status": "tested", + "type": "official" + }, + { + "ci": true, + "license": "Apache-2.0", + "name": "dokuwiki-rule-exclusions", + "repository": "https://github.com/coreruleset/dokuwiki-rule-exclusions-plugin", + "rule_id_range": { + "end": 9509999, + "start": 9509000 + }, + "status": "tested", + "type": "official" + }, + { + "ci": true, + "license": "Apache-2.0", + "name": "cpanel-rule-exclusions", + "repository": "https://github.com/coreruleset/cpanel-rule-exclusions-plugin", + "rule_id_range": { + "end": 9510999, + "start": 9510000 + }, + "status": "tested", + "type": "official" + }, + { + "ci": true, + "license": "Apache-2.0", + "name": "xenforo-rule-exclusions", + "repository": "https://github.com/coreruleset/xenforo-rule-exclusions-plugin", + "rule_id_range": { + "end": 9511999, + "start": 9511000 + }, + "status": "tested", + "type": "official" + }, + { + "ci": true, + "license": "Apache-2.0", + "name": "phpbb-rule-exclusions", + "repository": "https://github.com/coreruleset/phpbb-rule-exclusions-plugin", + "rule_id_range": { + "end": 9512999, + "start": 9512000 + }, + "status": "tested", + "type": "official" + }, + { + "ci": true, + "license": "Apache-2.0", + "name": "phpmyadmin-rule-exclusions", + "repository": "https://github.com/coreruleset/phpmyadmin-rule-exclusions-plugin", + "rule_id_range": { + "end": 9513999, + "start": 9513000 + }, + "status": "tested", + "type": "official" + }, + { + "license": "Apache-2.0", + "name": "dos-protection-modsecurity", + "repository": "https://github.com/coreruleset/dos-protection-plugin-modsecurity", + "rule_id_range": { + "end": 9514999, + "start": 9514000 + }, + "status": "untested", + "type": "official" + }, + { + "ci": true, + "license": "Apache-2.0", + "name": "machine-learning-integration-plugin", + "repository": "https://github.com/coreruleset/machine-learning-integration-plugin", + "rule_id_range": { + "end": 9516999, + "start": 9516000 + }, + "status": "draft", + "type": "official" + }, + { + "ci": true, + "license": "Apache-2.0", + "name": "performance-plugin", + "private": true, + "repository": "https://github.com/coreruleset/performance-plugin", + "rule_id_range": { + "end": 9517999, + "start": 9517000 + }, + "status": "draft", + "type": "official" + }, + { + "ci": true, + "license": "Apache-2.0", + "name": "ghost-rule-exclusions", + "private": true, + "repository": "https://github.com/coreruleset/ghost-rule-exclusions-plugin", + "rule_id_range": { + "end": 9518999, + "start": 9518000 + }, + "status": "draft", + "type": "official" + }, + { + "ci": true, + "license": "GPL-2.0", + "name": "roundcube-rule-exclusions-plugin", + "repository": "https://github.com/EsadCetiner/roundcube-rule-exclusions-plugin", + "rule_id_range": { + "end": 9519999, + "start": 9519000 + }, + "status": "tested", + "type": "3rd-party" + }, + { + "ci": true, + "license": "GPL-2.0", + "name": "sogo-rule-exclusions-plugin", + "repository": "https://github.com/EsadCetiner/sogo-rule-exclusions-plugin", + "rule_id_range": { + "end": 9520999, + "start": 9520000 + }, + "status": "tested", + "type": "3rd-party" + }, + { + "ci": true, + "license": "GPL-2.0", + "name": "iredadmin-rule-exclusions-plugin", + "repository": "https://github.com/EsadCetiner/iredadmin-rule-exclusions-plugin", + "rule_id_range": { + "end": 9521999, + "start": 9521000 + }, + "status": "tested", + "type": "3rd-party" + }, + { + "ci": true, + "license": "Apache-2.0", + "name": "wordpress-hardening-plugin", + "repository": "https://github.com/eilandert/wordpress-hardening-plugin", + "rule_id_range": { + "end": 9522999, + "start": 9522000 + }, + "status": "tested", + "type": "3rd-party" + }, + { + "license": "Apache-2.0", + "name": "database-logging-plugin", + "repository": "https://github.com/coreruleset/database-logging-plugin", + "rule_id_range": { + "end": 9523999, + "start": 9523000 + }, + "status": "untested", + "type": "official" + }, + { + "ci": true, + "license": "Apache-2.0", + "name": "referer-hardening-plugin", + "repository": "https://github.com/coreruleset/referer-hardening-plugin", + "rule_id_range": { + "end": 9524999, + "start": 9524000 + }, + "status": "tested", + "type": "official" + }, + { + "ci": true, + "license": "Apache-2.0", + "name": "false-positive-report-plugin", + "repository": "https://github.com/coreruleset/false-positive-report-plugin", + "rule_id_range": { + "end": 9525999, + "start": 9525000 + }, + "status": "tested", + "type": "official" + }, + { + "ci": true, + "license": "Apache-2.0", + "name": "traffic-observation-plugin", + "repository": "https://github.com/coreruleset/traffic-observation-plugin", + "rule_id_range": { + "end": 9526999, + "start": 9526000 + }, + "status": "untested", + "type": "official" + }, + { + "license": "GPL-3.0", + "name": "netnea-crs-upgrading-plugin", + "repository": "https://github.com/netnea/netnea-crs-upgrading-plugin", + "rule_id_range": { + "end": 9527999, + "start": 9527000 + }, + "status": "untested", + "type": "3rd-party" + }, + { + "ci": true, + "license": "GPL-2.0", + "name": "plausible-rule-exclusions-plugin", + "repository": "https://github.com/EsadCetiner/plausible-rule-exclusions-plugin", + "rule_id_range": { + "end": 9528999, + "start": 9528000 + }, + "status": "tested", + "type": "3rd-party" + }, + { + "ci": true, + "license": "Apache-2.0", + "name": "vimbadmin-crs-plugin", + "repository": "https://github.com/eilandert/vimbadmin-crs-plugin", + "rule_id_range": { + "end": 9529999, + "start": 9529000 + }, + "status": "tested", + "type": "3rd-party" + }, + { + "license": "Apache-2.0", + "name": "incubator", + "repository": "https://github.com/coreruleset/incubator-plugin", + "rule_id_range": { + "end": 9999999, + "start": 9900000 + }, + "status": "being-tested", + "type": "official" + } + ], + "reserved": [ + { + "note": "Vacated by the retired ModSecurity v3 dos-protection plugin (see issue #12); not yet reallocated.", + "rule_id_range": { + "end": 9515999, + "start": 9515000 + } + } + ], + "schema_version": 1 +} diff --git a/registry.yaml b/registry.yaml new file mode 100644 index 0000000..7febebf --- /dev/null +++ b/registry.yaml @@ -0,0 +1,250 @@ +# OWASP CRS Plugin Registry +# Authoritative list of registered plugins: rule ID range allocation and the +# vetting signal (type, status). Changed only through reviewed PRs. +# See registry-schema.json for the full JSON Schema definition and +# docs/registry-schema.md for what this file attests. + +schema_version: 1 + +plugins: + - name: template + rule_id_range: {start: 9500000, end: 9500999} + repository: https://github.com/coreruleset/template-plugin + type: official + status: tested + ci: true + license: Apache-2.0 + + - name: auto-decoding + rule_id_range: {start: 9501000, end: 9501999} + repository: https://github.com/coreruleset/auto-decoding-plugin + type: official + status: untested + license: Apache-2.0 + + - name: antivirus + rule_id_range: {start: 9502000, end: 9502999} + repository: https://github.com/coreruleset/antivirus-plugin + type: official + status: being-tested + license: Apache-2.0 + + - name: body-decompress + rule_id_range: {start: 9503000, end: 9503999} + repository: https://github.com/coreruleset/body-decompress-plugin + type: official + status: being-tested + ci: true + license: Apache-2.0 + + - name: fake-bot + rule_id_range: {start: 9504000, end: 9504999} + repository: https://github.com/coreruleset/fake-bot-plugin + type: official + status: tested + ci: true + license: Apache-2.0 + + - name: google-oauth2 + rule_id_range: {start: 9505000, end: 9505999} + repository: https://github.com/coreruleset/google-oauth2-plugin + type: official + status: tested + ci: true + license: Apache-2.0 + + - name: drupal-rule-exclusions + rule_id_range: {start: 9506000, end: 9506999} + repository: https://github.com/coreruleset/drupal-rule-exclusions-plugin + type: official + status: tested + ci: true + license: Apache-2.0 + + - name: wordpress-rule-exclusions + rule_id_range: {start: 9507000, end: 9507999} + repository: https://github.com/coreruleset/wordpress-rule-exclusions-plugin + type: official + status: tested + ci: true + license: Apache-2.0 + + - name: nextcloud-rule-exclusions + rule_id_range: {start: 9508000, end: 9508999} + repository: https://github.com/coreruleset/nextcloud-rule-exclusions-plugin + type: official + status: tested + ci: true + license: Apache-2.0 + + - name: dokuwiki-rule-exclusions + rule_id_range: {start: 9509000, end: 9509999} + repository: https://github.com/coreruleset/dokuwiki-rule-exclusions-plugin + type: official + status: tested + ci: true + license: Apache-2.0 + + - name: cpanel-rule-exclusions + rule_id_range: {start: 9510000, end: 9510999} + repository: https://github.com/coreruleset/cpanel-rule-exclusions-plugin + type: official + status: tested + ci: true + license: Apache-2.0 + + - name: xenforo-rule-exclusions + rule_id_range: {start: 9511000, end: 9511999} + repository: https://github.com/coreruleset/xenforo-rule-exclusions-plugin + type: official + status: tested + ci: true + license: Apache-2.0 + + - name: phpbb-rule-exclusions + rule_id_range: {start: 9512000, end: 9512999} + repository: https://github.com/coreruleset/phpbb-rule-exclusions-plugin + type: official + status: tested + ci: true + license: Apache-2.0 + + - name: phpmyadmin-rule-exclusions + rule_id_range: {start: 9513000, end: 9513999} + repository: https://github.com/coreruleset/phpmyadmin-rule-exclusions-plugin + type: official + status: tested + ci: true + license: Apache-2.0 + + - name: dos-protection-modsecurity + rule_id_range: {start: 9514000, end: 9514999} + repository: https://github.com/coreruleset/dos-protection-plugin-modsecurity + type: official + status: untested + license: Apache-2.0 + + - name: machine-learning-integration-plugin + rule_id_range: {start: 9516000, end: 9516999} + repository: https://github.com/coreruleset/machine-learning-integration-plugin + type: official + status: draft + ci: true + license: Apache-2.0 + + - name: performance-plugin + rule_id_range: {start: 9517000, end: 9517999} + repository: https://github.com/coreruleset/performance-plugin + type: official + status: draft + ci: true + private: true + license: Apache-2.0 + + - name: ghost-rule-exclusions + rule_id_range: {start: 9518000, end: 9518999} + repository: https://github.com/coreruleset/ghost-rule-exclusions-plugin + type: official + status: draft + ci: true + private: true + license: Apache-2.0 + + - name: roundcube-rule-exclusions-plugin + rule_id_range: {start: 9519000, end: 9519999} + repository: https://github.com/EsadCetiner/roundcube-rule-exclusions-plugin + type: 3rd-party + status: tested + ci: true + license: GPL-2.0 + + - name: sogo-rule-exclusions-plugin + rule_id_range: {start: 9520000, end: 9520999} + repository: https://github.com/EsadCetiner/sogo-rule-exclusions-plugin + type: 3rd-party + status: tested + ci: true + license: GPL-2.0 + + - name: iredadmin-rule-exclusions-plugin + rule_id_range: {start: 9521000, end: 9521999} + repository: https://github.com/EsadCetiner/iredadmin-rule-exclusions-plugin + type: 3rd-party + status: tested + ci: true + license: GPL-2.0 + + - name: wordpress-hardening-plugin + rule_id_range: {start: 9522000, end: 9522999} + repository: https://github.com/eilandert/wordpress-hardening-plugin + type: 3rd-party + status: tested + ci: true + license: Apache-2.0 + + - name: database-logging-plugin + rule_id_range: {start: 9523000, end: 9523999} + repository: https://github.com/coreruleset/database-logging-plugin + type: official + status: untested + license: Apache-2.0 + + - name: referer-hardening-plugin + rule_id_range: {start: 9524000, end: 9524999} + repository: https://github.com/coreruleset/referer-hardening-plugin + type: official + status: tested + ci: true + license: Apache-2.0 + + - name: false-positive-report-plugin + rule_id_range: {start: 9525000, end: 9525999} + repository: https://github.com/coreruleset/false-positive-report-plugin + type: official + status: tested + ci: true + license: Apache-2.0 + + - name: traffic-observation-plugin + rule_id_range: {start: 9526000, end: 9526999} + repository: https://github.com/coreruleset/traffic-observation-plugin + type: official + status: untested + ci: true + license: Apache-2.0 + + - name: netnea-crs-upgrading-plugin + rule_id_range: {start: 9527000, end: 9527999} + repository: https://github.com/netnea/netnea-crs-upgrading-plugin + type: 3rd-party + status: untested + license: GPL-3.0 + + - name: plausible-rule-exclusions-plugin + rule_id_range: {start: 9528000, end: 9528999} + repository: https://github.com/EsadCetiner/plausible-rule-exclusions-plugin + type: 3rd-party + status: tested + ci: true + license: GPL-2.0 + + - name: vimbadmin-crs-plugin + rule_id_range: {start: 9529000, end: 9529999} + repository: https://github.com/eilandert/vimbadmin-crs-plugin + type: 3rd-party + status: tested + ci: true + license: Apache-2.0 + + - name: incubator + rule_id_range: {start: 9900000, end: 9999999} + repository: https://github.com/coreruleset/incubator-plugin + type: official + status: being-tested + license: Apache-2.0 + +reserved: + - rule_id_range: {start: 9515000, end: 9515999} + note: >- + Vacated by the retired ModSecurity v3 dos-protection plugin + (see issue #12); not yet reallocated. diff --git a/scripts/generate_registry.py b/scripts/generate_registry.py new file mode 100755 index 0000000..bc2ae20 --- /dev/null +++ b/scripts/generate_registry.py @@ -0,0 +1,127 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.11" +# dependencies = ["pyyaml==6.0.3"] +# /// +"""Generate README.md's registry table and registry.json from registry.yaml. + +Fails with a non-zero exit code if registry.yaml contains duplicate plugin +names or overlapping rule ID ranges (checks that JSON Schema cannot express). +""" + +import json +import re +import sys +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parent.parent +REGISTRY_YAML = ROOT / "registry.yaml" +README = ROOT / "README.md" +REGISTRY_JSON = ROOT / "registry.json" + +BEGIN_MARKER = "" +END_MARKER = "" + +TABLE_HEADER = ( + "| *Plugin Name* | *Rule ID Range* | *Repository* | *Type* | *Status* | *CI* | *License* |\n" + "|---|---|---|---|---|---|---|" +) + + +def format_range(rule_id_range: dict) -> str: + return f"{rule_id_range['start']:,} - {rule_id_range['end']:,}" + + +def format_repository(repository: str) -> str: + slug = repository.removeprefix("https://github.com/").rstrip("/") + return f"[{slug}]({repository})" + + +def format_status(plugin: dict) -> str: + status = plugin["status"] + text = "✅ tested" if status == "tested" else status.replace("-", " ") + if plugin.get("private"): + text += " (Private)" + return text + + +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)" + + +def render_row(plugin: dict) -> str: + cells = [ + plugin["name"], + format_range(plugin["rule_id_range"]), + format_repository(plugin["repository"]), + plugin["type"], + format_status(plugin), + format_ci(plugin), + plugin["license"], + ] + return "| " + " | ".join(cells) + " |" + + +def check_no_overlaps(ranges: list[tuple[str, int, int]]) -> list[str]: + errors = [] + for label, start, end in ranges: + if start > end: + errors.append(f"{label}: start ({start}) is after end ({end})") + by_start = sorted(ranges, key=lambda r: r[1]) + for (label_a, _, end_a), (label_b, start_b, _) in zip(by_start, by_start[1:]): + if start_b <= end_a: + errors.append(f"{label_a} and {label_b} have overlapping rule ID ranges") + return errors + + +def validate(registry: dict) -> list[str]: + errors = [] + + names = [p["name"] for p in registry["plugins"]] + seen = set() + for name in names: + if name in seen: + errors.append(f"duplicate plugin name: {name}") + seen.add(name) + + ranges = [(p["name"], p["rule_id_range"]["start"], p["rule_id_range"]["end"]) for p in registry["plugins"]] + ranges += [ + (f"reserved({r['rule_id_range']['start']}-{r['rule_id_range']['end']})", r["rule_id_range"]["start"], r["rule_id_range"]["end"]) + for r in registry.get("reserved", []) + ] + errors += check_no_overlaps(ranges) + + return errors + + +def render_readme(registry: dict) -> None: + readme = README.read_text() + rows = "\n".join(render_row(p) for p in registry["plugins"]) + table = f"{BEGIN_MARKER}\n{TABLE_HEADER}\n{rows}\n{END_MARKER}" + 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)) + + +def render_json(registry: dict) -> None: + REGISTRY_JSON.write_text(json.dumps(registry, indent=2, sort_keys=True) + "\n") + + +def main() -> None: + registry = yaml.safe_load(REGISTRY_YAML.read_text()) + errors = validate(registry) + if errors: + for error in errors: + print(f"::error file=registry.yaml::{error}", file=sys.stderr) + raise SystemExit(1) + render_readme(registry) + render_json(registry) + + +if __name__ == "__main__": + main() diff --git a/tests/registry/invalid-semantic/duplicate-name.yaml b/tests/registry/invalid-semantic/duplicate-name.yaml new file mode 100644 index 0000000..969d022 --- /dev/null +++ b/tests/registry/invalid-semantic/duplicate-name.yaml @@ -0,0 +1,14 @@ +schema_version: 1 +plugins: + - name: example + rule_id_range: {start: 9990000, end: 9990999} + repository: https://github.com/coreruleset/example-plugin + type: official + status: tested + license: Apache-2.0 + - name: example + rule_id_range: {start: 9991000, end: 9991999} + repository: https://github.com/coreruleset/example-two-plugin + type: official + status: tested + license: Apache-2.0 diff --git a/tests/registry/invalid-semantic/overlapping-ranges.yaml b/tests/registry/invalid-semantic/overlapping-ranges.yaml new file mode 100644 index 0000000..05beede --- /dev/null +++ b/tests/registry/invalid-semantic/overlapping-ranges.yaml @@ -0,0 +1,14 @@ +schema_version: 1 +plugins: + - name: example-a + rule_id_range: {start: 9990000, end: 9990999} + repository: https://github.com/coreruleset/example-a-plugin + type: official + status: tested + license: Apache-2.0 + - name: example-b + rule_id_range: {start: 9990500, end: 9991500} + repository: https://github.com/coreruleset/example-b-plugin + type: official + status: tested + license: Apache-2.0 diff --git a/tests/registry/invalid/bad-license-not-spdx-shaped.yaml b/tests/registry/invalid/bad-license-not-spdx-shaped.yaml new file mode 100644 index 0000000..2c6fff9 --- /dev/null +++ b/tests/registry/invalid/bad-license-not-spdx-shaped.yaml @@ -0,0 +1,8 @@ +schema_version: 1 +plugins: + - name: example + rule_id_range: {start: 9990000, end: 9990999} + repository: https://github.com/coreruleset/example-plugin + type: official + status: tested + license: "Apache 2.0" diff --git a/tests/registry/invalid/bad-range-out-of-bounds.yaml b/tests/registry/invalid/bad-range-out-of-bounds.yaml new file mode 100644 index 0000000..76687c9 --- /dev/null +++ b/tests/registry/invalid/bad-range-out-of-bounds.yaml @@ -0,0 +1,8 @@ +schema_version: 1 +plugins: + - name: example + rule_id_range: {start: 1000000, end: 1000999} + repository: https://github.com/coreruleset/example-plugin + type: official + status: tested + license: Apache-2.0 diff --git a/tests/registry/invalid/bad-repository-not-github.yaml b/tests/registry/invalid/bad-repository-not-github.yaml new file mode 100644 index 0000000..5686e44 --- /dev/null +++ b/tests/registry/invalid/bad-repository-not-github.yaml @@ -0,0 +1,8 @@ +schema_version: 1 +plugins: + - name: example + rule_id_range: {start: 9990000, end: 9990999} + repository: https://gitlab.com/coreruleset/example-plugin + type: official + status: tested + license: Apache-2.0 diff --git a/tests/registry/invalid/bad-status-unknown.yaml b/tests/registry/invalid/bad-status-unknown.yaml new file mode 100644 index 0000000..5557ef9 --- /dev/null +++ b/tests/registry/invalid/bad-status-unknown.yaml @@ -0,0 +1,8 @@ +schema_version: 1 +plugins: + - name: example + rule_id_range: {start: 9990000, end: 9990999} + repository: https://github.com/coreruleset/example-plugin + type: official + status: production-ready + license: Apache-2.0 diff --git a/tests/registry/test_validate.py b/tests/registry/test_validate.py new file mode 100755 index 0000000..604e53b --- /dev/null +++ b/tests/registry/test_validate.py @@ -0,0 +1,45 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.11" +# dependencies = ["pyyaml==6.0.3"] +# /// +"""Self-check for the semantic validation in scripts/generate_registry.py. + +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. +""" + +import importlib.util +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent.parent +spec = importlib.util.spec_from_file_location("generate_registry", ROOT / "scripts" / "generate_registry.py") +generate_registry = importlib.util.module_from_spec(spec) +spec.loader.exec_module(generate_registry) + +import yaml + + +def load(path: Path) -> dict: + return yaml.safe_load(path.read_text()) + + +def main() -> None: + registry = load(ROOT / "registry.yaml") + assert generate_registry.validate(registry) == [], "registry.yaml must be semantically valid" + + duplicate = load(ROOT / "tests/registry/invalid-semantic/duplicate-name.yaml") + errors = generate_registry.validate(duplicate) + assert any("duplicate plugin name" in e for e in errors), errors + + overlapping = load(ROOT / "tests/registry/invalid-semantic/overlapping-ranges.yaml") + errors = generate_registry.validate(overlapping) + assert any("overlapping rule ID ranges" in e for e in errors), errors + + print("all checks passed") + + +if __name__ == "__main__": + main() diff --git a/tests/registry/valid/minimal.yaml b/tests/registry/valid/minimal.yaml new file mode 100644 index 0000000..a39dbb3 --- /dev/null +++ b/tests/registry/valid/minimal.yaml @@ -0,0 +1,12 @@ +schema_version: 1 +plugins: + - name: example + rule_id_range: {start: 9990000, end: 9990999} + repository: https://github.com/coreruleset/example-plugin + type: official + status: tested + ci: true + license: Apache-2.0 +reserved: + - rule_id_range: {start: 9991000, end: 9991999} + note: "Reserved for future use"