phpwarden 2.2.1__tar.gz → 2.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. {phpwarden-2.2.1 → phpwarden-2.4.0}/PKG-INFO +50 -19
  2. {phpwarden-2.2.1 → phpwarden-2.4.0}/README.md +47 -16
  3. {phpwarden-2.2.1 → phpwarden-2.4.0}/pyproject.toml +4 -4
  4. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden/__init__.py +1 -1
  5. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden/cli.py +173 -1
  6. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden/code.py +122 -6
  7. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden/code_rules.py +73 -3
  8. phpwarden-2.4.0/src/phpwarden/sast/__init__.py +10 -0
  9. phpwarden-2.4.0/src/phpwarden/sast/ast.py +85 -0
  10. phpwarden-2.4.0/src/phpwarden/sast/authz.py +284 -0
  11. phpwarden-2.4.0/src/phpwarden/sast/callgraph.py +627 -0
  12. phpwarden-2.4.0/src/phpwarden/sast/dbflow.py +720 -0
  13. phpwarden-2.4.0/src/phpwarden/sast/engine.py +138 -0
  14. phpwarden-2.4.0/src/phpwarden/sast/lexer.py +354 -0
  15. phpwarden-2.4.0/src/phpwarden/sast/models/__init__.py +6 -0
  16. phpwarden-2.4.0/src/phpwarden/sast/models/laravel.py +115 -0
  17. phpwarden-2.4.0/src/phpwarden/sast/models/php_builtins.py +123 -0
  18. phpwarden-2.4.0/src/phpwarden/sast/models/symfony.py +77 -0
  19. phpwarden-2.4.0/src/phpwarden/sast/models/wordpress.py +28 -0
  20. phpwarden-2.4.0/src/phpwarden/sast/parser.py +1565 -0
  21. phpwarden-2.4.0/src/phpwarden/sast/project.py +633 -0
  22. phpwarden-2.4.0/src/phpwarden/sast/symbols.py +184 -0
  23. phpwarden-2.4.0/src/phpwarden/sast/taint.py +2484 -0
  24. phpwarden-2.4.0/src/phpwarden/sast/types.py +432 -0
  25. phpwarden-2.4.0/src/phpwarden/shield/EDGE.md +108 -0
  26. phpwarden-2.4.0/src/phpwarden/shield/README.md +111 -0
  27. phpwarden-2.4.0/src/phpwarden/shield/__init__.py +1 -0
  28. phpwarden-2.4.0/src/phpwarden/shield/agent/phpwarden-shield.php +572 -0
  29. phpwarden-2.4.0/src/phpwarden/shield/library.py +403 -0
  30. phpwarden-2.4.0/src/phpwarden/shield/matcher.py +119 -0
  31. phpwarden-2.4.0/src/phpwarden/shield/policy.py +254 -0
  32. phpwarden-2.4.0/src/phpwarden/shield/report.py +89 -0
  33. phpwarden-2.4.0/src/phpwarden/thirdparty/__init__.py +1 -0
  34. phpwarden-2.4.0/src/phpwarden/thirdparty/trivy.py +188 -0
  35. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden/verdict.py +56 -4
  36. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden.egg-info/PKG-INFO +50 -19
  37. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden.egg-info/SOURCES.txt +37 -0
  38. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_code.py +14 -3
  39. phpwarden-2.4.0/tests/test_sast_dbflow.py +106 -0
  40. phpwarden-2.4.0/tests/test_sast_layer5.py +82 -0
  41. phpwarden-2.4.0/tests/test_sast_models.py +85 -0
  42. phpwarden-2.4.0/tests/test_sast_parser.py +237 -0
  43. phpwarden-2.4.0/tests/test_sast_project.py +134 -0
  44. phpwarden-2.4.0/tests/test_sast_taint.py +223 -0
  45. phpwarden-2.4.0/tests/test_shield_cli.py +51 -0
  46. phpwarden-2.4.0/tests/test_shield_dashboard.py +168 -0
  47. phpwarden-2.4.0/tests/test_shield_runtime.py +380 -0
  48. phpwarden-2.4.0/tests/test_trivy_import.py +131 -0
  49. {phpwarden-2.2.1 → phpwarden-2.4.0}/LICENSE +0 -0
  50. {phpwarden-2.2.1 → phpwarden-2.4.0}/setup.cfg +0 -0
  51. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden/data/php-cve-feed.json +0 -0
  52. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden/deps.py +0 -0
  53. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden/detect.py +0 -0
  54. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden/engine.py +0 -0
  55. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden/env.py +0 -0
  56. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden/feed.py +0 -0
  57. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden/guard.py +0 -0
  58. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden/integrity.py +0 -0
  59. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden/probe.py +0 -0
  60. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden/report.py +0 -0
  61. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden/rules.py +0 -0
  62. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden/sarif.py +0 -0
  63. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden/version.py +0 -0
  64. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden/vulndb.py +0 -0
  65. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden/wordpress.py +0 -0
  66. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden.egg-info/dependency_links.txt +0 -0
  67. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden.egg-info/entry_points.txt +0 -0
  68. {phpwarden-2.2.1 → phpwarden-2.4.0}/src/phpwarden.egg-info/top_level.txt +0 -0
  69. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_account_deletion.py +0 -0
  70. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_accounts.py +0 -0
  71. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_advisories.py +0 -0
  72. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_agency.py +0 -0
  73. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_assess.py +0 -0
  74. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_assignment.py +0 -0
  75. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_audit.py +0 -0
  76. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_billing.py +0 -0
  77. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_business_metrics.py +0 -0
  78. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_cli_edges.py +0 -0
  79. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_connect_flow.py +0 -0
  80. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_dashboard_states.py +0 -0
  81. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_deps_and_rules.py +0 -0
  82. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_detection_validation.py +0 -0
  83. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_developer.py +0 -0
  84. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_discussion.py +0 -0
  85. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_e2e_readiness.py +0 -0
  86. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_email.py +0 -0
  87. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_engine.py +0 -0
  88. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_engine_feed.py +0 -0
  89. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_feed_license.py +0 -0
  90. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_feed_subscription.py +0 -0
  91. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_fleet.py +0 -0
  92. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_free_feed.py +0 -0
  93. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_google_oauth.py +0 -0
  94. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_guard_live.py +0 -0
  95. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_integrity.py +0 -0
  96. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_kev_alert.py +0 -0
  97. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_kev_gaps.py +0 -0
  98. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_livefeed.py +0 -0
  99. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_mentions.py +0 -0
  100. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_probe.py +0 -0
  101. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_probe_cli.py +0 -0
  102. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_probe_dashboard.py +0 -0
  103. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_reachability.py +0 -0
  104. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_report_proof.py +0 -0
  105. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_saas.py +0 -0
  106. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_saas_billing.py +0 -0
  107. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_sarif.py +0 -0
  108. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_server.py +0 -0
  109. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_settings.py +0 -0
  110. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_shield.py +0 -0
  111. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_shield_plan.py +0 -0
  112. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_slack.py +0 -0
  113. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_sso.py +0 -0
  114. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_store_connections.py +0 -0
  115. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_team.py +0 -0
  116. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_triage.py +0 -0
  117. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_waf_dryrun.py +0 -0
  118. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_webhook_idempotency.py +0 -0
  119. {phpwarden-2.2.1 → phpwarden-2.4.0}/tests/test_wordpress_and_report.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phpwarden
