diff --git a/docs/plugin-descriptor-schema.md b/docs/plugin-descriptor-schema.md new file mode 100644 index 0000000..38f9dae --- /dev/null +++ b/docs/plugin-descriptor-schema.md @@ -0,0 +1,186 @@ +# CRS Plugin Descriptor Schema + +## Overview + +The plugin descriptor schema defines a `plugin.yaml` file that lives in the root of each CRS plugin repository. 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. + +## 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 + +The schema is defined as JSON Schema (2020-12) in [`plugin-schema.json`](../plugin-schema.json). A `plugin.yaml` file has the following top-level sections: + +### `schema_version` + +**Required.** Integer, currently fixed at `1`. + +Allows future evolution of the schema without breaking existing parsers. Tooling should check this field and handle unknown versions gracefully. + +### `plugin` + +**Required.** Plugin identity and metadata. + +| Field | Required | Description | +|--------------------|----------|-------------| +| `name` | yes | Plugin name following the CRS convention: `-plugin`. Validated by regex. | +| `description` | yes | One-line summary of the plugin's purpose. | +| `long_description` | no | Multi-line extended description for documentation and UI display. | +| `type` | yes | `official` (coreruleset-maintained) or `3rd-party`. | +| `category` | no | Functional category: `rule-exclusion`, `detection`, `protection`, `utility`, `logging`, or `performance`. | +| `status` | yes | Maturity level: `tested`, `being-tested`, `untested`, or `draft`. | +| `license` | yes | SPDX license identifier (e.g., `Apache-2.0`, `GPL-2.0-only`). | +| `authors` | yes | List of author objects with `name` (required), `email` and `url` (optional). | +| `repository` | yes | URL of the plugin source repository. | +| `homepage` | no | URL to documentation or project site. | +| `keywords` | no | Tags for discovery and categorization. | + +### `rule_id_range` + +**Required.** The registered rule ID block from the plugin registry. + +| Field | Description | +|---------|-------------| +| `start` | First rule ID in the allocated range (integer, 9500000-9999999). | +| `end` | Last rule ID in the allocated range (integer, 9500000-9999999). | + +Plugins typically receive a 1,000-ID range. The schema validates that values fall within the CRS plugin namespace (9,500,000 - 9,999,999). + +### `compatibility` + +**Optional.** WAF engine and CRS version requirements. + +| Field | Description | +|---------------|-------------| +| `crs_version` | Version constraint string (e.g., `>=4.0.0`). | +| `engines` | List of compatible WAF engines: `modsecurity2`, `modsecurity3`, `coraza`. | + +When omitted, no compatibility constraints are assumed. Tooling should treat missing engines as "compatible with all". + +### `dependencies` + +**Optional.** List of other CRS plugins this plugin depends on. + +Each entry has: +| Field | Required | Description | +|-----------|----------|-------------| +| `name` | yes | Name of the required plugin. | +| `version` | no | Version constraint for the dependency. | + +Most plugins have no dependencies. This field exists for plugins that build on top of other plugins. + +### `configuration` + +**Required.** The key section for automated tooling. Describes the config file and all user-tunable variables. + +| Field | Description | +|-------------|-------------| +| `file` | Path to the `-config.conf` file relative to the plugin root. | +| `variables` | List of transaction variable definitions (see below). | + +#### Variable Definition + +Each entry in `variables` describes a single `tx.*` variable from the config file: + +| Field | Required | Description | +|------------------|----------|-------------| +| `name` | yes | Full ModSecurity variable name (e.g., `tx.myplugin_enabled`). | +| `type` | yes | Data type: `boolean`, `integer`, `string`, or `enum`. | +| `default` | no | Default value if not set by the user. | +| `description` | yes | Human-readable explanation of the variable. | +| `required` | no | Whether the user must explicitly set this variable (default: `false`). | +| `allowed_values` | no | List of valid values when type is `enum`. | +| `example` | no | Example value for documentation. | +| `min` | no | Minimum value when type is `integer`. | +| `max` | no | Maximum value when type is `integer`. | + +#### Variable Types + +The four types cover all patterns found across existing CRS plugins: + +- **`boolean`** — Enable/disable flags. Every plugin has at least `tx._enabled`. Values are integers: `0` (disabled) or `1` (enabled), following the ModSecurity convention for boolean flags. +- **`integer`** — Numeric thresholds and limits (e.g., `tx.body-decompress-plugin_max_data_size_bytes`). Supports `min`/`max` constraints. +- **`string`** — Freeform text values (e.g., `tx.google-oauth2-plugin_whitelisted_parameters`). The `example` field helps users understand the expected format. +- **`enum`** — Constrained choices (e.g., `tx.phpmyadmin-rule-exclusions-plugin_url_format` with values `v51`, `v52`). Must include `allowed_values`. + +### `files` + +**Optional.** Maps the standard plugin files for tooling convenience. + +| Field | Description | +|----------|-------------| +| `config` | Path to the configuration file. | +| `before` | Path to the before-CRS rules file. | +| `after` | Path to the after-CRS rules file. | + +These follow the CRS naming convention: `plugins/-config.conf`, `plugins/-before.conf`, `plugins/-after.conf`. Some plugins use a variant like `-config-before.conf`. + +## Design Decisions + +### Why YAML over JSON or TOML? + +YAML supports comments, multi-line strings, and is widely used in the CRS ecosystem (test files use FTW YAML format). Plugin authors already work with YAML for regression tests, so it is familiar. + +### Why `schema_version` as a top-level field? + +Embedding versioning in the schema itself allows parsers to detect incompatible changes before attempting to parse. This is simpler than relying on file naming or external version tracking. + +### Why typed variables with constraints? + +A configurator project needs to know more than just variable names and defaults. By declaring types, allowed values, and numeric bounds, tooling can: + +1. Render appropriate input widgets (toggle for boolean, slider for bounded integer, dropdown for enum). +2. Validate user input before generating configuration. +3. Generate documentation automatically. + +### Why is `configuration` required? + +Every CRS plugin has at least one configuration variable (`tx._enabled`). Making this section required ensures consistency and guarantees that tooling always has something to work with, even for minimal rule-exclusion plugins. + +### Why no `version` field? + +The plugin version is derived from GitHub release tags at query time. Embedding a version in `plugin.yaml` would inevitably drift because developers forget to bump it. Tooling should fetch the latest release tag from the repository URL instead. This keeps `plugin.yaml` as a static descriptor that rarely needs updating. + +### Why separate `rule_id_range` from `plugin`? + +The rule ID range is a registry-level concern (namespace coordination), not a plugin identity attribute. Keeping it separate makes it clear that this range is allocated by the registry and should not be changed unilaterally. + +### Category taxonomy + +The six categories cover the existing plugin landscape: + +| Category | Examples | +|------------------|----------| +| `rule-exclusion` | wordpress, drupal, nextcloud, phpbb, cpanel | +| `detection` | fake-bot | +| `protection` | dos-protection | +| `utility` | template, body-decompress, auto-decoding | +| `logging` | database-logging, false-positive-report | +| `performance` | performance-plugin | + +New categories can be added to the schema's enum as the ecosystem grows. + +## Configurator Integration + +The primary consumer of `plugin.yaml` files is a configurator tool that: + +1. **Fetches** `plugin.yaml` from each registered plugin repository. +2. **Presents** available plugins grouped by category, with descriptions and compatibility info. +3. **Collects** user choices: which plugins to enable and how to tune their variables. +4. **Validates** input against variable constraints (type, min/max, allowed_values). +5. **Resolves** dependencies between plugins. +6. **Generates** a complete CRS configuration with all selected plugins and their tuned `tx.*` variables. + +## Rollout Plan + +1. Add `plugin.yaml` to `coreruleset/template-plugin` as the reference implementation. +2. Get feedback from the CRS team and iterate on the schema. +3. Once the schema is accepted, create PRs adding `plugin.yaml` to all registered plugins. +4. Update the registry to validate incoming plugin registrations against the schema. +5. Build the configurator project. diff --git a/examples/body-decompress-plugin.yaml b/examples/body-decompress-plugin.yaml new file mode 100644 index 0000000..dc924a3 --- /dev/null +++ b/examples/body-decompress-plugin.yaml @@ -0,0 +1,55 @@ +# OWASP CRS Plugin Descriptor +# https://github.com/coreruleset/plugin-registry + +schema_version: 1 + +plugin: + name: "body-decompress-plugin" + description: "On-the-fly decompression of response bodies for CRS inspection" + long_description: | + Decompresses gzip/deflate compressed HTTP response bodies so that CRS + response rules can inspect the actual content. Without this plugin, + compressed responses bypass response body inspection rules. + type: "official" + category: "utility" + status: "being-tested" + license: "Apache-2.0" + authors: + - name: "OWASP CRS Team" + url: "https://coreruleset.org" + repository: "https://github.com/coreruleset/body-decompress-plugin" + keywords: + - "decompression" + - "response-body" + - "gzip" + +rule_id_range: + start: 9503000 + end: 9503999 + +compatibility: + crs_version: ">=4.0.0" + engines: + - "modsecurity2" + - "modsecurity3" + +configuration: + file: "plugins/body-decompress-config-before.conf" + variables: + - name: "tx.body-decompress-plugin_enabled" + type: "boolean" + default: 1 + description: "Enable or disable response body decompression" + + - name: "tx.body-decompress-plugin_max_data_size_bytes" + type: "integer" + default: 102400 + description: "Maximum decompressed data size in bytes" + min: 1024 + max: 10485760 + example: 102400 + +files: + config: "plugins/body-decompress-config-before.conf" + before: "plugins/body-decompress-before.conf" + after: "plugins/body-decompress-after.conf" diff --git a/examples/fake-bot-plugin.yaml b/examples/fake-bot-plugin.yaml new file mode 100644 index 0000000..61efae4 --- /dev/null +++ b/examples/fake-bot-plugin.yaml @@ -0,0 +1,56 @@ +# OWASP CRS Plugin Descriptor +# https://github.com/coreruleset/plugin-registry + +schema_version: 1 + +plugin: + name: "fake-bot-plugin" + description: "Detects fake search engine bots by verifying crawler IP addresses" + long_description: | + Identifies HTTP requests that claim to be from known search engine crawlers + (Googlebot, Bingbot, etc.) but originate from IP addresses not belonging to + those search engines. Helps protect against scraping and abuse from clients + impersonating legitimate bots. + type: "official" + category: "detection" + status: "tested" + license: "Apache-2.0" + authors: + - name: "OWASP CRS Team" + url: "https://coreruleset.org" + repository: "https://github.com/coreruleset/fake-bot-plugin" + homepage: "https://coreruleset.org/docs/plugins/" + keywords: + - "bot-detection" + - "fake-bot" + - "crawler" + - "security" + +rule_id_range: + start: 9504000 + end: 9504999 + +compatibility: + crs_version: ">=4.0.0" + engines: + - "modsecurity2" + - "modsecurity3" + - "coraza" + +configuration: + file: "plugins/fake-bot-config.conf" + variables: + - name: "tx.fake-bot-plugin_enabled" + type: "boolean" + default: 1 + description: "Enable or disable the fake bot detection plugin (0 to disable)" + + - name: "tx.fake-bot-plugin_whitelist_broken_apple_devices" + type: "boolean" + default: 0 + description: "Whitelist broken Apple devices that incorrectly identify as Googlebot" + +files: + config: "plugins/fake-bot-config.conf" + before: "plugins/fake-bot-before.conf" + after: "plugins/fake-bot-after.conf" diff --git a/examples/wordpress-rule-exclusions-plugin.yaml b/examples/wordpress-rule-exclusions-plugin.yaml new file mode 100644 index 0000000..5494cf1 --- /dev/null +++ b/examples/wordpress-rule-exclusions-plugin.yaml @@ -0,0 +1,49 @@ +# OWASP CRS Plugin Descriptor +# https://github.com/coreruleset/plugin-registry + +schema_version: 1 + +plugin: + name: "wordpress-rule-exclusions-plugin" + description: "CRS rule exclusions for WordPress applications" + long_description: | + Provides targeted rule exclusions to eliminate false positives when + running OWASP CRS in front of WordPress sites. Covers the WordPress + admin panel, REST API, and common plugin/theme patterns. + type: "official" + category: "rule-exclusion" + status: "tested" + license: "Apache-2.0" + authors: + - name: "OWASP CRS Team" + url: "https://coreruleset.org" + repository: "https://github.com/coreruleset/wordpress-rule-exclusions-plugin" + keywords: + - "wordpress" + - "cms" + - "rule-exclusion" + - "false-positive" + +rule_id_range: + start: 9507000 + end: 9507999 + +compatibility: + crs_version: ">=4.0.0" + engines: + - "modsecurity2" + - "modsecurity3" + - "coraza" + +configuration: + file: "plugins/wordpress-rule-exclusions-config.conf" + variables: + - name: "tx.wordpress-rule-exclusions-plugin_enabled" + type: "boolean" + default: 1 + description: "Enable or disable WordPress rule exclusions (0 to disable)" + +files: + config: "plugins/wordpress-rule-exclusions-config.conf" + before: "plugins/wordpress-rule-exclusions-before.conf" + after: "plugins/wordpress-rule-exclusions-after.conf" diff --git a/plugin-schema.json b/plugin-schema.json new file mode 100644 index 0000000..dc8c5ae --- /dev/null +++ b/plugin-schema.json @@ -0,0 +1,362 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/coreruleset/plugin-registry/main/plugin-schema.json", + "title": "CRS Plugin Descriptor", + "description": "Schema for OWASP CRS plugin metadata files (plugin.yaml)", + "type": "object", + "required": [ + "schema_version", + "plugin", + "rule_id_range", + "configuration" + ], + "additionalProperties": false, + "properties": { + "schema_version": { + "type": "integer", + "const": 1, + "description": "Version of the plugin descriptor schema" + }, + "plugin": { + "type": "object", + "required": [ + "name", + "description", + "type", + "status", + "license", + "authors", + "repository" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z0-9]+(-[a-z0-9]+)*-plugin$", + "description": "Plugin name using the CRS naming convention: -plugin" + }, + "description": { + "type": "string", + "description": "Short human-readable description of what the plugin does" + }, + "long_description": { + "type": "string", + "description": "Extended description with details about functionality and use cases" + }, + "type": { + "type": "string", + "enum": [ + "official", + "3rd-party" + ], + "description": "Whether the plugin is maintained by the CRS team or a third party" + }, + "category": { + "type": "string", + "enum": [ + "rule-exclusion", + "detection", + "protection", + "utility", + "logging", + "performance" + ], + "description": "Functional category of the plugin" + }, + "status": { + "type": "string", + "enum": [ + "tested", + "being-tested", + "untested", + "draft" + ], + "description": "Current testing/maturity status" + }, + "license": { + "type": "string", + "description": "SPDX license identifier (e.g., Apache-2.0, GPL-2.0-only)" + }, + "authors": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "name" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "url": { + "type": "string", + "format": "uri" + } + } + } + }, + "repository": { + "type": "string", + "format": "uri", + "description": "URL of the plugin source repository" + }, + "homepage": { + "type": "string", + "format": "uri", + "description": "URL of the plugin homepage or documentation site" + }, + "keywords": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tags for discovery and categorization" + } + } + }, + "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" + } + } + }, + "compatibility": { + "type": "object", + "additionalProperties": false, + "properties": { + "crs_version": { + "type": "string", + "description": "CRS version constraint (e.g., '>=4.0.0')" + }, + "engines": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "modsecurity2", + "modsecurity3", + "coraza" + ] + }, + "description": "Compatible WAF engines" + } + } + }, + "dependencies": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Name of the required plugin" + }, + "version": { + "type": "string", + "description": "Version constraint for the dependency" + } + } + }, + "description": "Other CRS plugins this plugin depends on" + }, + "configuration": { + "type": "object", + "required": [ + "file", + "variables" + ], + "additionalProperties": false, + "properties": { + "file": { + "type": "string", + "pattern": "^plugins/.+-config(-before)?\\.conf$", + "description": "Path to the configuration file relative to the plugin root" + }, + "variables": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "name", + "type", + "description" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "pattern": "^tx\\..+", + "description": "Full ModSecurity transaction variable name (e.g., tx.myplugin_enabled)" + }, + "type": { + "type": "string", + "enum": [ + "boolean", + "integer", + "string", + "enum" + ], + "description": "Data type of the variable" + }, + "default": { + "description": "Default value if not explicitly set by the user" + }, + "description": { + "type": "string", + "description": "Human-readable explanation of the variable's purpose" + }, + "required": { + "type": "boolean", + "default": false, + "description": "Whether this variable must be set for the plugin to function" + }, + "allowed_values": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of allowed values when type is 'enum'" + }, + "example": { + "description": "Example value for documentation purposes" + }, + "min": { + "type": "integer", + "description": "Minimum value when type is 'integer'" + }, + "max": { + "type": "integer", + "description": "Maximum value when type is 'integer'" + } + }, + "allOf": [ + { + "if": { + "properties": { "type": { "const": "boolean" } }, + "required": ["type"] + }, + "then": { + "properties": { + "default": { "type": "integer", "enum": [0, 1] }, + "example": { "type": "integer", "enum": [0, 1] } + } + } + }, + { + "if": { + "properties": { "type": { "const": "integer" } }, + "required": ["type"] + }, + "then": { + "properties": { + "default": { "type": "integer" }, + "example": { "type": "integer" } + } + } + }, + { + "if": { + "properties": { "type": { "const": "string" } }, + "required": ["type"] + }, + "then": { + "properties": { + "default": { "type": "string" }, + "example": { "type": "string" } + } + } + }, + { + "if": { + "properties": { "type": { "const": "enum" } }, + "required": ["type"] + }, + "then": { + "required": ["allowed_values"], + "properties": { + "default": { "type": "string" }, + "example": { "type": "string" } + } + } + }, + { + "if": { + "not": { + "properties": { "type": { "const": "integer" } }, + "required": ["type"] + } + }, + "then": { + "not": { + "anyOf": [ + { "required": ["min"] }, + { "required": ["max"] } + ] + } + } + }, + { + "if": { + "not": { + "properties": { "type": { "const": "enum" } }, + "required": ["type"] + } + }, + "then": { + "not": { + "required": ["allowed_values"] + } + } + } + ] + } + } + } + }, + "files": { + "type": "object", + "additionalProperties": false, + "properties": { + "config": { + "type": "string", + "description": "Configuration file path (relative to plugin root)" + }, + "before": { + "type": "string", + "description": "Before-CRS rules file path" + }, + "after": { + "type": "string", + "description": "After-CRS rules file path" + } + } + } + } +} diff --git a/plugin.yaml.template b/plugin.yaml.template new file mode 100644 index 0000000..1031ea4 --- /dev/null +++ b/plugin.yaml.template @@ -0,0 +1,86 @@ +# OWASP CRS Plugin Descriptor +# Copy this file as 'plugin.yaml' in your plugin repository root. +# See plugin-schema.json for the full JSON Schema definition. +# Docs: https://github.com/coreruleset/plugin-registry + +schema_version: 1 + +plugin: + # Plugin name must follow the pattern: -plugin + name: "template-plugin" + description: "Short description of what the plugin does" + long_description: | + Extended description explaining the plugin's purpose, how it works, + and when users should consider enabling it. This field supports + multi-line text. + # "official" for coreruleset-maintained, "3rd-party" otherwise + type: "official" + # Category: rule-exclusion | detection | protection | utility | logging | performance + category: "utility" + # Status: tested | being-tested | untested | draft + status: "untested" + license: "Apache-2.0" + authors: + - name: "Your Name" + # email: "you@example.com" # optional + # url: "https://example.com" # optional + repository: "https://github.com/coreruleset/template-plugin" + # homepage: "https://example.com/docs" # optional + keywords: + - "example" + - "template" + +# Registered rule ID range from the plugin registry +rule_id_range: + start: 9500000 + end: 9500999 + +# WAF engine and CRS version compatibility (optional) +compatibility: + crs_version: ">=4.0.0" + engines: + - "modsecurity2" + - "modsecurity3" + - "coraza" + +# Plugin dependencies (optional, omit if none) +# dependencies: +# - name: "other-plugin" +# version: ">=1.0.0" + +# Configuration variables exposed by the plugin +configuration: + file: "plugins/template-config.conf" + variables: + - name: "tx.template-plugin_enabled" + type: "boolean" + default: 1 + description: "Enable or disable the plugin (0 to disable)" + required: false + + # Add your plugin-specific variables below. Examples: + # + # - name: "tx.template-plugin_threshold" + # type: "integer" + # default: 100 + # description: "Detection threshold" + # min: 1 + # max: 1000 + # + # - name: "tx.template-plugin_mode" + # type: "enum" + # default: "block" + # description: "Operating mode" + # allowed_values: ["detect", "block"] + # + # - name: "tx.template-plugin_whitelist" + # type: "string" + # default: "" + # description: "Space-separated list of whitelisted values" + # example: "/value1/ /value2/" + +# Plugin rule files (optional, helps tooling locate files) +files: + config: "plugins/template-config.conf" + before: "plugins/template-before.conf" + after: "plugins/template-after.conf"