Skip to content

Add a version suitable for plugins #135

Description

@fzipi

Here's a feature analysis for adding plugin support to the linter. I went through the plugin registry, the template plugin, the writing plugins documentation, and several existing plugins (WordPress, phpMyAdmin, etc.) to identify the differences.

Plugin categories

The registry reveals three distinct plugin categories that need different linting treatment:

  1. Rule exclusion plugins (WordPress, Drupal, Nextcloud, phpMyAdmin, DokuWiki, cPanel, XenForo, phpBB, Roundcube, SOGo, iRedAdmin) — the majority. They primarily use ctl:ruleRemoveTargetById in chained rules with pass,nolog. They don't do anomaly scoring. They live almost exclusively in *-before.conf files.

  2. Functional/detection plugins (fake-bot, antivirus, dos-protection, auto-decoding, referer-hardening, traffic-observation, false-positive-report, machine-learning) — these write actual detection rules with scoring, may use Lua, may use persistent collections, and behave more like CRS core rules.

  3. Incubator plugin — special case with an oversized range (9,900,000–9,999,999) that maps CRS rule IDs. Needs its own linting profile.

Key differences between CRS core and plugins

1. Rule ID ranges

CRS core uses 900000–959999. Plugins get a registered 1,000-ID block from 9,500,000 onwards, with internal convention:

  • 9,5XX,000–9,5XX,099: Initialization
  • 9,5XX,100–9,5XX,499: Request rules
  • 9,5XX,500–9,5XX,999: Response rules

The linter would need to accept the plugin's registered range as input or read it from the registry. Note the gap at 9,515,000 (no plugin registered there) — ranges aren't contiguous.

2. File naming and structure

CRS core: REQUEST-9XXXXX-*.conf / RESPONSE-9XXXXX-*.conf
Plugins: <plugin-name>-config.conf, <plugin-name>-before.conf, <plugin-name>-after.conf

The linter's filename validation needs to switch to plugin-specific patterns.

3. ver tag format

CRS core: ver:'OWASP_CRS/4.x.x'
Plugins: ver:'<plugin-name>/X.Y.Z' (e.g., ver:'phpmyadmin-rule-exclusions-plugin/1.0.0')

4. Approved tags

CRS core uses a strict APPROVED_TAGS file. Plugins may use a subset of CRS tags or introduce their own. Some CRS-specific tags like paranoia-level/X may not apply to all plugins. The linter should either accept a plugin-specific tags file or relax tag validation.

5. TX variable initialization context

This is a critical one. The current linter checks that all TX variables are initialized before use. However, plugins inherently depend on TX variables set by CRS core (tx.inbound_anomaly_score_pl1, tx.critical_anomaly_score, tx.paranoia_level, etc.) and crs-setup.conf. Running the linter on a plugin in isolation would produce many false "uninitialized variable" warnings.

The linter needs awareness of the CRS-provided TX variable namespace, or an external allow-list of known TX variables.

Similarly, the mandatory <plugin-name>_enabled TX variable pattern should be validated as present.

6. Anomaly scoring is optional

CRS core requires anomaly scoring with setvar:'tx.inbound_anomaly_score_plX=+%{tx.critical_anomaly_score}'. Plugins are explicitly allowed to issue direct deny actions. The linter shouldn't flag disruptive actions in plugins.

7. CAPEC tags, severity, and scoring setvars

Rule exclusion plugins don't detect attacks — they suppress false positives. The linter should not enforce:

  • CAPEC tags on rules that only use ctl:ruleRemove* actions
  • severity on pass,nolog rules
  • Anomaly score setvar on exclusion rules

Functional/detection plugins should still have these on their detection rules.

8. Mandatory plugin control rules

Plugins have specific mandatory rules that CRS core doesn't:

  • The enabled/disabled check (e.g., rule 9500010 setting <plugin>_enabled TX variable)
  • The skipAfter control rule (e.g., rule 9500099 that conditionally removes plugin rules via ctl:ruleRemoveById)

The linter should validate the presence and correctness of these control rules.

9. Phase constraints and scoring interactions

The plugin architecture has specific constraints:

  • Phase 1 anomaly scoring in *-before.conf gets overwritten by CRS 901 initialization
  • Phase 2 anomaly scoring in *-after.conf happens after the blocking decision

The linter could warn about these patterns.

10. ctl:ruleRemove* as primary action

Rule exclusion plugins heavily use ctl:ruleRemoveTargetById, ctl:ruleRemoveById, etc. CRS core rarely uses these. The linter needs to properly validate these ctl directives and understand chained rule patterns (match path/cookie → apply exclusion).

11. Test coverage paths

CRS core: tests/regression/tests/
Plugins: tests/regression/<plugin-name>/

12. Checks that apply equally

These current linter checks should work the same for both:

  • Indentation/formatting
  • t:lowercase + (?i) combined usage
  • capture action when using TX:N in chained rules
  • General ModSecurity syntax validation

Summary table

Check CRS Core Plugin (Exclusion) Plugin (Functional)
Rule ID range 900000–959999 Registered 1K block Registered 1K block
File naming REQUEST-*/RESPONSE-* *-config/before/after.conf *-config/before/after.conf
ver tag OWASP_CRS/X.Y.Z <plugin>/X.Y.Z <plugin>/X.Y.Z
CAPEC tags Required (detection rules) Not applicable Required
Severity Required Not required Required
Anomaly scoring setvar Required Not required Recommended
Direct deny/block Not allowed (use anomaly) Not expected Allowed
ctl:ruleRemove* usage Rare Primary pattern Occasional
Mandatory control rules N/A Validate _enabled + skip Validate _enabled + skip
TX var init Self-contained Allow CRS-provided vars Allow CRS-provided vars
Phase/scoring warnings Standard Before-file phase warnings Phase scoring warnings
Test path tests/regression/tests/ tests/regression/<plugin>/ tests/regression/<plugin>/
Indentation/formatting Strict Same Same
t:lowercase + (?i) Flag Same Same
Capture + TX.N chain Flag Same Same

Implementation suggestion

A --profile flag (or similar) with values like core, plugin-exclusion, plugin-functional, and plugin-incubator would be the most explicit approach. Each profile activates the right combination of checks.

Alternatively, a simpler --mode plugin with auto-detection of the plugin sub-type based on file content patterns (presence of ctl:ruleRemove* as the dominant action → exclusion plugin) could reduce configuration burden for plugin authors.

The --mode plugin approach could also accept a --plugin-id-range 9507000-9507999 parameter to validate rule IDs against the registered range.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions