phpwarden 2.3.0__tar.gz → 2.5.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 (120) hide show
  1. {phpwarden-2.3.0 → phpwarden-2.5.0}/PKG-INFO +57 -21
  2. {phpwarden-2.3.0 → phpwarden-2.5.0}/README.md +54 -18
  3. {phpwarden-2.3.0 → phpwarden-2.5.0}/pyproject.toml +4 -4
  4. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/__init__.py +1 -1
  5. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/cli.py +104 -3
  6. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/probe.py +301 -11
  7. phpwarden-2.5.0/src/phpwarden/sast/dbflow.py +941 -0
  8. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/sast/engine.py +11 -4
  9. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/sast/taint.py +50 -3
  10. phpwarden-2.5.0/src/phpwarden/shield/EDGE.md +108 -0
  11. phpwarden-2.5.0/src/phpwarden/shield/README.md +98 -0
  12. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/shield/agent/phpwarden-shield.php +237 -7
  13. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/shield/policy.py +56 -10
  14. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/shield/report.py +19 -3
  15. phpwarden-2.5.0/src/phpwarden/thirdparty/__init__.py +1 -0
  16. phpwarden-2.5.0/src/phpwarden/thirdparty/trivy.py +188 -0
  17. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/verdict.py +56 -4
  18. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden.egg-info/PKG-INFO +57 -21
  19. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden.egg-info/SOURCES.txt +8 -0
  20. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_account_deletion.py +5 -2
  21. phpwarden-2.5.0/tests/test_probe_auth.py +308 -0
  22. phpwarden-2.5.0/tests/test_sast_dbflow.py +132 -0
  23. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_shield_dashboard.py +9 -0
  24. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_shield_runtime.py +155 -0
  25. phpwarden-2.5.0/tests/test_trivy_import.py +131 -0
  26. {phpwarden-2.3.0 → phpwarden-2.5.0}/LICENSE +0 -0
  27. {phpwarden-2.3.0 → phpwarden-2.5.0}/setup.cfg +0 -0
  28. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/code.py +0 -0
  29. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/code_rules.py +0 -0
  30. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/data/php-cve-feed.json +0 -0
  31. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/deps.py +0 -0
  32. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/detect.py +0 -0
  33. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/engine.py +0 -0
  34. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/env.py +0 -0
  35. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/feed.py +0 -0
  36. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/guard.py +0 -0
  37. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/integrity.py +0 -0
  38. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/report.py +0 -0
  39. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/rules.py +0 -0
  40. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/sarif.py +0 -0
  41. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/sast/__init__.py +0 -0
  42. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/sast/ast.py +0 -0
  43. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/sast/authz.py +0 -0
  44. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/sast/callgraph.py +0 -0
  45. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/sast/lexer.py +0 -0
  46. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/sast/models/__init__.py +0 -0
  47. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/sast/models/laravel.py +0 -0
  48. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/sast/models/php_builtins.py +0 -0
  49. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/sast/models/symfony.py +0 -0
  50. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/sast/models/wordpress.py +0 -0
  51. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/sast/parser.py +0 -0
  52. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/sast/project.py +0 -0
  53. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/sast/symbols.py +0 -0
  54. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/sast/types.py +0 -0
  55. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/shield/__init__.py +0 -0
  56. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/shield/library.py +0 -0
  57. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/shield/matcher.py +0 -0
  58. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/version.py +0 -0
  59. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/vulndb.py +0 -0
  60. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden/wordpress.py +0 -0
  61. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden.egg-info/dependency_links.txt +0 -0
  62. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden.egg-info/entry_points.txt +0 -0
  63. {phpwarden-2.3.0 → phpwarden-2.5.0}/src/phpwarden.egg-info/top_level.txt +0 -0
  64. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_accounts.py +0 -0
  65. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_advisories.py +0 -0
  66. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_agency.py +0 -0
  67. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_assess.py +0 -0
  68. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_assignment.py +0 -0
  69. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_audit.py +0 -0
  70. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_billing.py +0 -0
  71. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_business_metrics.py +0 -0
  72. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_cli_edges.py +0 -0
  73. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_code.py +0 -0
  74. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_connect_flow.py +0 -0
  75. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_dashboard_states.py +0 -0
  76. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_deps_and_rules.py +0 -0
  77. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_detection_validation.py +0 -0
  78. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_developer.py +0 -0
  79. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_discussion.py +0 -0
  80. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_e2e_readiness.py +0 -0
  81. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_email.py +0 -0
  82. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_engine.py +0 -0
  83. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_engine_feed.py +0 -0
  84. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_feed_license.py +0 -0
  85. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_feed_subscription.py +0 -0
  86. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_fleet.py +0 -0
  87. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_free_feed.py +0 -0
  88. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_google_oauth.py +0 -0
  89. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_guard_live.py +0 -0
  90. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_integrity.py +0 -0
  91. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_kev_alert.py +0 -0
  92. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_kev_gaps.py +0 -0
  93. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_livefeed.py +0 -0
  94. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_mentions.py +0 -0
  95. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_probe.py +0 -0
  96. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_probe_cli.py +0 -0
  97. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_probe_dashboard.py +0 -0
  98. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_reachability.py +0 -0
  99. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_report_proof.py +0 -0
  100. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_saas.py +0 -0
  101. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_saas_billing.py +0 -0
  102. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_sarif.py +0 -0
  103. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_sast_layer5.py +0 -0
  104. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_sast_models.py +0 -0
  105. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_sast_parser.py +0 -0
  106. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_sast_project.py +0 -0
  107. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_sast_taint.py +0 -0
  108. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_server.py +0 -0
  109. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_settings.py +0 -0
  110. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_shield.py +0 -0
  111. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_shield_cli.py +0 -0
  112. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_shield_plan.py +0 -0
  113. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_slack.py +0 -0
  114. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_sso.py +0 -0
  115. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_store_connections.py +0 -0
  116. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_team.py +0 -0
  117. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_triage.py +0 -0
  118. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_waf_dryrun.py +0 -0
  119. {phpwarden-2.3.0 → phpwarden-2.5.0}/tests/test_webhook_idempotency.py +0 -0
  120. {phpwarden-2.3.0 → phpwarden-2.5.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.3.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. Offline, zero dependencies.
3
+ Version: 2.5.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
@@ -141,10 +142,44 @@ files / high or critical `code` and `probe` findings (`probe` also on any
141
142
  known-exploited finding) / anything to act on now or this week (`assess`),
142
143
  `2` possibly exposed (`scan`), `3` PHP version unknown (`scan`), `4` input
143
144
  error, `5` `--report-to` upload failed on an otherwise clean run, `64` usage
144
- error. Full table: `phpwarden --help`.
145
-
146
-
147
- ## New in 2.3.0 and 2.2
145
+ error. `1` also means `feed update` failed. Full table: `phpwarden --help`.
146
+
147
+
148
+ ## New in 2.5.0, 2.4.0, 2.3.0 and 2.2
149
+
150
+ - **2.5.0: live check behind a login.** On a site that has proved ownership,
151
+ `probe --cookie` / `--cookie-file` / `--header` (or a saved test-account login
152
+ on the dashboard, one same-origin login POST) repeats a few read-only requests
153
+ signed in: on WordPress, `/wp-admin/` (version) and `plugins.php` (plugin
154
+ versions). Logout, delete and nonce URLs are never requested; credential values
155
+ are never printed or logged, are stored encrypted (AES-256-GCM) and are deleted
156
+ with the project. Use a low-privilege test account. [Docs](https://phpwarden.io/docs#probe-auth).
157
+ - **2.5.0: PhpStorm plugin** in `integrations/phpstorm-plugin`, at parity with
158
+ the VS Code extension. Not on JetBrains Marketplace yet: build it and install
159
+ from disk.
160
+ - **2.5.0: `phpwarden code` resolves table names fixed in the code** (constants,
161
+ `define()`, Laravel `config()`, Symfony parameters, concatenation, `$wpdb->prefix`
162
+ and core table properties, Eloquent `$table`), closing the dynamic-table-name
163
+ limit of 2.4.0. Benchmark results are unchanged.
164
+
165
+ - **2.4.0: `phpwarden code` follows data through the database and callbacks.**
166
+ Request data saved with raw SQL, `$wpdb`, Eloquent, the query builder or
167
+ Doctrine and read back from the same table is traced to where it is printed
168
+ or queried (stored XSS, second-order SQL injection), and callbacks passed to
169
+ `array_map`, `usort`, `preg_replace_callback`, `add_filter` and the like,
170
+ `$f()` and first-class callables are followed. DVWA: 8/8/7 of 8 types under
171
+ the comparison's scorer (was 7/7/6); clean-code counts unchanged.
172
+ - **2.4.0: shield rate limits.** Per-client limits on WordPress login, password
173
+ reset, xmlrpc and `system.multicall`, Laravel login and reset, component
174
+ writes, and a burst guard. Log-only until promoted, then 429; APCu or file
175
+ store; fails open. `--trust-proxy CIDR`, `--no-limits`, `--limit-path REGEX`.
176
+ 0 trips on 6,000 ordinary requests; 6 of 6 abuse scenarios caught. Pairing
177
+ with Cloudflare or nginx: [EDGE.md](src/phpwarden/shield/EDGE.md).
178
+ - **2.4.0: `phpwarden trivy-import` and `assess --trivy FILE` / `--run-trivy`.**
179
+ Trivy's JSON (npm/JS, OS packages, images, IaC, secrets) in the same ranked
180
+ list as PHP findings: KEV, then EPSS (`--kev`, `--epss`), then severity.
181
+ Secrets by file and line only. Never downloads Trivy. See
182
+ [src/phpwarden/thirdparty/README.md](src/phpwarden/thirdparty/README.md).
148
183
 
149
184
  - **`phpwarden probe URL`**: a live check of a running site using GET and HEAD
150
185
  requests only. It looks for exposed files (`.env`, `.git`, backups), open debug
@@ -158,14 +193,13 @@ error. Full table: `phpwarden --help`.
158
193
  includes, redirects and HTTP clients, with a trace per finding (SARIF
159
194
  `codeFlows`). Models Laravel, Symfony and WordPress: routes with no auth check,
160
195
  mass assignment, raw Blade/Twig output, IDOR hints, CSRF exclusions, nonces.
161
- Follows data stored in the session or WordPress options (second-order).
162
- 89 rules. On the same pinned apps it found more than Semgrep and Psalm with far
196
+ Follows data stored in the session or WordPress options, and (2.4.0; names resolved in 2.5.0) data saved to database tables (second-order), matched by table name: literal names, constants, Laravel config / Symfony parameters, `$wpdb->prefix` + name, `$wpdb->posts` etc. and Eloquent `$table` are resolved; table names built from run-time data are not matched.
197
+ 89 rules. In our published comparison (2026-11-15) it found more than Semgrep, Psalm and Snyk Code, with far
163
198
  fewer findings on clean framework code ([comparison](https://github.com/oroma-alfred/phpwarden/tree/main/bench/sast-compare-2026-11)).
164
- Data that round-trips through the database is not followed.
165
199
  - **`phpwarden shield`**: one PHP file loaded before your app that blocks exploit
166
200
  attempts against the exploited vulnerabilities this site actually has, scoped
167
201
  to their entry points. Logs until you promote rules (7-day burn-in, explicit
168
- flag), fails open, has a kill switch, about 0.07 ms per request. Not an edge
202
+ flag), fails open, has a kill switch, about 0.1 ms per request in our benchmark. Not an edge
169
203
  WAF: no DDoS, bot or TLS handling.
170
204
  - **`phpwarden assess`**: one ranked list from every view of a site: installed
171
205
  versions, your code, the live site from outside, and the access log, with the
@@ -180,9 +214,11 @@ error. Full table: `phpwarden --help`.
180
214
  - **Coverage commitment**: in-scope CISA KEV entries are in the maintained feed
181
215
  within 24 hours of listing, checked on every rebuild and published at
182
216
  https://phpwarden.io/feeds/kev-status.json. Details: https://phpwarden.io/sla
183
- - **Editors**: a VS Code extension in `integrations/vscode` (build and install
184
- the `.vsix` from that folder; not on the Marketplace yet) and PhpStorm External Tool steps in
185
- `integrations/phpstorm`.
217
+ - **Editors**: a VS Code extension on the
218
+ [Marketplace](https://marketplace.visualstudio.com/items?itemName=phpwarden.phpwarden)
219
+ (`code --install-extension phpwarden.phpwarden`; source in `integrations/vscode`) and a PhpStorm plugin
220
+ in `integrations/phpstorm-plugin` (not on JetBrains Marketplace yet: build it and
221
+ use Install Plugin from Disk; External Tool steps in `integrations/phpstorm`).
186
222
 
187
223
  ## Replacing Wordfence CLI
188
224
 
@@ -248,7 +284,7 @@ see [phpwarden/laravel](https://packagist.org/packages/phpwarden/laravel).
248
284
  ```yaml
249
285
  repos:
250
286
  - repo: https://github.com/oroma-alfred/phpwarden
251
- rev: v2.3.0 # pin to a tag with `pre-commit autoupdate`
287
+ rev: v2.5.0 # pin to a tag with `pre-commit autoupdate`
252
288
  hooks:
253
289
  - id: phpwarden-deps
254
290
  # args: ["--no-reachability"] # faster: skip the reachability pass
@@ -259,14 +295,14 @@ timer recipes are in [`docs/recipes.md`](docs/recipes.md).
259
295
 
260
296
  ## The feed
261
297
 
262
- The bundled community feed (2026.11.13.3 in this release) works forever, offline:
298
+ The bundled community feed (2026.11.14.2 in this release) works forever, offline:
263
299
 
264
300
  - **~21,300 advisories**: ~8,800 Composer (from [OSV.dev](https://osv.dev) and
265
301
  [FriendsOfPHP](https://github.com/FriendsOfPHP/security-advisories)),
266
302
  ~12,300 WordPress (from [NVD](https://nvd.nist.gov): plugin, core and theme),
267
- and 145 PHP engine/FPM CVEs, including hand-curated entries with per-branch
268
- fixed versions, exposure conditions and detection signatures. About 19,700 carry an
269
- EPSS score; 41 distinct vulnerabilities were in KEV when this feed was built.
303
+ and 146 PHP engine/FPM CVEs, including hand-curated entries with per-branch
304
+ fixed versions, exposure conditions and detection signatures. About 18,200 distinct vulnerabilities carry an
305
+ EPSS score; 54 distinct vulnerabilities were in KEV when this feed was built.
270
306
  - **License-independent by construction.** Every source is open or
271
307
  public-domain (OSV, NVD, FriendsOfPHP, php-src release notes, CISA KEV, FIRST EPSS). No commercial
272
308
  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
@@ -119,10 +120,44 @@ files / high or critical `code` and `probe` findings (`probe` also on any
119
120
  known-exploited finding) / anything to act on now or this week (`assess`),
120
121
  `2` possibly exposed (`scan`), `3` PHP version unknown (`scan`), `4` input
121
122
  error, `5` `--report-to` upload failed on an otherwise clean run, `64` usage
122
- error. Full table: `phpwarden --help`.
123
-
124
-
125
- ## New in 2.3.0 and 2.2
123
+ error. `1` also means `feed update` failed. Full table: `phpwarden --help`.
124
+
125
+
126
+ ## New in 2.5.0, 2.4.0, 2.3.0 and 2.2
127
+
128
+ - **2.5.0: live check behind a login.** On a site that has proved ownership,
129
+ `probe --cookie` / `--cookie-file` / `--header` (or a saved test-account login
130
+ on the dashboard, one same-origin login POST) repeats a few read-only requests
131
+ signed in: on WordPress, `/wp-admin/` (version) and `plugins.php` (plugin
132
+ versions). Logout, delete and nonce URLs are never requested; credential values
133
+ are never printed or logged, are stored encrypted (AES-256-GCM) and are deleted
134
+ with the project. Use a low-privilege test account. [Docs](https://phpwarden.io/docs#probe-auth).
135
+ - **2.5.0: PhpStorm plugin** in `integrations/phpstorm-plugin`, at parity with
136
+ the VS Code extension. Not on JetBrains Marketplace yet: build it and install
137
+ from disk.
138
+ - **2.5.0: `phpwarden code` resolves table names fixed in the code** (constants,
139
+ `define()`, Laravel `config()`, Symfony parameters, concatenation, `$wpdb->prefix`
140
+ and core table properties, Eloquent `$table`), closing the dynamic-table-name
141
+ limit of 2.4.0. Benchmark results are unchanged.
142
+
143
+ - **2.4.0: `phpwarden code` follows data through the database and callbacks.**
144
+ Request data saved with raw SQL, `$wpdb`, Eloquent, the query builder or
145
+ Doctrine and read back from the same table is traced to where it is printed
146
+ or queried (stored XSS, second-order SQL injection), and callbacks passed to
147
+ `array_map`, `usort`, `preg_replace_callback`, `add_filter` and the like,
148
+ `$f()` and first-class callables are followed. DVWA: 8/8/7 of 8 types under
149
+ the comparison's scorer (was 7/7/6); clean-code counts unchanged.
150
+ - **2.4.0: shield rate limits.** Per-client limits on WordPress login, password
151
+ reset, xmlrpc and `system.multicall`, Laravel login and reset, component
152
+ writes, and a burst guard. Log-only until promoted, then 429; APCu or file
153
+ store; fails open. `--trust-proxy CIDR`, `--no-limits`, `--limit-path REGEX`.
154
+ 0 trips on 6,000 ordinary requests; 6 of 6 abuse scenarios caught. Pairing
155
+ with Cloudflare or nginx: [EDGE.md](src/phpwarden/shield/EDGE.md).
156
+ - **2.4.0: `phpwarden trivy-import` and `assess --trivy FILE` / `--run-trivy`.**
157
+ Trivy's JSON (npm/JS, OS packages, images, IaC, secrets) in the same ranked
158
+ list as PHP findings: KEV, then EPSS (`--kev`, `--epss`), then severity.
159
+ Secrets by file and line only. Never downloads Trivy. See
160
+ [src/phpwarden/thirdparty/README.md](src/phpwarden/thirdparty/README.md).
126
161
 
127
162
  - **`phpwarden probe URL`**: a live check of a running site using GET and HEAD
128
163
  requests only. It looks for exposed files (`.env`, `.git`, backups), open debug
@@ -136,14 +171,13 @@ error. Full table: `phpwarden --help`.
136
171
  includes, redirects and HTTP clients, with a trace per finding (SARIF
137
172
  `codeFlows`). Models Laravel, Symfony and WordPress: routes with no auth check,
138
173
  mass assignment, raw Blade/Twig output, IDOR hints, CSRF exclusions, nonces.
139
- Follows data stored in the session or WordPress options (second-order).
140
- 89 rules. On the same pinned apps it found more than Semgrep and Psalm with far
174
+ Follows data stored in the session or WordPress options, and (2.4.0; names resolved in 2.5.0) data saved to database tables (second-order), matched by table name: literal names, constants, Laravel config / Symfony parameters, `$wpdb->prefix` + name, `$wpdb->posts` etc. and Eloquent `$table` are resolved; table names built from run-time data are not matched.
175
+ 89 rules. In our published comparison (2026-11-15) it found more than Semgrep, Psalm and Snyk Code, with far
141
176
  fewer findings on clean framework code ([comparison](https://github.com/oroma-alfred/phpwarden/tree/main/bench/sast-compare-2026-11)).
142
- Data that round-trips through the database is not followed.
143
177
  - **`phpwarden shield`**: one PHP file loaded before your app that blocks exploit
144
178
  attempts against the exploited vulnerabilities this site actually has, scoped
145
179
  to their entry points. Logs until you promote rules (7-day burn-in, explicit
146
- flag), fails open, has a kill switch, about 0.07 ms per request. Not an edge
180
+ flag), fails open, has a kill switch, about 0.1 ms per request in our benchmark. Not an edge
147
181
  WAF: no DDoS, bot or TLS handling.
148
182
  - **`phpwarden assess`**: one ranked list from every view of a site: installed
149
183
  versions, your code, the live site from outside, and the access log, with the
@@ -158,9 +192,11 @@ error. Full table: `phpwarden --help`.
158
192
  - **Coverage commitment**: in-scope CISA KEV entries are in the maintained feed
159
193
  within 24 hours of listing, checked on every rebuild and published at
160
194
  https://phpwarden.io/feeds/kev-status.json. Details: https://phpwarden.io/sla
161
- - **Editors**: a VS Code extension in `integrations/vscode` (build and install
162
- the `.vsix` from that folder; not on the Marketplace yet) and PhpStorm External Tool steps in
163
- `integrations/phpstorm`.
195
+ - **Editors**: a VS Code extension on the
196
+ [Marketplace](https://marketplace.visualstudio.com/items?itemName=phpwarden.phpwarden)
197
+ (`code --install-extension phpwarden.phpwarden`; source in `integrations/vscode`) and a PhpStorm plugin
198
+ in `integrations/phpstorm-plugin` (not on JetBrains Marketplace yet: build it and
199
+ use Install Plugin from Disk; External Tool steps in `integrations/phpstorm`).
164
200
 
165
201
  ## Replacing Wordfence CLI
166
202
 
@@ -226,7 +262,7 @@ see [phpwarden/laravel](https://packagist.org/packages/phpwarden/laravel).
226
262
  ```yaml
227
263
  repos:
228
264
  - repo: https://github.com/oroma-alfred/phpwarden
229
- rev: v2.3.0 # pin to a tag with `pre-commit autoupdate`
265
+ rev: v2.5.0 # pin to a tag with `pre-commit autoupdate`
230
266
  hooks:
231
267
  - id: phpwarden-deps
232
268
  # args: ["--no-reachability"] # faster: skip the reachability pass
@@ -237,14 +273,14 @@ timer recipes are in [`docs/recipes.md`](docs/recipes.md).
237
273
 
238
274
  ## The feed
239
275
 
240
- The bundled community feed (2026.11.13.3 in this release) works forever, offline:
276
+ The bundled community feed (2026.11.14.2 in this release) works forever, offline:
241
277
 
242
278
  - **~21,300 advisories**: ~8,800 Composer (from [OSV.dev](https://osv.dev) and
243
279
  [FriendsOfPHP](https://github.com/FriendsOfPHP/security-advisories)),
244
280
  ~12,300 WordPress (from [NVD](https://nvd.nist.gov): plugin, core and theme),
245
- and 145 PHP engine/FPM CVEs, including hand-curated entries with per-branch
246
- fixed versions, exposure conditions and detection signatures. About 19,700 carry an
247
- EPSS score; 41 distinct vulnerabilities were in KEV when this feed was built.
281
+ and 146 PHP engine/FPM CVEs, including hand-curated entries with per-branch
282
+ fixed versions, exposure conditions and detection signatures. About 18,200 distinct vulnerabilities carry an
283
+ EPSS score; 54 distinct vulnerabilities were in KEV when this feed was built.
248
284
  - **License-independent by construction.** Every source is open or
249
285
  public-domain (OSV, NVD, FriendsOfPHP, php-src release notes, CISA KEV, FIRST EPSS). No commercial
250
286
  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.3.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. Offline, zero dependencies."
7
+ version = "2.5.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", "shield/agent/*.php"]
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.3.0"
9
+ __version__ = "2.5.0"
@@ -455,8 +455,32 @@ def _probe(args):
455
455
  f" 3. A tag in the homepage <head>:\n <meta name=\"phpwarden-verify\" content=\"{token}\">\n\n"
456
456
  f"Without it, probe only reads the homepage response (passive mode).")
457
457
  return 0
458
+ auth = None
459
+ if args.cookie or args.cookie_file or args.header:
460
+ if args.passive:
461
+ sys.stderr.write("error: --cookie/--cookie-file need the active checks; drop --passive\n")
462
+ return 4
463
+ try:
464
+ raw = list(args.cookie or [])
465
+ if args.cookie_file:
466
+ with open(args.cookie_file, encoding="utf-8") as fh:
467
+ raw.append(fh.read())
468
+ cookies = {}
469
+ for r in raw:
470
+ cookies.update(pr.parse_cookies(r))
471
+ headers = {}
472
+ for hv in args.header or []:
473
+ k, sep, v = hv.partition(":")
474
+ if not sep:
475
+ raise ValueError("header must be 'Name: value'")
476
+ headers[k.strip()] = v.strip()
477
+ auth = pr.Auth(cookies=cookies, headers=headers)
478
+ except (OSError, ValueError) as e: # messages never contain the cookie value
479
+ sys.stderr.write(f"error: could not read the cookie: {e.__class__.__name__}"
480
+ f"{'' if isinstance(e, OSError) else ': ' + str(e)}\n")
481
+ return 4
458
482
  res = pr.run_probe(url, token=token, active=False if args.passive else None,
459
- max_requests=args.max_requests)
483
+ max_requests=args.max_requests, auth=auth)
460
484
  if args.json:
461
485
  print(json.dumps(res, indent=2))
462
486
  elif _fmt(args) == "sarif":
@@ -546,8 +570,29 @@ def _assess(args):
546
570
  if any("homepage unreachable" in n for n in probe_result.get("notes", [])):
547
571
  sys.stderr.write(f"error: could not reach {url}\n")
548
572
  return 4
573
+ trivy_fs, trivy_note = None, None
574
+ if args.trivy or args.run_trivy:
575
+ from phpwarden.thirdparty import trivy as tv
576
+ try:
577
+ kev, epss = tv.load_kev(args.kev), tv.load_epss(args.epss)
578
+ docs = [tv.load_report(f) for f in (args.trivy or [])]
579
+ except (OSError, ValueError) as e:
580
+ sys.stderr.write(f"error: {e}\n")
581
+ return 4
582
+ if args.run_trivy and args.path:
583
+ doc, msg = tv.run_trivy_fs(args.path)
584
+ if doc is not None:
585
+ docs.append(doc)
586
+ else:
587
+ trivy_note = msg
588
+ sys.stderr.write(msg + "\n")
589
+ trivy_fs = [f for d in docs for f in tv.findings(d, kev, epss)]
590
+ if docs and not (args.kev or args.epss):
591
+ trivy_note = (trivy_note + "; " if trivy_note else "") + \
592
+ "non-PHP findings ranked by severity only (pass --kev and --epss for exploitation data)"
549
593
  try:
550
- res = vd.assess(args.path, probe_result=probe_result, logfile=args.log, run_code=not args.no_code)
594
+ res = vd.assess(args.path, probe_result=probe_result, logfile=args.log, run_code=not args.no_code,
595
+ trivy_findings=trivy_fs, trivy_note=trivy_note)
551
596
  except OSError as e: # unreadable access log
552
597
  sys.stderr.write(f"error: {e}\n")
553
598
  return 4
@@ -563,6 +608,31 @@ def _assess(args):
563
608
  return vd.exit_code(res)
564
609
 
565
610
 
611
+ def _trivy_import(args):
612
+ from phpwarden.thirdparty import trivy as tv
613
+ try:
614
+ kev, epss = tv.load_kev(args.kev), tv.load_epss(args.epss)
615
+ fs = [f for p in args.files for f in tv.findings(tv.load_report(p), kev, epss)]
616
+ except (OSError, ValueError) as e:
617
+ sys.stderr.write(f"error: {e}\n")
618
+ return 4
619
+ doc = tv.to_report_doc(fs, args.files)
620
+ if args.json:
621
+ print(json.dumps(doc, indent=2))
622
+ else:
623
+ kev_n = sum(1 for f in fs if f["known_exploited"])
624
+ print(f"{len(fs)} finding(s) from {len(args.files)} Trivy report(s); {kev_n} known exploited. "
625
+ f"Ranked by exploitation{'' if (args.kev or args.epss) else ' (no --kev/--epss: by severity)'}:")
626
+ for f in fs[:50]:
627
+ tag = " [KEV]" if f["known_exploited"] else (f" [EPSS {f['epss'] * 100:.0f}%]" if f.get("epss") else "")
628
+ dev = " (dev)" if f["dev"] else ""
629
+ print(f" {f['severity']:<8} {tv.ident(f)}{tag}{dev} {f['title'][:90]}")
630
+ if len(fs) > 50:
631
+ print(f" ... and {len(fs) - 50} more; use --json for all")
632
+ rc = 1 if any(f["known_exploited"] or f["severity"] in ("critical", "high") for f in fs) else 0
633
+ return _combine(rc, _maybe_report(args, doc))
634
+
635
+
566
636
  def _shield(args):
567
637
  from pathlib import Path as _P
568
638
  from phpwarden.shield import policy as sp, report as sr
@@ -572,7 +642,9 @@ def _shield(args):
572
642
  sys.stderr.write(f"error: not a directory: {args.path}\n")
573
643
  return 4
574
644
  pol = sp.build_policy_for_path(str(root), FeedManager().current(), php_version=args.php_version,
575
- php_sapi=args.php_sapi, log_path=args.log)
645
+ php_sapi=args.php_sapi, log_path=args.log,
646
+ limits=not args.no_limits, limit_paths=args.limit_path or [],
647
+ trust_proxy=args.trust_proxy or [])
576
648
  out = args.out or "phpwarden-shield.policy.json"
577
649
  j, php = sp.write_policy(pol, out)
578
650
  if args.json:
@@ -754,6 +826,15 @@ def main(argv=None):
754
826
  _report_args(wpc)
755
827
  wpc.set_defaults(func=_wordpress)
756
828
 
829
+ tvi = sub.add_parser("trivy-import", help="read Trivy JSON reports (npm/JS, OS packages, images, IaC), "
830
+ "rank them by exploitation, optionally send them to the dashboard")
831
+ tvi.add_argument("files", nargs="+", metavar="TRIVY_JSON", help="output of trivy fs/image/config --format json")
832
+ tvi.add_argument("--kev", metavar="FILE", help="CISA KEV catalogue JSON")
833
+ tvi.add_argument("--epss", metavar="FILE", help="FIRST EPSS CSV (.csv or .csv.gz)")
834
+ tvi.add_argument("--json", action="store_true", help="machine-readable JSON output")
835
+ _report_args(tvi)
836
+ tvi.set_defaults(func=_trivy_import)
837
+
757
838
  sh = sub.add_parser("shield", help="protection inside PHP: block exploits of the vulnerabilities this site "
758
839
  "actually has (log-only until you promote rules)")
759
840
  shs = sh.add_subparsers(dest="shield_cmd", required=True, parser_class=_Parser)
@@ -763,6 +844,12 @@ def main(argv=None):
763
844
  shp.add_argument("--php-version", help="the production PHP version, for engine CVE rules")
764
845
  shp.add_argument("--php-sapi", choices=["fpm", "cgi", "cli"], help="the production SAPI")
765
846
  shp.add_argument("--log", help="where the agent should write its log (default: next to the agent)")
847
+ shp.add_argument("--no-limits", action="store_true",
848
+ help="leave out the rate limits (login, password reset, xmlrpc, burst guard)")
849
+ shp.add_argument("--limit-path", action="append", metavar="REGEX",
850
+ help="also rate-limit requests to this path (repeatable)")
851
+ shp.add_argument("--trust-proxy", action="append", metavar="CIDR",
852
+ help="trust forwarded client IPs only from this proxy range, e.g. your CDN (repeatable)")
766
853
  shp.add_argument("--json", action="store_true", help="print the policy as JSON")
767
854
  shi = shs.add_parser("install", help="show how to load the agent (and copy it with --dest)")
768
855
  shi.add_argument("--dest", metavar="DIR", help="copy phpwarden-shield.php into this folder")
@@ -789,6 +876,12 @@ def main(argv=None):
789
876
  asx.add_argument("--baseline", metavar="FILE",
790
877
  help="an earlier --out file: report what is fixed and verified, still open, and new")
791
878
  asx.add_argument("--json", action="store_true", help="machine-readable JSON output")
879
+ asx.add_argument("--trivy", metavar="FILE", action="append",
880
+ help="add a Trivy JSON report (npm/JS, OS packages, images, IaC); repeatable")
881
+ asx.add_argument("--run-trivy", action="store_true",
882
+ help="run `trivy fs` on the path if Trivy is installed (never downloads it)")
883
+ asx.add_argument("--kev", metavar="FILE", help="CISA KEV catalogue JSON, to rank non-PHP CVEs by exploitation")
884
+ asx.add_argument("--epss", metavar="FILE", help="FIRST EPSS CSV (.csv or .csv.gz) for non-PHP CVEs")
792
885
  asx.set_defaults(func=_assess)
793
886
 
794
887
  cd = sub.add_parser("code", help="scan your own PHP code for high-signal security bugs "
@@ -814,6 +907,14 @@ def main(argv=None):
814
907
  help="show how to verify ownership of the site, then exit")
815
908
  pb.add_argument("--passive", action="store_true", help="read the homepage response only")
816
909
  pb.add_argument("--max-requests", type=int, default=60, help="request budget (default 60)")
910
+ pb.add_argument("--cookie", action="append", metavar="NAME=VALUE",
911
+ help="session cookie for the signed-in check (repeatable; used only after ownership is "
912
+ "verified, only on that site, GET/HEAD only; never printed). Use a low-privilege test account")
913
+ pb.add_argument("--cookie-file", metavar="FILE",
914
+ help="read cookies from FILE (name=value lines or a Netscape cookies.txt); keeps them out of "
915
+ "shell history")
916
+ pb.add_argument("--header", action="append", metavar="'NAME: VALUE'",
917
+ help="extra header sent with the signed-in requests (repeatable; never printed)")
817
918
  pb.add_argument("--json", action="store_true", help="machine-readable JSON output")
818
919
  _format_arg(pb)
819
920
  pb.set_defaults(func=_probe)