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:
-
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.
-
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.
-
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.
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:
Rule exclusion plugins (WordPress, Drupal, Nextcloud, phpMyAdmin, DokuWiki, cPanel, XenForo, phpBB, Roundcube, SOGo, iRedAdmin) — the majority. They primarily use
ctl:ruleRemoveTargetByIdin chained rules withpass,nolog. They don't do anomaly scoring. They live almost exclusively in*-before.conffiles.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.
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: Initialization9,5XX,100–9,5XX,499: Request rules9,5XX,500–9,5XX,999: Response rulesThe 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-*.confPlugins:
<plugin-name>-config.conf,<plugin-name>-before.conf,<plugin-name>-after.confThe linter's filename validation needs to switch to plugin-specific patterns.
3.
vertag formatCRS 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_TAGSfile. Plugins may use a subset of CRS tags or introduce their own. Some CRS-specific tags likeparanoia-level/Xmay 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.) andcrs-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>_enabledTX 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 directdenyactions. 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:
ctl:ruleRemove*actionsseverityonpass,nologrulessetvaron exclusion rulesFunctional/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:
<plugin>_enabledTX variable)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:
*-before.confgets overwritten by CRS 901 initialization*-after.confhappens after the blocking decisionThe linter could warn about these patterns.
10.
ctl:ruleRemove*as primary actionRule exclusion plugins heavily use
ctl:ruleRemoveTargetById,ctl:ruleRemoveById, etc. CRS core rarely uses these. The linter needs to properly validate thesectldirectives 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:
t:lowercase+(?i)combined usagecaptureaction when usingTX:Nin chained rulesSummary table
REQUEST-*/RESPONSE-**-config/before/after.conf*-config/before/after.confvertagOWASP_CRS/X.Y.Z<plugin>/X.Y.Z<plugin>/X.Y.Zsetvardeny/blockctl:ruleRemove*usage_enabled+ skip_enabled+ skiptests/regression/tests/tests/regression/<plugin>/tests/regression/<plugin>/t:lowercase+(?i)Implementation suggestion
A
--profileflag (or similar) with values likecore,plugin-exclusion,plugin-functional, andplugin-incubatorwould be the most explicit approach. Each profile activates the right combination of checks.Alternatively, a simpler
--mode pluginwith auto-detection of the plugin sub-type based on file content patterns (presence ofctl:ruleRemove*as the dominant action → exclusion plugin) could reduce configuration burden for plugin authors.The
--mode pluginapproach could also accept a--plugin-id-range 9507000-9507999parameter to validate rule IDs against the registered range.