|
| 1 | +# Severity Ranking MVP v1 |
| 2 | + |
| 3 | +**Context**: WP Code Check is a CLI scanner that detects 15+ WordPress performance/security antipatterns. It already ships hardcoded checks with fixed severity levels (CRITICAL, HIGH, MEDIUM, LOW). This feature allows teams to customize severity rankings per-project to reduce noise and focus on what matters to them. |
| 4 | + |
| 5 | +## Problem |
| 6 | +- **Developer A** thinks missing nonce checks are CRITICAL (security team) |
| 7 | +- **Developer B** downranks them to MEDIUM (code review overhead) |
| 8 | +- **Developer C** wants `deprecated-function` as LOW (legacy codebase) |
| 9 | + |
| 10 | +Currently: All teams get the same severity levels. No customization = noise. |
| 11 | + |
| 12 | +## The MVP Approach: Shipped Config + Local Override |
| 13 | + |
| 14 | +**Core Concept**: Ship a `/dist/config/severity-levels.json` with the project. Users copy it locally, customize it, and push to CI/CD. Factory defaults live in the file as an escape hatch. |
| 15 | + |
| 16 | +### Current State (Before MVP) |
| 17 | + |
| 18 | +The script `./dist/bin/check-performance.sh` has hardcoded severity levels: |
| 19 | + |
| 20 | +```bash |
| 21 | +# Current (in script, not configurable) |
| 22 | +text_echo "${BLUE}▸ Missing nonce validation ${RED}[HIGH]${NC}" |
| 23 | +text_echo "${BLUE}▸ REST endpoints without pagination ${RED}[CRITICAL]${NC}" |
| 24 | +text_echo "${BLUE}▸ Unbounded queries ${RED}[CRITICAL]${NC}" |
| 25 | +text_echo "${BLUE}▸ N+1 patterns ${YELLOW}[MEDIUM]${NC}" |
| 26 | +``` |
| 27 | + |
| 28 | +### MVP: Shipped Config File |
| 29 | + |
| 30 | +**Phase 1: Create `/dist/config/severity-levels.json`** |
| 31 | + |
| 32 | +```json |
| 33 | +{ |
| 34 | + "_metadata": { |
| 35 | + "version": "1.0.59", |
| 36 | + "description": "WP Code Check - Severity Level Customization", |
| 37 | + "last_updated": "2025-12-31" |
| 38 | + }, |
| 39 | + "severity_levels": { |
| 40 | + "wp-ajax-missing-nonce": { |
| 41 | + "id": "wp-ajax-missing-nonce", |
| 42 | + "level": "HIGH", |
| 43 | + "factory_default": "HIGH", |
| 44 | + "category": "security", |
| 45 | + "description": "AJAX handler missing nonce validation" |
| 46 | + }, |
| 47 | + "unbounded-rest-endpoint": { |
| 48 | + "id": "unbounded-rest-endpoint", |
| 49 | + "level": "CRITICAL", |
| 50 | + "factory_default": "CRITICAL", |
| 51 | + "category": "performance", |
| 52 | + "description": "REST endpoint without pagination/limits" |
| 53 | + }, |
| 54 | + "unbounded-query-get-posts": { |
| 55 | + "id": "unbounded-query-get-posts", |
| 56 | + "level": "CRITICAL", |
| 57 | + "factory_default": "CRITICAL", |
| 58 | + "category": "performance", |
| 59 | + "description": "WP_Query with unbounded posts_per_page" |
| 60 | + }, |
| 61 | + "n-plus-one-pattern": { |
| 62 | + "id": "n-plus-one-pattern", |
| 63 | + "level": "MEDIUM", |
| 64 | + "factory_default": "MEDIUM", |
| 65 | + "category": "performance", |
| 66 | + "description": "Potential N+1 pattern (get_post_meta in loop)" |
| 67 | + }, |
| 68 | + "deprecated-function": { |
| 69 | + "id": "deprecated-function", |
| 70 | + "level": "MEDIUM", |
| 71 | + "factory_default": "MEDIUM", |
| 72 | + "category": "maintenance", |
| 73 | + "description": "Using deprecated WordPress function" |
| 74 | + } |
| 75 | + } |
| 76 | +} |
| 77 | +``` |
| 78 | + |
| 79 | +**Phase 2: How Users Customize** |
| 80 | + |
| 81 | +```bash |
| 82 | +# Step 1: Copy shipped config to project root or ~/.wp-code-check.json |
| 83 | +cp ./dist/config/severity-levels.json ./.wp-code-check-severity.json |
| 84 | + |
| 85 | +# Step 2: Edit locally (change "level" field only) |
| 86 | +# Change deprecated-function from MEDIUM to LOW: |
| 87 | +# "deprecated-function": { |
| 88 | +# "level": "LOW", <- Edit this |
| 89 | +# "factory_default": "MEDIUM" <- Leave this for reference |
| 90 | + |
| 91 | +# Step 3: Run scanner with custom config |
| 92 | +./dist/bin/check-performance.sh --paths . --severity-config ./.wp-code-check-severity.json |
| 93 | + |
| 94 | +# Step 4: If you break it, just check factory_default in the file |
| 95 | +# Or delete the file to use shipped defaults |
| 96 | +``` |
| 97 | + |
| 98 | +**How It Works in the Script:** |
| 99 | + |
| 100 | +```bash |
| 101 | +# 1. Load shipped defaults |
| 102 | +load_severity_defaults() |
| 103 | + |
| 104 | +# 2. Load user config if it exists (overrides shipped) |
| 105 | +if [ -f "$SEVERITY_CONFIG_FILE" ]; then |
| 106 | + CUSTOM_LEVELS=$(load_custom_severity_config "$SEVERITY_CONFIG_FILE") |
| 107 | +fi |
| 108 | + |
| 109 | +# 3. Resolve final severity for display |
| 110 | +get_severity_for_check() { |
| 111 | + local check_id="$1" |
| 112 | + # Return custom > shipped |
| 113 | + echo "${CUSTOM_LEVELS[$check_id]:-${SHIPPED_LEVELS[$check_id]}}" |
| 114 | +} |
| 115 | +``` |
| 116 | + |
| 117 | +**Why This Works:** |
| 118 | +- ✅ Zero complexity (just JSON + array lookup) |
| 119 | +- ✅ Shipped by default (lives in `/dist/config/`) |
| 120 | +- ✅ Self-documenting (factory defaults in same file) |
| 121 | +- ✅ Version controllable (commit to repo for team alignment) |
| 122 | +- ✅ Low risk (users can always reference or delete file to reset) |
| 123 | +- ✅ Multi-location support (project-level, user-level, explicit path) |
| 124 | +- ✅ Integrates seamlessly with existing bash script (jq already used) |
| 125 | + |
| 126 | +### Data Model (Minimal) |
| 127 | + |
| 128 | +```bash |
| 129 | +# In the script, after loading config: |
| 130 | +declare -A SEVERITY_OVERRIDES # Custom severities loaded from file |
| 131 | + |
| 132 | +# Load shipped defaults into associative array |
| 133 | +load_severity_defaults() { |
| 134 | + SEVERITY_SHIPPED["wp-ajax-missing-nonce"]="HIGH" |
| 135 | + SEVERITY_SHIPPED["unbounded-rest-endpoint"]="CRITICAL" |
| 136 | + SEVERITY_SHIPPED["unbounded-query-get-posts"]="CRITICAL" |
| 137 | + SEVERITY_SHIPPED["n-plus-one-pattern"]="MEDIUM" |
| 138 | + SEVERITY_SHIPPED["deprecated-function"]="MEDIUM" |
| 139 | +} |
| 140 | + |
| 141 | +# Load custom overrides from user config file (if exists) |
| 142 | +load_custom_severity_config() { |
| 143 | + local config_file="$1" |
| 144 | + # Parse JSON and populate SEVERITY_OVERRIDES array |
| 145 | + # Use jq or simple grep/sed for portability |
| 146 | +} |
| 147 | + |
| 148 | +# Get final severity (custom > shipped) |
| 149 | +get_severity() { |
| 150 | + local check_id="$1" |
| 151 | + echo "${SEVERITY_OVERRIDES[$check_id]:-${SEVERITY_SHIPPED[$check_id]}}" |
| 152 | +} |
| 153 | +``` |
| 154 | + |
| 155 | +### Quickest Win: 3-Day Sprint |
| 156 | + |
| 157 | +**Day 1**: Extract hardcoded severity levels from `check-performance.sh` into `/dist/config/severity-levels.json` |
| 158 | +- Identify all 15+ checks and their current hardcoded levels |
| 159 | +- Create JSON structure with factory defaults |
| 160 | +- Add metadata (version, last_updated) |
| 161 | + |
| 162 | +**Day 2**: Add `load_custom_severity_config()` + `get_severity()` helpers in `check-performance.sh` |
| 163 | +- Parse JSON config file with jq (already used elsewhere in script) |
| 164 | +- Merge user overrides into shipped defaults |
| 165 | +- Update all check output lines to call `get_severity()` instead of hardcoding `[CRITICAL]` |
| 166 | +- Add `--severity-config` CLI option |
| 167 | + |
| 168 | +**Day 3**: Testing + documentation |
| 169 | +- Test with custom config in different locations (project root, home dir) |
| 170 | +- Verify CLI option works |
| 171 | +- Update README with usage examples |
| 172 | +- Add validation (warn if invalid severity level in config) |
| 173 | + |
| 174 | +### Example Output (After MVP) |
| 175 | + |
| 176 | +**TEXT REPORT:** |
| 177 | +``` |
| 178 | +WP Code Check v1.0.59 |
| 179 | +
|
| 180 | +Project: My Plugin v2.1.0 [plugin] |
| 181 | +Scanning paths: . |
| 182 | +Severity config: ./.wp-code-check-severity.json (2 customizations active) |
| 183 | +
|
| 184 | +━━━ CRITICAL CHECKS (will fail build) ━━━ |
| 185 | +
|
| 186 | +▸ REST endpoints without pagination [CRITICAL] |
| 187 | + ✗ FAILED |
| 188 | + ./includes/api.php:42: register_rest_route() |
| 189 | +
|
| 190 | +▸ Unbounded queries [CRITICAL] |
| 191 | + ✗ FAILED |
| 192 | + ./admin/list-users.php:15: posts_per_page => -1 |
| 193 | +
|
| 194 | +━━━ HIGH CHECKS ━━━ |
| 195 | +
|
| 196 | +▸ AJAX missing nonce validation [HIGH → CRITICAL] |
| 197 | + ✗ FAILED |
| 198 | + ./assets/js/admin.js:78: jQuery.post('/wp-admin/admin-ajax.php') |
| 199 | + Note: Severity customized (factory: CRITICAL, custom: HIGH) |
| 200 | +
|
| 201 | +━━━ MEDIUM CHECKS ━━━ |
| 202 | +
|
| 203 | +▸ Using deprecated functions [MEDIUM → LOW] |
| 204 | + ✓ PASSED |
| 205 | + ./includes/legacy.php:92: wp_make_content_safe() |
| 206 | + Note: Severity customized (factory: MEDIUM, custom: LOW) |
| 207 | +
|
| 208 | +━━━ SUMMARY ━━━ |
| 209 | +Errors: 2 (CRITICAL checks) |
| 210 | +Warnings: 1 (HIGH checks) |
| 211 | +
|
| 212 | +⚠️ Severity customizations applied (2/15 checks): |
| 213 | + - wp-ajax-missing-nonce: CRITICAL → HIGH |
| 214 | + - deprecated-function: MEDIUM → LOW |
| 215 | +``` |
| 216 | + |
| 217 | +**JSON REPORT:** |
| 218 | +```json |
| 219 | +{ |
| 220 | + "metadata": { |
| 221 | + "version": "1.0.59", |
| 222 | + "project": "My Plugin", |
| 223 | + "type": "plugin", |
| 224 | + "scan_time": "2025-12-31T15:30:45Z", |
| 225 | + "severity_config": "./.wp-code-check-severity.json", |
| 226 | + "severity_customizations": { |
| 227 | + "wp-ajax-missing-nonce": { |
| 228 | + "factory_default": "CRITICAL", |
| 229 | + "custom_level": "HIGH", |
| 230 | + "reason": "User override in project config" |
| 231 | + }, |
| 232 | + "deprecated-function": { |
| 233 | + "factory_default": "MEDIUM", |
| 234 | + "custom_level": "LOW", |
| 235 | + "reason": "User override in project config" |
| 236 | + } |
| 237 | + } |
| 238 | + }, |
| 239 | + "findings": [ |
| 240 | + { |
| 241 | + "id": "unbounded-rest-endpoint", |
| 242 | + "severity": "CRITICAL", |
| 243 | + "severity_customized": false, |
| 244 | + "file": "./includes/api.php", |
| 245 | + "line": 42, |
| 246 | + "code": "register_rest_route()" |
| 247 | + }, |
| 248 | + { |
| 249 | + "id": "wp-ajax-missing-nonce", |
| 250 | + "severity": "HIGH", |
| 251 | + "severity_customized": true, |
| 252 | + "factory_default": "CRITICAL", |
| 253 | + "file": "./assets/js/admin.js", |
| 254 | + "line": 78, |
| 255 | + "code": "jQuery.post('/wp-admin/admin-ajax.php')" |
| 256 | + } |
| 257 | + ] |
| 258 | +} |
| 259 | +``` |
| 260 | + |
| 261 | +**HTML REPORT:** |
| 262 | +```html |
| 263 | +<div class="severity-customizations-banner"> |
| 264 | + <h3>⚠️ Severity Customizations Active</h3> |
| 265 | + <p>This report uses custom severity levels. 2 out of 15 checks have been customized:</p> |
| 266 | + <table> |
| 267 | + <tr> |
| 268 | + <th>Check</th> |
| 269 | + <th>Factory Default</th> |
| 270 | + <th>Custom Level</th> |
| 271 | + </tr> |
| 272 | + <tr> |
| 273 | + <td>AJAX missing nonce</td> |
| 274 | + <td><span class="badge critical">CRITICAL</span></td> |
| 275 | + <td><span class="badge high">HIGH</span></td> |
| 276 | + </tr> |
| 277 | + <tr> |
| 278 | + <td>Deprecated functions</td> |
| 279 | + <td><span class="badge medium">MEDIUM</span></td> |
| 280 | + <td><span class="badge low">LOW</span></td> |
| 281 | + </tr> |
| 282 | + </table> |
| 283 | + <p><strong>Config file:</strong> ./.wp-code-check-severity.json</p> |
| 284 | + <button>Restore Factory Defaults</button> |
| 285 | +</div> |
| 286 | + |
| 287 | +<!-- Each finding shows if severity was customized --> |
| 288 | +<div class="finding"> |
| 289 | + <h4>REST endpoints without pagination <span class="badge critical">CRITICAL</span></h4> |
| 290 | + <p>File: ./includes/api.php:42</p> |
| 291 | + <p><code>register_rest_route()</code></p> |
| 292 | + <!-- No custom notice for this one --> |
| 293 | +</div> |
| 294 | + |
| 295 | +<div class="finding"> |
| 296 | + <h4>AJAX missing nonce <span class="badge high">HIGH</span></h4> |
| 297 | + <p>File: ./assets/js/admin.js:78</p> |
| 298 | + <p><code>jQuery.post('/wp-admin/admin-ajax.php')</code></p> |
| 299 | + <div class="custom-severity-notice"> |
| 300 | + <strong>Note:</strong> Severity customized for this rule |
| 301 | + <br/>Factory default: <span class="badge critical">CRITICAL</span> → Custom: <span class="badge high">HIGH</span> |
| 302 | + </div> |
| 303 | +</div> |
| 304 | +``` |
| 305 | + |
| 306 | +### Rules for Users |
| 307 | + |
| 308 | +1. **Where to customize**: |
| 309 | + - Copy shipped `/dist/config/severity-levels.json` to project root: `./.wp-code-check-severity.json` |
| 310 | + - Or place in home directory: `~/.wp-code-check-severity.json` |
| 311 | + - Pass explicit path: `./dist/bin/check-performance.sh --paths . --severity-config ./custom-levels.json` |
| 312 | + |
| 313 | +2. **Restore to factory defaults**: |
| 314 | + - Check `"factory_default"` in the file to see what the original was |
| 315 | + - Delete your local copy to revert to shipped defaults |
| 316 | + - Or manually change `"level"` back to match `"factory_default"` |
| 317 | + |
| 318 | +3. **Version control** (optional): |
| 319 | + - Commit `.wp-code-check-severity.json` to repo for team alignment |
| 320 | + - All developers on team get same severity customizations |
| 321 | + - CI/CD automatically uses the project's custom config |
| 322 | + |
| 323 | +### Config File Locations (Priority Order) |
| 324 | + |
| 325 | +1. `--severity-config <path>` (explicit argument, highest priority) |
| 326 | +2. `./.wp-code-check-severity.json` (project root) |
| 327 | +3. `~/.wp-code-check-severity.json` (user home) |
| 328 | +4. Shipped defaults in `/dist/config/severity-levels.json` (fallback) |
| 329 | + |
| 330 | +### Implementation Checklist |
| 331 | + |
| 332 | +**Phase 1 Work:** |
| 333 | +- [ ] Extract all hardcoded severity levels from `check-performance.sh` (grep for `\[CRITICAL\]`, `\[HIGH\]`, `\[MEDIUM\]`, `\[LOW\]`) |
| 334 | +- [ ] Create `/dist/config/severity-levels.json` with all checks + factory defaults |
| 335 | +- [ ] Map check patterns to rule IDs (e.g., `wp-ajax-missing-nonce`, `unbounded-rest-endpoint`) |
| 336 | +- [ ] Document each check with category (security, performance, maintenance) |
| 337 | + |
| 338 | +**Phase 2 Work:** |
| 339 | +- [ ] Add `load_severity_defaults()` function to setup shipped levels |
| 340 | +- [ ] Add `load_custom_severity_config()` to parse JSON file (use jq) |
| 341 | +- [ ] Add `get_severity(rule_id)` lookup function |
| 342 | +- [ ] Track which checks are customized (array of rule_ids with custom levels) |
| 343 | +- [ ] Update all check output lines to use `get_severity()` instead of hardcoded levels |
| 344 | +- [ ] Show customization notice in text report header (e.g., "2 customizations active") |
| 345 | +- [ ] Add inline notes for customized findings (e.g., "CRITICAL → HIGH") |
| 346 | +- [ ] Add `--severity-config <path>` CLI option |
| 347 | +- [ ] Support config file discovery (project root, home dir) |
| 348 | + |
| 349 | +**Phase 3 Work:** |
| 350 | +- [ ] Unit tests: verify config loading, merging, priority order |
| 351 | +- [ ] Integration tests: run with custom config, verify output includes customization notices |
| 352 | +- [ ] Text report: show banner header with active customizations + inline notes per finding |
| 353 | +- [ ] JSON report: add `severity_customizations` metadata + `severity_customized` flag per finding |
| 354 | +- [ ] HTML report: add customizations banner table + highlight customized findings with badges |
| 355 | +- [ ] Error handling: warn if config missing, invalid JSON, unknown rule IDs |
| 356 | + |
| 357 | +## Future Extensions (Not MVP) |
| 358 | + |
| 359 | +- ☐ Filter reports by severity threshold: `--min-severity HIGH` (only show HIGH, CRITICAL) |
| 360 | +- ☐ Fail CI only on specific severities: `--fail-on CRITICAL` (ignore MEDIUM warnings) |
| 361 | +- ☐ Team presets in config: `"team_override": { "security": {...}, "devs": {...} }` |
| 362 | +- ☐ Per-rule comments in config: `"note": "This team ignores deprecated warnings"` |
| 363 | +- ☐ HTML dashboard showing custom vs factory defaults |
| 364 | + |
| 365 | +## Why Not Build This? |
| 366 | + |
| 367 | +❌ **Weighted scoring systems**: Overkill for v1, simple levels are enough |
| 368 | +❌ **Per-file rules**: Not needed yet—most teams apply same rules project-wide |
| 369 | +❌ **Auto-detection**: Not our job—user decides what matters to them |
| 370 | +❌ **Database storage**: JSON files are simpler, version-controllable, mergeable |
| 371 | +❌ **UI/CLI filters**: Start with config file, add filter options later if needed |
0 commit comments