3
- Version: 2.2.1
4
- Summary: PHP vulnerability scanner for the PHP engine, Composer, WordPress, your own code and the live site. Ranked by real-world exploitation (CISA KEV, then EPSS), not CVSS. Offline, zero dependencies.
3
+ Version: 2.4.0
4
+ Summary: PHP vulnerability scanner for the PHP engine, Composer, WordPress, your own code and the live site. Ranked by real-world exploitation (CISA KEV, then EPSS), not CVSS; blocks exploits inside PHP (shield). Offline, zero dependencies.
5
5
  Author: oroma-alfred
6
6
  License-Expression: Apache-2.0
7
7
  Project-URL: Homepage, https://phpwarden.io
8
8
  Project-URL: Source, https://github.com/oroma-alfred/phpwarden
9
9
  Project-URL: Issues, https://github.com/oroma-alfred/phpwarden/issues
10
- Keywords: php,composer,wordpress,php-fpm,security,vulnerability,cve,kev,epss,sca,scanner
10
+ Keywords: php,composer,wordpress,php-fpm,security,vulnerability,cve,kev,epss,sca,sast,waf,scanner
11
11
  Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Environment :: Console
13
13
  Classifier: Intended Audience :: System Administrators
@@ -28,8 +28,9 @@ exploited (CISA KEV, as of 2026-11-14). phpwarden puts those first, then sorts
28
28
  by EPSS probability, then CVSS — so a `composer.lock` with 30 findings tells
29
29
  you which one to fix today.
30
30
 
31
- It scans the PHP engine/FPM version, Composer dependencies and WordPress
32
- installs against one bundled, offline advisory feed. Zero runtime dependencies
31
+ It scans the PHP engine/FPM version, Composer dependencies, WordPress installs
32
+ and your own PHP code, checks live sites from outside, and can block exploits
33
+ inside PHP, against one bundled, offline advisory feed. Zero runtime dependencies
33
34
  (Python 3.8+), Apache-2.0.
34
35
 
35
36
  **Reproducible benchmark:** on four real PHP stacks, phpwarden found 295 of 295
@@ -59,7 +60,8 @@ $ phpwarden deps composer.lock
59
60
  | Composer dependency CVEs | `deps` | Snyk, `composer audit` |
60
61
  | WordPress core, plugin and theme CVEs | `wordpress` | Patchstack, Wordfence |
61
62
  | WordPress file integrity (wordpress.org checksums, injected files) | `integrity` | Wordfence CLI malware-scan (partly) |
62
- | Your own PHP code (75 rules) | `code` | — (not a PHPStan/Psalm replacement) |
63
+ | Your own PHP code: whole-program, Laravel/Symfony/WordPress-aware (89 rules) | `code` | Snyk Code, Semgrep, Psalm taint |
64
+ | Exploits of the vulnerabilities this site has, blocked inside PHP | `shield` | the WAF rule you would have to write |
63
65
  | Live site from outside: exposed files, debug endpoints | `probe` | — |
64
66
  | One ranked verdict across all of the above | `assess` | — |
65
67
  | Exploitation signatures in logs | `detect` | SIEM content |
@@ -120,7 +122,8 @@ phpwarden rules --format nginx --out php-block.conf # PHP-specific WAF rules (
120
122
  phpwarden rules --format nginx --dry-run # the same rules, log-only: nothing is blocked
121
123
  phpwarden rules --replay /var/log/nginx/access.log # what the rules would have blocked in an existing log
122
124
  phpwarden probe https://example.com # live check of a site you own: exposed files, debug endpoints, versions vs the feed
