Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/validate-registry.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pycache__/
75 changes: 43 additions & 32 deletions README.md

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions docs/plugin-descriptor-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
65 changes: 65 additions & 0 deletions docs/registry-schema.md
Original file line number Diff line number Diff line change
@@ -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`.
134 changes: 134 additions & 0 deletions registry-schema.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
}
Loading
Loading