123
- phpwarden code . # your own PHP: 75 high-signal rules, 74 on by default (injection, unserialize, WordPress handlers)
125
+ phpwarden code . # your own PHP, whole program: 89 rules (injection, auth, mass assignment, Blade/Twig, WordPress)
126
+ phpwarden shield policy . && phpwarden shield install # block exploits of what is installed here, inside PHP (log-only first)
124
127
  phpwarden assess . --url https://example.com --log access.log # one ranked verdict; --out / --baseline shows what is fixed and verified
125
128
  phpwarden report web01.json web02.json --out fleet.html # aggregate a fleet view
126
129
  phpwarden feed status # which feed is active, how many advisories
@@ -133,7 +136,7 @@ phpwarden feed update # pull a newer feed: free weekly snapshot,
133
136
  `probe`, `code`, `assess`, `--format sarif`, `rules --dry-run` / `--replay` and
134
137
  the no-key `feed update` are new in 2.2.0.
135
138
 
136
- Every command takes `--json`. Exit codes are shared across subcommands and
139
+ Every scanning command takes `--json` (`shield install` and `shield promote` do not). Exit codes are shared across subcommands and
137
140
  built for gating: `0` clean, `1` exposed / vulnerable / signature hits / tampered
138
141
  files / high or critical `code` and `probe` findings (`probe` also on any
139
142
  known-exploited finding) / anything to act on now or this week (`assess`),
@@ -142,7 +145,26 @@ error, `5` `--report-to` upload failed on an otherwise clean run, `64` usage
142
145
  error. Full table: `phpwarden --help`.
143
146
 
144
147
 
145
- ## New in 2.2.0
148
+ ## New in 2.4.0, 2.3.0 and 2.2
149
+
150
+ - **2.4.0: `phpwarden code` follows data through the database and callbacks.**
151
+ Request data saved with raw SQL, `$wpdb`, Eloquent, the query builder or
152
+ Doctrine and read back from the same table is traced to where it is printed
153
+ or queried (stored XSS, second-order SQL injection), and callbacks passed to
154
+ `array_map`, `usort`, `preg_replace_callback`, `add_filter` and the like,
155
+ `$f()` and first-class callables are followed. DVWA: 8/8/7 of 8 types under
156
+ the comparison's scorer (was 7/7/6); clean-code counts unchanged.
157
+ - **2.4.0: shield rate limits.** Per-client limits on WordPress login, password
158
+ reset, xmlrpc and `system.multicall`, Laravel login and reset, component
159
+ writes, and a burst guard. Log-only until promoted, then 429; APCu or file
160
+ store; fails open. `--trust-proxy CIDR`, `--no-limits`, `--limit-path REGEX`.
161
+ 0 trips on 6,000 ordinary requests; 6 of 6 abuse scenarios caught. Pairing
162
+ with Cloudflare or nginx: [EDGE.md](src/phpwarden/shield/EDGE.md).
163
+ - **2.4.0: `phpwarden trivy-import` and `assess --trivy FILE` / `--run-trivy`.**
164
+ Trivy's JSON (npm/JS, OS packages, images, IaC, secrets) in the same ranked
165
+ list as PHP findings: KEV, then EPSS (`--kev`, `--epss`), then severity.
166
+ Secrets by file and line only. Never downloads Trivy. See
167
+ [src/phpwarden/thirdparty/README.md](src/phpwarden/thirdparty/README.md).
146
168
 
147
169
  - **`phpwarden probe URL`**: a live check of a running site using GET and HEAD
148
170
  requests only. It looks for exposed files (`.env`, `.git`, backups), open debug
@@ -150,11 +172,20 @@ error. Full table: `phpwarden --help`.
150
172
  matches those against the feed. Active checks run only after you prove the
151
173
  site is yours (DNS TXT record, a file, or a meta tag; `--print-token` shows
152
174
  how). Without that it reads the homepage and stops.
153
- - **`phpwarden code PATH`**: 75 rules (74 on by default) for your own PHP code, following request
154
- data to `eval`, shell commands, SQL, `unserialize`, includes and redirects.
155
- It follows data across files (helper functions and classes elsewhere in the
156
- project, fixed includes, class properties); it is not a type checker like
157
- PHPStan or Psalm. `phpwarden code --list-rules` lists every rule.
175
+ - **`phpwarden code PATH`** (whole-program engine, default in 2.3.0): parses the
176
+ project, resolves namespaces, Composer autoloading, inheritance and types, and
177
+ follows request data across files to SQL, shell, `eval`, `unserialize`,
178
+ includes, redirects and HTTP clients, with a trace per finding (SARIF
179
+ `codeFlows`). Models Laravel, Symfony and WordPress: routes with no auth check,
180
+ mass assignment, raw Blade/Twig output, IDOR hints, CSRF exclusions, nonces.
181
+ Follows data stored in the session or WordPress options (second-order).
182
+ 89 rules. On the same pinned apps it found more than Semgrep and Psalm with far
183
+ fewer findings on clean framework code ([comparison](https://github.com/oroma-alfred/phpwarden/tree/main/bench/sast-compare-2026-11)).
184
+ - **`phpwarden shield`**: one PHP file loaded before your app that blocks exploit
185
+ attempts against the exploited vulnerabilities this site actually has, scoped
186
+ to their entry points. Logs until you promote rules (7-day burn-in, explicit
187
+ flag), fails open, has a kill switch, about 0.07 ms per request. Not an edge
188
+ WAF: no DDoS, bot or TLS handling.
158
189
  - **`phpwarden assess`**: one ranked list from every view of a site: installed
159
190
  versions, your code, the live site from outside, and the access log, with the
160
191
  reason for each item ("exploited, reachable, visible from outside, already
@@ -236,7 +267,7 @@ see [phpwarden/laravel](https://packagist.org/packages/phpwarden/laravel).
236
267
  ```yaml
237
268
  repos:
238
269
  - repo: https://github.com/oroma-alfred/phpwarden
239
- rev: v2.1.0 # pin to a tag with `pre-commit autoupdate`
270
+ rev: v2.4.0 # pin to a tag with `pre-commit autoupdate`
240
271
  hooks:
241
272
  - id: phpwarden-deps
242
273
  # args: ["--no-reachability"] # faster: skip the reachability pass
@@ -247,14 +278,14 @@ timer recipes are in [`docs/recipes.md`](docs/recipes.md).
247
278
 
248
279
  ## The feed
249
280
 
250
- The bundled community feed (2026.11.13.3 in this release) works forever, offline:
281
+ The bundled community feed (2026.11.14.2 in this release) works forever, offline:
251
282
 
252
283
  - **~21,300 advisories**: ~8,800 Composer (from [OSV.dev](https://osv.dev) and
253
284
  [FriendsOfPHP](https://github.com/FriendsOfPHP/security-advisories)),
254
285
  ~12,300 WordPress (from [NVD](https://nvd.nist.gov): plugin, core and theme),
255
- and 145 PHP engine/FPM CVEs, including hand-curated entries with per-branch
256
- fixed versions, exposure conditions and detection signatures. About 19,700 carry an
257
- EPSS score; 41 distinct vulnerabilities were in KEV when this feed was built.
286
+ and 146 PHP engine/FPM CVEs, including hand-curated entries with per-branch
287
+ fixed versions, exposure conditions and detection signatures. About 19,800 carry an
288
+ EPSS score; 54 distinct vulnerabilities were in KEV when this feed was built.
258
289
  - **License-independent by construction.** Every source is open or
259
290
  public-domain (OSV, NVD, FriendsOfPHP, php-src release notes, CISA KEV, FIRST EPSS). No commercial
260
291
  vendor's feed is used, so coverage cannot be revoked or priced away.
@@ -6,8 +6,9 @@ exploited (CISA KEV, as of 2026-11-14). phpwarden puts those first, then sorts
6
6
  by EPSS probability, then CVSS — so a `composer.lock` with 30 findings tells
7
7
  you which one to fix today.
8
8
 
9
- It scans the PHP engine/FPM version, Composer dependencies and WordPress
10
- installs against one bundled, offline advisory feed. Zero runtime dependencies
9
+ It scans the PHP engine/FPM version, Composer dependencies, WordPress installs
10
+ and your own PHP code, checks live sites from outside, and can block exploits
11
+ inside PHP, against one bundled, offline advisory feed. Zero runtime dependencies
11
12
  (Python 3.8+), Apache-2.0.
12
13
 
13
14
  **Reproducible benchmark:** on four real PHP stacks, phpwarden found 295 of 295
@@ -37,7 +38,8 @@ $ phpwarden deps composer.lock
37
38
  | Composer dependency CVEs | `deps` | Snyk, `composer audit` |
38
39
  | WordPress core, plugin and theme CVEs | `wordpress` | Patchstack, Wordfence |
39
40
  | WordPress file integrity (wordpress.org checksums, injected files) | `integrity` | Wordfence CLI malware-scan (partly) |
40
- | Your own PHP code (75 rules) | `code` | — (not a PHPStan/Psalm replacement) |
41
+ | Your own PHP code: whole-program, Laravel/Symfony/WordPress-aware (89 rules) | `code` | Snyk Code, Semgrep, Psalm taint |
42
+ | Exploits of the vulnerabilities this site has, blocked inside PHP | `shield` | the WAF rule you would have to write |
41
43
  | Live site from outside: exposed files, debug endpoints | `probe` | — |
42
44
  | One ranked verdict across all of the above | `assess` | — |
43
45
  | Exploitation signatures in logs | `detect` | SIEM content |
@@ -98,7 +100,8 @@ phpwarden rules --format nginx --out php-block.conf # PHP-specific WAF rules (
98
100
  phpwarden rules --format nginx --dry-run # the same rules, log-only: nothing is blocked
99
101
  phpwarden rules --replay /var/log/nginx/access.log # what the rules would have blocked in an existing log
100
102
  phpwarden probe https://example.com # live check of a site you own: exposed files, debug endpoints, versions vs the feed
101
- phpwarden code . # your own PHP: 75 high-signal rules, 74 on by default (injection, unserialize, WordPress handlers)
103
+ phpwarden code . # your own PHP, whole program: 89 rules (injection, auth, mass assignment, Blade/Twig, WordPress)
104
+ phpwarden shield policy . && phpwarden shield install # block exploits of what is installed here, inside PHP (log-only first)
102
105
  phpwarden assess . --url https://example.com --log access.log # one ranked verdict; --out / --baseline shows what is fixed and verified
103
106
  phpwarden report web01.json web02.json --out fleet.html # aggregate a fleet view
104
107
  phpwarden feed status # which feed is active, how many advisories
@@ -111,7 +114,7 @@ phpwarden feed update # pull a newer feed: free weekly snapshot,
111
114
  `probe`, `code`, `assess`, `--format sarif`, `rules --dry-run` / `--replay` and
112
115
  the no-key `feed update` are new in 2.2.0.
113
116
 
114
- Every command takes `--json`. Exit codes are shared across subcommands and
117
+ Every scanning command takes `--json` (`shield install` and `shield promote` do not). Exit codes are shared across subcommands and
115
118
  built for gating: `0` clean, `1` exposed / vulnerable / signature hits / tampered
116
119
  files / high or critical `code` and `probe` findings (`probe` also on any
117
120
  known-exploited finding) / anything to act on now or this week (`assess`),
@@ -120,7 +123,26 @@ error, `5` `--report-to` upload failed on an otherwise clean run, `64` usage
120
123
  error. Full table: `phpwarden --help`.
121
124
 
122
125
 
123
- ## New in 2.2.0
126
+ ## New in 2.4.0, 2.3.0 and 2.2
127
+
128
+ - **2.4.0: `phpwarden code` follows data through the database and callbacks.**
129
+ Request data saved with raw SQL, `$wpdb`, Eloquent, the query builder or
130
+ Doctrine and read back from the same table is traced to where it is printed
131
+ or queried (stored XSS, second-order SQL injection), and callbacks passed to
132
+ `array_map`, `usort`, `preg_replace_callback`, `add_filter` and the like,
133
+ `$f()` and first-class callables are followed. DVWA: 8/8/7 of 8 types under
134
+ the comparison's scorer (was 7/7/6); clean-code counts unchanged.
135
+ - **2.4.0: shield rate limits.** Per-client limits on WordPress login, password
136
+ reset, xmlrpc and `system.multicall`, Laravel login and reset, component
137
+ writes, and a burst guard. Log-only until promoted, then 429; APCu or file
138
+ store; fails open. `--trust-proxy CIDR`, `--no-limits`, `--limit-path REGEX`.
139
+ 0 trips on 6,000 ordinary requests; 6 of 6 abuse scenarios caught. Pairing
140
+ with Cloudflare or nginx: [EDGE.md](src/phpwarden/shield/EDGE.md).
141
+ - **2.4.0: `phpwarden trivy-import` and `assess --trivy FILE` / `--run-trivy`.**
142
+ Trivy's JSON (npm/JS, OS packages, images, IaC, secrets) in the same ranked
143
+ list as PHP findings: KEV, then EPSS (`--kev`, `--epss`), then severity.
144
+ Secrets by file and line only. Never downloads Trivy. See
145
+ [src/phpwarden/thirdparty/README.md](src/phpwarden/thirdparty/README.md).
124
146
 
125
147
  - **`phpwarden probe URL`**: a live check of a running site using GET and HEAD
126
148
  requests only. It looks for exposed files (`.env`, `.git`, backups), open debug
@@ -128,11 +150,20 @@ error. Full table: `phpwarden --help`.
128
150
  matches those against the feed. Active checks run only after you prove the
129
151
  site is yours (DNS TXT record, a file, or a meta tag; `--print-token` shows
130
152
  how). Without that it reads the homepage and stops.
131
- - **`phpwarden code PATH`**: 75 rules (74 on by default) for your own PHP code, following request
132
- data to `eval`, shell commands, SQL, `unserialize`, includes and redirects.
133
- It follows data across files (helper functions and classes elsewhere in the
134
- project, fixed includes, class properties); it is not a type checker like
135
- PHPStan or Psalm. `phpwarden code --list-rules` lists every rule.
153
+ - **`phpwarden code PATH`** (whole-program engine, default in 2.3.0): parses the
154
+ project, resolves namespaces, Composer autoloading, inheritance and types, and
155
+ follows request data across files to SQL, shell, `eval`, `unserialize`,
156
+ includes, redirects and HTTP clients, with a trace per finding (SARIF
157
+ `codeFlows`). Models Laravel, Symfony and WordPress: routes with no auth check,
158
+ mass assignment, raw Blade/Twig output, IDOR hints, CSRF exclusions, nonces.
159
+ Follows data stored in the session or WordPress options (second-order).
160
+ 89 rules. On the same pinned apps it found more than Semgrep and Psalm with far
161
+ fewer findings on clean framework code ([comparison](https://github.com/oroma-alfred/phpwarden/tree/main/bench/sast-compare-2026-11)).
162
+ - **`phpwarden shield`**: one PHP file loaded before your app that blocks exploit
163
+ attempts against the exploited vulnerabilities this site actually has, scoped
164
+ to their entry points. Logs until you promote rules (7-day burn-in, explicit
165
+ flag), fails open, has a kill switch, about 0.07 ms per request. Not an edge
166
+ WAF: no DDoS, bot or TLS handling.
136
167
  - **`phpwarden assess`**: one ranked list from every view of a site: installed
137
168
  versions, your code, the live site from outside, and the access log, with the
138
169
  reason for each item ("exploited, reachable, visible from outside, already
@@ -214,7 +245,7 @@ see [phpwarden/laravel](https://packagist.org/packages/phpwarden/laravel).
214
245
  ```yaml
215
246
  repos:
216
247
  - repo: https://github.com/oroma-alfred/phpwarden
217
- rev: v2.1.0 # pin to a tag with `pre-commit autoupdate`
248
+ rev: v2.4.0 # pin to a tag with `pre-commit autoupdate`
218
249
  hooks:
219
250
  - id: phpwarden-deps
220
251
  # args: ["--no-reachability"] # faster: skip the reachability pass
@@ -225,14 +256,14 @@ timer recipes are in [`docs/recipes.md`](docs/recipes.md).
225
256
 
226
257
  ## The feed
227
258
 
228
- The bundled community feed (2026.11.13.3 in this release) works forever, offline:
259
+ The bundled community feed (2026.11.14.2 in this release) works forever, offline:
229
260
 
230
261
  - **~21,300 advisories**: ~8,800 Composer (from [OSV.dev](https://osv.dev) and
231
262
  [FriendsOfPHP](https://github.com/FriendsOfPHP/security-advisories)),
232
263
  ~12,300 WordPress (from [NVD](https://nvd.nist.gov): plugin, core and theme),
233
- and 145 PHP engine/FPM CVEs, including hand-curated entries with per-branch
234
- fixed versions, exposure conditions and detection signatures. About 19,700 carry an
235
- EPSS score; 41 distinct vulnerabilities were in KEV when this feed was built.
264
+ and 146 PHP engine/FPM CVEs, including hand-curated entries with per-branch
265
+ fixed versions, exposure conditions and detection signatures. About 19,800 carry an
266
+ EPSS score; 54 distinct vulnerabilities were in KEV when this feed was built.
236
267
  - **License-independent by construction.** Every source is open or
237
268
  public-domain (OSV, NVD, FriendsOfPHP, php-src release notes, CISA KEV, FIRST EPSS). No commercial
238
269
  vendor's feed is used, so coverage cannot be revoked or priced away.
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "phpwarden"
7
- version = "2.2.1"
8
- description = "PHP vulnerability scanner for the PHP engine, Composer, WordPress, your own code and the live site. Ranked by real-world exploitation (CISA KEV, then EPSS), not CVSS. Offline, zero dependencies."
7
+ version = "2.4.0"
8
+ description = "PHP vulnerability scanner for the PHP engine, Composer, WordPress, your own code and the live site. Ranked by real-world exploitation (CISA KEV, then EPSS), not CVSS; blocks exploits inside PHP (shield). Offline, zero dependencies."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
11
11
  license = "Apache-2.0"
12
12
  license-files = ["LICENSE"]
13
13
  authors = [{ name = "oroma-alfred" }]
14
- keywords = ["php", "composer", "wordpress", "php-fpm", "security", "vulnerability", "cve", "kev", "epss", "sca", "scanner"]
14
+ keywords = ["php", "composer", "wordpress", "php-fpm", "security", "vulnerability", "cve", "kev", "epss", "sca", "sast", "waf", "scanner"]
15
15
  classifiers = [
16
16
  "Development Status :: 4 - Beta",
17
17
  "Environment :: Console",
@@ -39,4 +39,4 @@ package-dir = { "" = "src" }
39
39
  where = ["src"]
40
40
 
41
41
  [tool.setuptools.package-data]
42
- phpwarden = ["data/*.json"]
42
+ phpwarden = ["data/*.json", "shield/agent/*.php", "shield/*.md"]
@@ -6,4 +6,4 @@ real-world exploitation (CISA KEV, then EPSS, then CVSS), and can watch PHP-FPM
6
6
  for active exploitation. The engine and the bundled feed are open; free users
7
7
  get weekly feed updates, and a subscription adds the feed rebuilt twice a day.
8
8
  """
9
- __version__ = "2.2.1"
9
+ __version__ = "2.4.0"
@@ -13,6 +13,7 @@
13
13
  phpwarden probe URL safe live HTTP check of a site you own
14
14
  phpwarden code PATH scan your own PHP code for high-signal security bugs
15
15
  phpwarden assess PATH --url U --log L one ranked verdict from every view
16
+ phpwarden shield policy PATH protection inside PHP for the exploited vulns installed here
16
17
 
17
18
  Exit codes (shared by every subcommand; usable as a CI gate):
18
19
  0 nothing applies / clean
@@ -545,8 +546,29 @@ def _assess(args):
545
546
  if any("homepage unreachable" in n for n in probe_result.get("notes", [])):
546
547
  sys.stderr.write(f"error: could not reach {url}\n")
547
548
  return 4
549
+ trivy_fs, trivy_note = None, None
550
+ if args.trivy or args.run_trivy:
551
+ from phpwarden.thirdparty import trivy as tv
552
+ try:
553
+ kev, epss = tv.load_kev(args.kev), tv.load_epss(args.epss)
554
+ docs = [tv.load_report(f) for f in (args.trivy or [])]
555
+ except (OSError, ValueError) as e:
556
+ sys.stderr.write(f"error: {e}\n")
557
+ return 4
558
+ if args.run_trivy and args.path:
559
+ doc, msg = tv.run_trivy_fs(args.path)
560
+ if doc is not None:
561
+ docs.append(doc)
562
+ else:
563
+ trivy_note = msg
564
+ sys.stderr.write(msg + "\n")
565
+ trivy_fs = [f for d in docs for f in tv.findings(d, kev, epss)]
566
+ if docs and not (args.kev or args.epss):
567
+ trivy_note = (trivy_note + "; " if trivy_note else "") + \
568
+ "non-PHP findings ranked by severity only (pass --kev and --epss for exploitation data)"
548
569
  try:
549
- res = vd.assess(args.path, probe_result=probe_result, logfile=args.log, run_code=not args.no_code)
570
+ res = vd.assess(args.path, probe_result=probe_result, logfile=args.log, run_code=not args.no_code,
571
+ trivy_findings=trivy_fs, trivy_note=trivy_note)
550
572
  except OSError as e: # unreadable access log
551
573
  sys.stderr.write(f"error: {e}\n")
552
574
  return 4
@@ -562,6 +584,112 @@ def _assess(args):
562
584
  return vd.exit_code(res)
563
585
 
564
586
 
587
+ def _trivy_import(args):
588
+ from phpwarden.thirdparty import trivy as tv
589
+ try:
590
+ kev, epss = tv.load_kev(args.kev), tv.load_epss(args.epss)
591
+ fs = [f for p in args.files for f in tv.findings(tv.load_report(p), kev, epss)]
592
+ except (OSError, ValueError) as e:
593
+ sys.stderr.write(f"error: {e}\n")
594
+ return 4
595
+ doc = tv.to_report_doc(fs, args.files)
596
+ if args.json:
597
+ print(json.dumps(doc, indent=2))
598
+ else:
599
+ kev_n = sum(1 for f in fs if f["known_exploited"])
600
+ print(f"{len(fs)} finding(s) from {len(args.files)} Trivy report(s); {kev_n} known exploited. "
601
+ f"Ranked by exploitation{'' if (args.kev or args.epss) else ' (no --kev/--epss: by severity)'}:")
602
+ for f in fs[:50]:
603
+ tag = " [KEV]" if f["known_exploited"] else (f" [EPSS {f['epss'] * 100:.0f}%]" if f.get("epss") else "")
604
+ dev = " (dev)" if f["dev"] else ""
605
+ print(f" {f['severity']:<8} {tv.ident(f)}{tag}{dev} {f['title'][:90]}")
606
+ if len(fs) > 50:
607
+ print(f" ... and {len(fs) - 50} more; use --json for all")
608
+ rc = 1 if any(f["known_exploited"] or f["severity"] in ("critical", "high") for f in fs) else 0
609
+ return _combine(rc, _maybe_report(args, doc))
610
+
611
+
612
+ def _shield(args):
613
+ from pathlib import Path as _P
614
+ from phpwarden.shield import policy as sp, report as sr
615
+ if args.shield_cmd == "policy":
616
+ root = _P(args.path)
617
+ if not root.is_dir():
618
+ sys.stderr.write(f"error: not a directory: {args.path}\n")
619
+ return 4
620
+ pol = sp.build_policy_for_path(str(root), FeedManager().current(), php_version=args.php_version,
621
+ php_sapi=args.php_sapi, log_path=args.log,
622
+ limits=not args.no_limits, limit_paths=args.limit_path or [],
623
+ trust_proxy=args.trust_proxy or [])
624
+ out = args.out or "phpwarden-shield.policy.json"
625
+ j, php = sp.write_policy(pol, out)
626
+ if args.json:
627
+ print(json.dumps(pol, indent=2))
628
+ return 0
629
+ print(f"wrote {j} and {php} ({len(pol['rules'])} rule(s), mode: log)")
630
+ for r in pol["rules"]:
631
+ print(f" + {r['id']:<34} {r.get('cve') or '':<16} {r['component']}: {r['why']}")
632
+ if not pol["rules"]:
633
+ print(" No exploited vulnerability with a known request shape is installed here; the policy only has")
634
+ print(" the generic checks, if any applied. Nothing to protect against beyond those.")
635
+ print("\nNext: `phpwarden shield install` shows how to load the agent. It only logs until you promote rules.")
636
+ return 0
637
+ if args.shield_cmd == "install":
638
+ agent = _P(__file__).resolve().parent / "shield" / "agent" / "phpwarden-shield.php"
639
+ dest = _P(args.dest) if args.dest else None
640
+ if dest:
641
+ try:
642
+ dest.mkdir(parents=True, exist_ok=True)
643
+ (dest / "phpwarden-shield.php").write_bytes(agent.read_bytes())
644
+ except OSError as e:
645
+ sys.stderr.write(f"error: {e}\n")
646
+ return 4
647
+ where = (dest / "phpwarden-shield.php").resolve()
648
+ print(f"copied the agent to {where}")
649
+ else:
650
+ where = "/path/to/phpwarden-shield.php"
651
+ print(f"The agent ships with phpwarden at {agent}. Copy it next to your site, or rerun with --dest DIR.")
652
+ print(f"""
653
+ Load it before your application (pick one; nothing is changed for you):
654
+ php.ini or .user.ini: auto_prepend_file={where}
655
+ WordPress: add require '{where}'; as the first line of wp-config.php
656
+ Laravel / other apps: add require '{where}'; at the top of public/index.php
657
+
658
+ Put phpwarden-shield.policy.php (from `phpwarden shield policy`) next to the agent, or set
659
+ PHPWARDEN_SHIELD_POLICY to its path. It starts in log-only mode and fails open on any error.
660
+ Turn it off instantly: create a file named phpwarden-shield.off next to the agent, or set
661
+ PHPWARDEN_SHIELD=off.""")
662
+ return 0
663
+ if args.shield_cmd == "report":
664
+ try:
665
+ with open(args.log, errors="replace") as fh:
666
+ summ = sr.summarize(fh)
667
+ except OSError as e:
668
+ sys.stderr.write(f"error: {e}\n")
669
+ return 4
670
+ print(json.dumps(summ, indent=2, default=list) if args.json else sr.to_text(summ))
671
+ return 0
672
+ if args.shield_cmd == "promote":
673
+ try:
674
+ pol = json.loads(_P(args.policy).read_text())
675
+ with open(args.log, errors="replace") as fh:
676
+ summ = sr.summarize(fh)
677
+ except (OSError, ValueError) as e:
678
+ sys.stderr.write(f"error: {e}\n")
679
+ return 4
680
+ pol, rep = sp.promote(pol, summ, operator_ack=args.i_understand_this_blocks_requests,
681
+ rule_ids=set(args.rule) if args.rule else None)
682
+ for rid in rep["promoted"]:
683
+ print(f" BLOCK {rid}")
684
+ for r in rep["refused"]:
685
+ print(f" still logging {r['id']}: {r['reason']}")
686
+ if rep["promoted"]:
687
+ sp.write_policy(pol, args.policy)
688
+ print(f"\nupdated {args.policy} (and its .php twin). Promoted rules now return 403.")
689
+ return 0
690
+ return EX_USAGE
691
+
692
+
565
693
  def _integrity(args):
566
694
  from phpwarden import integrity as integ
567
695
  res = integ.check(args.path, locale=args.locale, plugins=not args.no_plugins)
@@ -674,6 +802,44 @@ def main(argv=None):
674
802
  _report_args(wpc)
675
803
  wpc.set_defaults(func=_wordpress)
676
804
 
805
+ tvi = sub.add_parser("trivy-import", help="read Trivy JSON reports (npm/JS, OS packages, images, IaC), "
806
+ "rank them by exploitation, optionally send them to the dashboard")
807
+ tvi.add_argument("files", nargs="+", metavar="TRIVY_JSON", help="output of trivy fs/image/config --format json")
808
+ tvi.add_argument("--kev", metavar="FILE", help="CISA KEV catalogue JSON")
809
+ tvi.add_argument("--epss", metavar="FILE", help="FIRST EPSS CSV (.csv or .csv.gz)")
810
+ tvi.add_argument("--json", action="store_true", help="machine-readable JSON output")
811
+ _report_args(tvi)
812
+ tvi.set_defaults(func=_trivy_import)
813
+
814
+ sh = sub.add_parser("shield", help="protection inside PHP: block exploits of the vulnerabilities this site "
815
+ "actually has (log-only until you promote rules)")
816
+ shs = sh.add_subparsers(dest="shield_cmd", required=True, parser_class=_Parser)
817
+ shp = shs.add_parser("policy", help="build a policy for the exploited vulnerabilities installed at PATH")
818
+ shp.add_argument("path", nargs="?", default=".", help="project or WordPress root (default: .)")
819
+ shp.add_argument("--out", help="policy file (default phpwarden-shield.policy.json; a .php twin is written too)")
820
+ shp.add_argument("--php-version", help="the production PHP version, for engine CVE rules")
821
+ shp.add_argument("--php-sapi", choices=["fpm", "cgi", "cli"], help="the production SAPI")
822
+ shp.add_argument("--log", help="where the agent should write its log (default: next to the agent)")
823
+ shp.add_argument("--no-limits", action="store_true",
824
+ help="leave out the rate limits (login, password reset, xmlrpc, burst guard)")
825
+ shp.add_argument("--limit-path", action="append", metavar="REGEX",
826
+ help="also rate-limit requests to this path (repeatable)")
827
+ shp.add_argument("--trust-proxy", action="append", metavar="CIDR",
828
+ help="trust forwarded client IPs only from this proxy range, e.g. your CDN (repeatable)")
829
+ shp.add_argument("--json", action="store_true", help="print the policy as JSON")
830
+ shi = shs.add_parser("install", help="show how to load the agent (and copy it with --dest)")
831
+ shi.add_argument("--dest", metavar="DIR", help="copy phpwarden-shield.php into this folder")
832
+ shr = shs.add_parser("report", help="summarise the agent's log")
833
+ shr.add_argument("log", help="phpwarden-shield.log")
834
+ shr.add_argument("--json", action="store_true", help="machine-readable JSON output")
835
+ shm = shs.add_parser("promote", help="switch rules from logging to blocking after a clean burn-in")
836
+ shm.add_argument("policy", help="the policy JSON to update")
837
+ shm.add_argument("--log", required=True, help="the agent log covering the burn-in")
838
+ shm.add_argument("--rule", action="append", metavar="ID", help="only this rule (repeatable)")
839
+ shm.add_argument("--i-understand-this-blocks-requests", action="store_true",
840
+ help="required: promoted rules return 403 to matching requests")
841
+ sh.set_defaults(func=_shield)
842
+
677
843
  asx = sub.add_parser("assess", help="one ranked verdict from every view: installed versions, your code, "
678
844
  "the live site from outside, and attacks in the access log")
679
845
  asx.add_argument("path", nargs="?", help="project or site root (composer.lock, WordPress, PHP code)")
@@ -686,6 +852,12 @@ def main(argv=None):
686
852
  asx.add_argument("--baseline", metavar="FILE",
687
853
  help="an earlier --out file: report what is fixed and verified, still open, and new")
688
854
  asx.add_argument("--json", action="store_true", help="machine-readable JSON output")
855
+ asx.add_argument("--trivy", metavar="FILE", action="append",
856
+ help="add a Trivy JSON report (npm/JS, OS packages, images, IaC); repeatable")
857
+ asx.add_argument("--run-trivy", action="store_true",
858
+ help="run `trivy fs` on the path if Trivy is installed (never downloads it)")
859
+ asx.add_argument("--kev", metavar="FILE", help="CISA KEV catalogue JSON, to rank non-PHP CVEs by exploitation")
860
+ asx.add_argument("--epss", metavar="FILE", help="FIRST EPSS CSV (.csv or .csv.gz) for non-PHP CVEs")
689
861
  asx.set_defaults(func=_assess)
690
862
 
691
863
  cd = sub.add_parser("code", help="scan your own PHP code for high-signal security bugs "