penthera 1.8.0 → 1.9.0-rc.1
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.
- package/CHANGELOG.md +272 -0
- package/README.md +21 -4
- package/lib/advisories/evidence.js +22 -8
- package/lib/advisories/index.js +35 -7
- package/lib/advisories/products.js +30 -3
- package/lib/advisories/versions.js +35 -3
- package/lib/ai/consent.js +213 -0
- package/lib/ai/hunt.js +233 -0
- package/lib/ai/index.js +86 -0
- package/lib/ai/ollama.js +102 -0
- package/lib/ai/units.js +139 -0
- package/lib/blackbox/challenge.js +33 -0
- package/lib/blackbox/headers.js +24 -0
- package/lib/blackbox/openapi.js +17 -3
- package/lib/coverage.js +215 -0
- package/lib/deps/capabilities.js +1 -1
- package/lib/deps/findings.js +11 -6
- package/lib/deps/index.js +2 -0
- package/lib/deps/iocs.js +15 -10
- package/lib/deps/python/capabilities.js +45 -7
- package/lib/deps/python/index.js +4 -1
- package/lib/hook/index.js +12 -2
- package/lib/install/policy.js +5 -3
- package/lib/llm-audit/CLOUDFLARE-LICENSE +25 -0
- package/lib/llm-audit/classes.js +93 -0
- package/lib/llm-audit/cloudflare-report-schema.json +461 -0
- package/lib/llm-audit/cloudflare.js +167 -0
- package/lib/llm-audit/export.js +145 -0
- package/lib/llm-audit/verify.js +205 -0
- package/lib/owasp-wstg.js +6 -3
- package/lib/references.json +22 -0
- package/lib/rules/advisories/advisories.json +62 -0
- package/lib/rules/advisories/eol.json +98 -0
- package/lib/rules/advisories/pack.json +4 -4
- package/lib/rules/advisories/pack.json.sig +2 -2
- package/lib/rules/ioc-pypi-2026/files.json +19 -0
- package/lib/rules/ioc-pypi-2026/hosts.json +19 -0
- package/lib/rules/ioc-pypi-2026/pack.json +25 -0
- package/lib/rules/ioc-pypi-2026/pack.json.sig +6 -0
- package/lib/rules/ioc-pypi-2026/packages.json +17 -0
- package/lib/rules/pack.js +18 -2
- package/lib/templates.js +112 -1
- package/lib/tls.js +61 -0
- package/lib/whitebox/code-sinks.js +267 -0
- package/lib/whitebox/secrets.js +20 -0
- package/package.json +5 -2
- package/skills/penthera/SKILL.md +8 -7
- package/skills/penthera/references/profiles-and-flags.md +3 -3
- package/skills/penthera/references/troubleshooting.md +2 -2
- package/skills/penthera/scripts/preflight.sh +27 -10
- package/src/agent/loop.js +4 -1
- package/src/cli/ai.js +177 -0
- package/src/cli/install.js +3 -2
- package/src/cli/mcp.js +85 -0
- package/src/cli/merge-results.js +1 -0
- package/src/cli/profiles.js +3 -3
- package/src/cli/prompt.js +31 -6
- package/src/cli/ui.js +20 -5
- package/src/cli/verify.js +197 -0
- package/src/cli.js +115 -4
- package/src/mcp/results.js +153 -0
- package/src/mcp/server.js +271 -0
- package/src/mcp/tools.js +285 -0
- package/src/mcp/validate.js +57 -0
- package/src/mcp/workspace.js +112 -0
- package/src/report/markdown.js +68 -1
- package/src/reporter.js +68 -6
- package/src/scan-repo.js +58 -38
- package/src/scan-url.js +19 -2
- package/src/ui/api/routes.js +2 -0
- package/src/ui/api/runs.js +107 -23
- package/src/ui/server.js +6 -1
- package/src/ui/static/app.css +18 -1
- package/src/ui/static/js/center.js +34 -3
- package/src/ui/static/js/copy.js +42 -0
- package/src/ui/static/js/dialog.js +108 -3
- package/src/ui/static/js/format.js +2 -0
- package/src/ui/static/js/right.js +10 -3
- package/src/utils/url.js +30 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,278 @@ All notable changes to Penthera are documented here. Versioning follows [SemVer]
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.9.0-rc.1] - 2026-10-22
|
|
8
|
+
|
|
9
|
+
Release candidate for 1.9.0, on npm's `next` dist-tag (`npm install penthera@next`).
|
|
10
|
+
New: `penthera mcp` (an MCP server for coding agents, local and read-only by
|
|
11
|
+
default), opt-in AI mode where a local model proposes and Penthera's rules
|
|
12
|
+
verify, new detection rules, and default scans that send no attack payloads.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Coverage ledger** in every `--repo` report (terminal, Markdown, JSON
|
|
17
|
+
`coverage`): which files each check read, which it did not and why, and the
|
|
18
|
+
source files no code analysis looked at. It reads only file names and sizes,
|
|
19
|
+
and is the first step toward an opt-in AI mode.
|
|
20
|
+
- **AI mode, opt-in and experimental: `penthera --repo . --ai`.** A model in
|
|
21
|
+
your local Ollama reads your code part by part and proposes problems no rule
|
|
22
|
+
describes; Penthera checks each proposal with its rules. Confirmed ones mark
|
|
23
|
+
the rule finding "also proposed by AI"; the rest are shown as
|
|
24
|
+
"model-proposed, unverified", apart from rule findings, and never change the
|
|
25
|
+
exit code. Every run asks first (default no), on stderr; runs that cannot ask
|
|
26
|
+
need `--ai-consent local` or `"ai": { "allow": ["local"] }` in
|
|
27
|
+
`penthera.policy.json`, or stop with exit 2 before scanning. Ollama cloud
|
|
28
|
+
models (`-cloud`) are recognised and need a second screen where you type
|
|
29
|
+
`ollama.com`. Secrets are masked before any code reaches the model; budgets
|
|
30
|
+
(`--ai-budget`) are hard stops; consents are logged locally
|
|
31
|
+
(`penthera ai consent`). `penthera ai models` lists models and where they
|
|
32
|
+
run. See [docs/ai-mode.md](docs/ai-mode.md).
|
|
33
|
+
- **AI mode in the dashboard.** New scan has an AI mode switch, off; turning
|
|
34
|
+
it on opens the consent sheet on every scan (a picture of where the code
|
|
35
|
+
goes, the model picker, the terminal's exact consent lines, two equal
|
|
36
|
+
buttons; cloud models need `ollama.com` typed). The server checks the
|
|
37
|
+
model's route with Ollama itself and logs the consent. Runs show an
|
|
38
|
+
"AI · local" tag, unverified proposals are folded under their own heading,
|
|
39
|
+
and Settings lists the consent log.
|
|
40
|
+
- **`penthera verify <findings.json>`** checks findings from an AI security
|
|
41
|
+
audit against Penthera's rules on your checkout. It reads Cloudflare's
|
|
42
|
+
security-audit skill `findings.json` (schema vendored from commit `c1c8a8c`,
|
|
43
|
+
MIT). A finding a rule agrees with is shown as Penthera's own finding, marked
|
|
44
|
+
"also proposed by" the model; the rest are labelled "model-proposed,
|
|
45
|
+
unverified" and never fail CI. Penthera runs no model and sends nothing
|
|
46
|
+
anywhere. See [docs/commands/verify.md](docs/commands/verify.md).
|
|
47
|
+
- **`--audit-seeds <file>`** writes repo findings in the security-audit
|
|
48
|
+
skill's `findings.json` schema, as seeds for an AI audit: each entry is
|
|
49
|
+
`needs_validation`, names its class and the line the rule matched, and is
|
|
50
|
+
checked against the skill's schema before it is written.
|
|
51
|
+
- Risky-pattern findings from `--repo` now carry the repo-relative `file` and
|
|
52
|
+
the `pattern` name.
|
|
53
|
+
- **`penthera mcp`: Penthera as an MCP server for your coding agent.** Claude
|
|
54
|
+
Code, Cursor, Codex, VS Code and Windsurf can call it over stdio. Add it
|
|
55
|
+
with a pinned version: `claude mcp add penthera -- npx -y penthera@1.9.0 mcp`.
|
|
56
|
+
Six read-only tools: `scan_repo`, `scan_url` (passive only),
|
|
57
|
+
`check_dependencies`, `audit_ai_tooling`, `explain_finding` and
|
|
58
|
+
`runtime_advisories`. Results are structured JSON plus a summary, with
|
|
59
|
+
evidence at `file:line`, confidence labels and each tool's limits.
|
|
60
|
+
- Tools read only inside the workspace roots, and symlinks cannot lead
|
|
61
|
+
out. `scan_url` reaches loopback, or hosts you list with
|
|
62
|
+
`--allow-host`; active modes are not reachable over MCP.
|
|
63
|
+
- Text from scanned files has invisible characters removed and is marked
|
|
64
|
+
as data, so a file cannot steer the agent through the results.
|
|
65
|
+
- Speaks MCP 2026-07-28 and the earlier `initialize` versions. No new
|
|
66
|
+
dependency, no port, no telemetry.
|
|
67
|
+
- `server.json` and `mcpName` for the MCP Registry
|
|
68
|
+
(`io.github.danoszz/penthera`). See `docs/commands/mcp.md`.
|
|
69
|
+
- **Secret findings carry the line number**, so reports and SARIF point at
|
|
70
|
+
the line.
|
|
71
|
+
|
|
72
|
+
### Detection
|
|
73
|
+
|
|
74
|
+
- Three exposure checks in the default URL scan, each a documented miss in
|
|
75
|
+
the E10 pilot (Juice Shop). All send plain `GET`s, no payloads:
|
|
76
|
+
- a public Prometheus `/metrics` endpoint (medium), matched on the
|
|
77
|
+
exposition format, so an SPA's index page at `/metrics` does not count;
|
|
78
|
+
- browsable directory listings (medium) in folders that `robots.txt`
|
|
79
|
+
disallows and in common upload and backup folders, matched on the
|
|
80
|
+
listing's page title;
|
|
81
|
+
- a stack trace in the error page for a path that does not exist (low),
|
|
82
|
+
matched on Node.js, Python, Java, PHP and .NET frame shapes.
|
|
83
|
+
- The header audit reports an `X-Powered-By` header (low, `confirmed`), such
|
|
84
|
+
as Express's default `X-Powered-By: Express`. It was a documented miss in
|
|
85
|
+
the E10 pilot (DVNA). The finding names the value and how to turn the
|
|
86
|
+
header off.
|
|
87
|
+
- The secret scan reads Terraform and HCL files (`.tf`, `.tfvars`, `.hcl`).
|
|
88
|
+
A private key inline in a Terraform file was a documented miss in the E10
|
|
89
|
+
pilot (Juice Shop).
|
|
90
|
+
- New secret rule: a session signing secret written into
|
|
91
|
+
`session({ secret: '...' })` or `cookieSession(...)`, including a hardcoded
|
|
92
|
+
fallback after `process.env.X ||` (high). Express's documented example,
|
|
93
|
+
`'keyboard cat'`, was a documented miss in the E10 pilot (DVNA).
|
|
94
|
+
- The repo scan looks for code-execution sinks in all server code, not just
|
|
95
|
+
route handlers: `eval`/`new Function` with a non-constant argument, `vm`
|
|
96
|
+
calls and `child_process` `exec` with request data in their arguments.
|
|
97
|
+
A taint-lite pass (assignments and destructuring within one file) sets the
|
|
98
|
+
confidence: `likely` when request data reaches the sink, `potential` for an
|
|
99
|
+
`eval` of a value it cannot trace. Before, a repo with an Express `routes/`
|
|
100
|
+
folder stopped before any code-pattern check; three E10 pilot misses (DVNA's
|
|
101
|
+
`exec` in `core/appHandler.js`, Juice Shop's B2B `vm` and profile `eval`)
|
|
102
|
+
came from this.
|
|
103
|
+
- The advisories pack knows MongoDB: end-of-life lines from MongoDB's
|
|
104
|
+
lifecycle schedule (4.4 ended 2024-02-29, 5.0 on 2024-10-31, 6.0 on
|
|
105
|
+
2025-07-31, and the interim releases), matched against the official `mongo`
|
|
106
|
+
image in Dockerfiles and compose files and `mongodb` in `.tool-versions`.
|
|
107
|
+
`mongo:4.4.29` in Juice Shop's compose file was a documented miss in the
|
|
108
|
+
E10 pilot. **The pack is unsigned in this change**: the maintainer signs it
|
|
109
|
+
(`scripts/rules/sign-pack.sh`) before release.
|
|
110
|
+
|
|
111
|
+
### Changed
|
|
112
|
+
|
|
113
|
+
- **Faster merges for contributors.** Pull requests get a quicker install check
|
|
114
|
+
(Linux and macOS, Node 22), and docs-, site- or benchmark-only changes skip it.
|
|
115
|
+
The merge queue and `main` still run the full matrix (Linux, macOS and
|
|
116
|
+
Windows on Node 22 and 24, plus the upgrade test). Release notes are written
|
|
117
|
+
as fragments in `changelog.d/`, so parallel pull requests no longer conflict
|
|
118
|
+
on `CHANGELOG.md`.
|
|
119
|
+
- **The default scan sends no attack payloads to your login.** A URL scan
|
|
120
|
+
used to POST nine logins and an `' OR '1'='1` string to `/api/login`,
|
|
121
|
+
which a CDN firewall (Vercel, Cloudflare) can read as an attack and start
|
|
122
|
+
challenging you. Now `quick` sends no login request, `standard` sends one
|
|
123
|
+
login with invalid credentials (to catch a login that accepts anything),
|
|
124
|
+
and the rate-limit burst and the SQL error check run only with `--deep`.
|
|
125
|
+
The MCP `scan_url` tool follows the same profiles.
|
|
126
|
+
|
|
127
|
+
### Fixed
|
|
128
|
+
|
|
129
|
+
- `penthera ui` listens for SIGINT and SIGTERM before it prints that it is
|
|
130
|
+
listening. A signal that arrived in between got the default action and
|
|
131
|
+
killed the process without "Stopped." (seen as a hung CI test; about one
|
|
132
|
+
run in a hundred locally). The test now fails within seconds with the
|
|
133
|
+
child's output instead of hanging for three minutes.
|
|
134
|
+
- `--repo` scans of Express, Hono and Fastify apps with a `routes/`, `api/` or
|
|
135
|
+
`server/routes/` folder now get route discovery, trust-boundary and
|
|
136
|
+
risky-pattern checks. That folder was taken for a Next.js API root, no
|
|
137
|
+
`route.js` files were found there, and route analysis stopped. An explicit
|
|
138
|
+
`--api-root` is still used as given.
|
|
139
|
+
- **TLS keys anyone can compute.** URL scans report, as critical, any
|
|
140
|
+
certificate in the served chain whose EC public key (P-256, P-384, P-521) is
|
|
141
|
+
k·G for a private scalar k below 1024. The private key then follows from
|
|
142
|
+
the certificate alone. This is the flaw in eMudhra's P-384 intermediates
|
|
143
|
+
([Mozilla bug 2083449](https://bugzilla.mozilla.org/show_bug.cgi?id=2083449),
|
|
144
|
+
private keys below 256). The check is arithmetic, not a list, so it has no
|
|
145
|
+
false positives and no data to maintain. Research note:
|
|
146
|
+
[docs/research/emudhra-weak-keys-2026-10.md](docs/research/emudhra-weak-keys-2026-10.md).
|
|
147
|
+
- **Node.js security release of 21 October 2026** in the `advisories` pack:
|
|
148
|
+
CVE-2026-111497 (Brotli decoder out-of-bounds write, remote code execution)
|
|
149
|
+
and the TLS certificate hostname bypasses CVE-2026-106069, CVE-2026-105483
|
|
150
|
+
and CVE-2026-106072, fixed in 22.23.4, 24.22.1 and 26.12.1, from
|
|
151
|
+
[nodejs.org](https://nodejs.org/en/blog/vulnerability/october-2026-security-releases).
|
|
152
|
+
Node.js has no server banner, so repo scans find it (`.nvmrc`,
|
|
153
|
+
`.node-version`, `.tool-versions`, `node:` images); 20.x and 25.x are
|
|
154
|
+
reported as end-of-life lines. The pack's signature was removed with the
|
|
155
|
+
change; `npm run rules:check` fails until the maintainer re-signs it.
|
|
156
|
+
- **Python start-up code in wheels.** `penthera deps` now finds `.pth` files
|
|
157
|
+
and a shipped `sitecustomize.py` / `usercustomize.py` wherever a wheel puts
|
|
158
|
+
them at the site-packages root, including `<name>-<version>.data/purelib/`
|
|
159
|
+
and `platlib/`, the layout the malicious litellm 1.107.0 used
|
|
160
|
+
([OSV MAL-2026-2144](https://osv.dev/vulnerability/MAL-2026-2144)). Before,
|
|
161
|
+
only top-level `.pth` files were read, so that wheel's hook was missed until
|
|
162
|
+
it was installed. A `.pth` import of clean code is now medium (was low);
|
|
163
|
+
risky code behind it, or any shipped `sitecustomize.py`, is high. The
|
|
164
|
+
`penthera install` policy holds `startup-hook` for approval by default.
|
|
165
|
+
- **PyPI indicators.** A new rule kind, `ioc-pypi-package` (PEP 503 names,
|
|
166
|
+
PEP 440 versions, kept apart from npm names), and a new bundled pack,
|
|
167
|
+
`ioc-pypi-2026`, with the litellm 1.82.7, 1.82.8 and 1.107.0 versions, the
|
|
168
|
+
1.107.0 payload files by SHA-256 and its endpoints. Python scans now run
|
|
169
|
+
`ioc-file` and `ioc-host` rules too. The pack is unsigned until the
|
|
170
|
+
maintainer signs it, so `npm run rules:check` fails until then.
|
|
171
|
+
- **The pre-commit hook keeps working when the npx cache is cleaned.** A hook
|
|
172
|
+
installed through `npx penthera` pointed at a copy in npm's cache; once
|
|
173
|
+
that was gone, every commit skipped the check silently. It now falls back
|
|
174
|
+
to the same version through npx (`npx -y penthera@<version> hook run`,
|
|
175
|
+
pinned), and only skips, with a clear warning, when that cannot run either.
|
|
176
|
+
|
|
177
|
+
### Site
|
|
178
|
+
|
|
179
|
+
- The FAQ says how to scan your own site behind Vercel's or Cloudflare's
|
|
180
|
+
firewall: allow your IP first, or scan a preview URL.
|
|
181
|
+
|
|
182
|
+
### Docs
|
|
183
|
+
|
|
184
|
+
- README, ROADMAP and the research plan describe what is on main for 1.9
|
|
185
|
+
(`penthera mcp`, AI mode, new detection, no attack payloads by default) and
|
|
186
|
+
list the E10 benchmark.
|
|
187
|
+
|
|
188
|
+
## [1.8.1] - 2026-10-22
|
|
189
|
+
|
|
190
|
+
A fixes-only release. Runtime advisories are more careful: pre-release builds
|
|
191
|
+
are never reported as having the fix, a banner without a distribution token is
|
|
192
|
+
at most `potential`, release lines older than a flaw are not reported as
|
|
193
|
+
affected, and two more banner formats are read. `penthera ui` now stops
|
|
194
|
+
cleanly under `npx`.
|
|
195
|
+
|
|
196
|
+
### Fixed
|
|
197
|
+
|
|
198
|
+
- Runtime advisories no longer claim `likely` for a banner that may be a
|
|
199
|
+
patched distribution package. Debian 11 to 13, Ubuntu 24.04 and later, and
|
|
200
|
+
RHEL print plain upstream PHP versions, so a patched Ubuntu 24.04
|
|
201
|
+
`X-Powered-By: PHP/8.3.6` behind nginx or a CDN was reported as critical
|
|
202
|
+
and likely affected. For PHP, Apache httpd, nginx, Python and OpenSSL,
|
|
203
|
+
"affected" and "end of life" findings from a banner with no distribution
|
|
204
|
+
token are now `potential`, with a "check on server" note and the check
|
|
205
|
+
command. Severity is unchanged. Official images and version files are
|
|
206
|
+
upstream builds and stay `likely`. Found by the runtime-strings test set
|
|
207
|
+
(L1 in [#97](https://github.com/danoszz/penthera/pull/97)).
|
|
208
|
+
|
|
209
|
+
- Runtime advisories resolve Node.js LTS codenames to their release line:
|
|
210
|
+
nvm's `lts/iron` in `.nvmrc` and the node image's `iron`, `jod-alpine`
|
|
211
|
+
and `hydrogen` tags. They read as no version before, so `lts/iron` (Node.js
|
|
212
|
+
20, end of life 30 April 2026) went unreported. Found by the
|
|
213
|
+
runtime-strings test set (B4 in
|
|
214
|
+
[#97](https://github.com/danoszz/penthera/pull/97)).
|
|
215
|
+
- Runtime advisories read aiohttp's `Server: Python/3.9 aiohttp/3.8.6`.
|
|
216
|
+
aiohttp prints only the Python release line, and the banner pattern needed
|
|
217
|
+
x.y.z, so an end-of-life Python behind aiohttp went unreported. Found by
|
|
218
|
+
the runtime-strings test set (B3 in
|
|
219
|
+
[#97](https://github.com/danoszz/penthera/pull/97)).
|
|
220
|
+
- Runtime advisories honour `introduced` on older release lines. CVE-2021-41773
|
|
221
|
+
and CVE-2021-42013 exist only in httpd 2.4.49 and 2.4.50. Yet
|
|
222
|
+
`Apache/2.2.34` and `httpd:2.2` were reported critical, "end of life, no
|
|
223
|
+
fix", and CentOS 6's `Apache/2.2.15 (CentOS)` was "check on server". A line
|
|
224
|
+
older than the one the flaw was introduced on now gets no verdict for it.
|
|
225
|
+
The end-of-life finding stays. Found by the runtime-strings test set (B1 in
|
|
226
|
+
[#97](https://github.com/danoszz/penthera/pull/97)).
|
|
227
|
+
- Runtime advisories read pre-releases correctly. `PHP/8.4.27-dev`, a PHP-8.4
|
|
228
|
+
branch build from before 8.4.27, was taken for a distribution build of
|
|
229
|
+
8.4.27 and reported as having the 21 October fix. Now `-dev`, alpha, beta
|
|
230
|
+
and RC builds (`8.5.0RC1`, `3.15.0rc1`, `4.1.0-beta2`, `php:8.6.0RC4-fpm`)
|
|
231
|
+
sort below their release, so a pre-release of the fixed version is
|
|
232
|
+
affected. A pre-release of the version after the fix (`8.4.28-dev`) is
|
|
233
|
+
"check on server", never "has the fix". Found by the runtime-strings test set
|
|
234
|
+
(B2 in [#97](https://github.com/danoszz/penthera/pull/97)).
|
|
235
|
+
- **`npx penthera ui` stops cleanly on Ctrl-C and SIGTERM.** npm forwards
|
|
236
|
+
the signal to penthera right after the terminal delivered it, and the
|
|
237
|
+
second copy could kill the dashboard before it printed "Stopped." and
|
|
238
|
+
closed its server. Repeated signals are now ignored while it stops. This
|
|
239
|
+
was the intermittent macOS failure in the install-matrix smoke check.
|
|
240
|
+
|
|
241
|
+
### Site
|
|
242
|
+
|
|
243
|
+
- penthera.org counts anonymous page views and load times with Vercel Web
|
|
244
|
+
Analytics and Speed Insights (cookieless, served from the same origin). The
|
|
245
|
+
CSP now allows connections only to those two paths on penthera.org; the
|
|
246
|
+
browser check still sends nothing, and the page says so. The CLI, the
|
|
247
|
+
dashboard and the npm package have no telemetry, unchanged.
|
|
248
|
+
- A shared penthera.org link now plays a 14-second muted reel in iMessage
|
|
249
|
+
and other apps that support `og:video`: setup, an offline scan, the
|
|
250
|
+
verdict, the PHP advisory of 21 October 2026, and the share card.
|
|
251
|
+
`og.png` stays the poster and the fallback; X keeps the image card.
|
|
252
|
+
Rendered by `site/tools/reel.mjs`.
|
|
253
|
+
|
|
254
|
+
### Research
|
|
255
|
+
|
|
256
|
+
- Draft protocol for experiment E10: Penthera against six deliberately
|
|
257
|
+
vulnerable apps with public answer keys (Juice Shop, DVNA, NodeGoat, DVWA,
|
|
258
|
+
crAPI, WebGoat), each pinned by commit and image digest. Answer keys in
|
|
259
|
+
`bench/known-vuln/`, frozen before any scan; an opt-in runner
|
|
260
|
+
(`npm run bench:known`, not in `npm test` or the package) starts the apps
|
|
261
|
+
isolated in Docker on 127.0.0.1. A pilot on Juice Shop and DVNA is in
|
|
262
|
+
`bench/known-vuln/RESULTS.md`, marked as a pilot.
|
|
263
|
+
- Each of the pilot's 13 standard-run misses is verified in
|
|
264
|
+
`bench/known-vuln/MISSES.md` (all real gaps; none a scoring artefact).
|
|
265
|
+
A re-run after the fixes that followed (#107, #108, #110, #111) is in
|
|
266
|
+
`bench/known-vuln/RESULTS-after-fixes.md`, next to the unchanged pilot:
|
|
267
|
+
pooled standard recall 1/14 to 10/14 at the same precision, flagged as not
|
|
268
|
+
independent, with WebGoat as a held-out app that did not change. The
|
|
269
|
+
runner gains `--out <file>`.
|
|
270
|
+
|
|
271
|
+
### Docs
|
|
272
|
+
|
|
273
|
+
- The PHP case study and `docs/runtime-advisories.md` cite OpenAI's
|
|
274
|
+
php2shell-checker as an independent cross-check. A test pins that Penthera
|
|
275
|
+
agrees with its table on all 14 PHP branches.
|
|
276
|
+
- The website is live at [penthera.org](https://penthera.org): the README links
|
|
277
|
+
it, and the npm package's homepage points to it from the next release.
|
|
278
|
+
|
|
7
279
|
## [1.8.0] - 2026-10-21
|
|
8
280
|
|
|
9
281
|
1.8.0 is 1.8.0-rc.1 plus the changes below; there was no separate rc.2
|
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
The security review you never had time for. Offline, rule-based, and you can verify every finding yourself.
|
|
10
10
|
|
|
11
|
+
**[penthera.org](https://penthera.org)**: try the check in your browser. Nothing you paste leaves the page.
|
|
12
|
+
|
|
11
13
|
<br/>
|
|
12
14
|
|
|
13
15
|
[](https://www.npmjs.com/package/penthera)
|
|
@@ -85,6 +87,21 @@ npx skills add danoszz/penthera
|
|
|
85
87
|
The skill tells your agent to refuse targets you do not own or are not
|
|
86
88
|
authorized to test. Other install routes (the one-line installer, the Claude
|
|
87
89
|
Code plugin, a per-project copy) are in [docs/commands/skill.md](docs/commands/skill.md).
|
|
90
|
+
|
|
91
|
+
### Use it from your agent
|
|
92
|
+
|
|
93
|
+
Penthera is also an MCP server, so Claude Code, Cursor, Codex, VS Code and
|
|
94
|
+
Windsurf can call it as a tool. Pin the version:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
claude mcp add penthera -- npx -y penthera@1.9.0 mcp
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
It offers six read-only tools: `scan_repo`, `check_dependencies`,
|
|
101
|
+
`audit_ai_tooling`, `runtime_advisories`, `explain_finding`, and `scan_url`
|
|
102
|
+
(passive, loopback or hosts you list). Tools read only inside your project,
|
|
103
|
+
never apply fixes, and run no shell. Configs for the other clients and the
|
|
104
|
+
safety rules are in [docs/commands/mcp.md](docs/commands/mcp.md).
|
|
88
105
|
Penthera needs Node.js 22 or later. To run it in a container:
|
|
89
106
|
`docker build -t penthera . && docker run --rm penthera https://your-app.example`.
|
|
90
107
|
|
|
@@ -265,10 +282,10 @@ What changed, what we are measuring, and what comes next, in one place.
|
|
|
265
282
|
|
|
266
283
|
| | Where it stands | Read more |
|
|
267
284
|
|---|---|---|
|
|
268
|
-
| **Latest release** | 1.8.
|
|
269
|
-
| **On main, unreleased** |
|
|
270
|
-
| **Now** |
|
|
271
|
-
| **Research** |
|
|
285
|
+
| **Latest release** | 1.8.1 (2026-10-22): more careful runtime advisories (pre-releases, distribution builds, older lines) on top of 1.8.0's runtime advisories, security-review fixes and redesigned dashboard | [CHANGELOG](CHANGELOG.md) |
|
|
286
|
+
| **On main, unreleased** | For 1.9: `penthera mcp` for coding agents, opt-in AI mode (a local model proposes, rules verify), new detection rules, no attack payloads in default scans | [Unreleased](CHANGELOG.md#unreleased) |
|
|
287
|
+
| **Now** | 1.9.0-rc.1 on npm `next`, then accuracy on real code (E1) | [Roadmap](ROADMAP.md#on-main-for-19) |
|
|
288
|
+
| **Research** | E10 pilot published: precision 0.88, recall 0.07 → 0.71 on two known-vulnerable apps after fixes (one labeller, not independent; a re-run is pending because default scans no longer send login checks). E1 on real code is pre-registered; E8 (agent guardrails) is a draft | [E10 results](bench/known-vuln/RESULTS.md), [E1 pre-registration](docs/research/prereg/E1-real-world-accuracy.md), [agents safely](docs/agents-safely.md) |
|
|
272
289
|
| **Next** | Real-world precision and recall with intervals, published whatever the result | [Roadmap: next](ROADMAP.md#next), [research plan](docs/vision-2.0.md) |
|
|
273
290
|
|
|
274
291
|
Releases are deliberately slow: a feature freeze, an independent review, the
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* "present" the product is in use, the version is not visible
|
|
16
16
|
* where the header, cookie or file it came from
|
|
17
17
|
* raw the text it was read from (a header value, a line), for the report
|
|
18
|
+
* banner true for black-box evidence (a header), which may come from a
|
|
19
|
+
* distribution package even without a distribution token
|
|
18
20
|
*
|
|
19
21
|
* Nothing here decides whether anything is vulnerable; that is index.js.
|
|
20
22
|
*/
|
|
@@ -27,6 +29,7 @@ const clip = (s, n = 200) => (s.length > n ? `${s.slice(0, n)}…` : s);
|
|
|
27
29
|
|
|
28
30
|
function kindOf(product, v, distro) {
|
|
29
31
|
if (!v) return "present";
|
|
32
|
+
// A pre-release is an upstream build, not a distribution one.
|
|
30
33
|
if (v.patch !== null) return distro || v.suffix ? "distro" : "exact";
|
|
31
34
|
// "php:8" spans release lines; "php:8.3" and "node:20" name one.
|
|
32
35
|
return PRODUCTS[product].branchOf(v) ? "branch" : "present";
|
|
@@ -55,7 +58,7 @@ export function evidenceFromResponse(fp = {}, more = {}) {
|
|
|
55
58
|
if (m) {
|
|
56
59
|
const v = parseVersion(m[1]);
|
|
57
60
|
if (v) {
|
|
58
|
-
out.push({ product: id, version: v, kind: kindOf(id, v, distro), where: `${name} header`, raw: clip(`${name}: ${value}`) });
|
|
61
|
+
out.push({ product: id, version: v, kind: kindOf(id, v, distro), where: `${name} header`, raw: clip(`${name}: ${value}`), banner: true });
|
|
59
62
|
found = true;
|
|
60
63
|
continue;
|
|
61
64
|
}
|
|
@@ -81,7 +84,7 @@ export function evidenceFromResponse(fp = {}, more = {}) {
|
|
|
81
84
|
|
|
82
85
|
// ── White-box ─────────────────────────────────────────────────────────────
|
|
83
86
|
|
|
84
|
-
const
|
|
87
|
+
export const RUNTIME_PIN_SKIP_DIRS = new Set([
|
|
85
88
|
"node_modules", ".git", "dist", "build", ".next", "coverage", "reports", "vendor",
|
|
86
89
|
"venv", ".venv", "site-packages", "__pycache__", ".tox", ".nox", ".obsidian",
|
|
87
90
|
]);
|
|
@@ -93,7 +96,9 @@ const isCompose = (n) => /^(?:docker-)?compose(?:\.[\w-]+)?\.ya?ml$/i.test(n);
|
|
|
93
96
|
const VERSION_FILES = new Set(Object.values(PRODUCTS).flatMap((p) => p.versionFiles));
|
|
94
97
|
const ROOT_ONLY = new Set([".tool-versions", "runtime.txt", "composer.json"]);
|
|
95
98
|
|
|
96
|
-
|
|
99
|
+
/** Would the runtime-pin check read a file with this name at this folder depth (0 = repo root)? */
|
|
100
|
+
export function isRuntimePinFile(name, depth) {
|
|
101
|
+
if (depth > MAX_DEPTH) return false;
|
|
97
102
|
if (isDockerfile(name) || isCompose(name) || VERSION_FILES.has(name)) return true;
|
|
98
103
|
return depth === 0 && ROOT_ONLY.has(name);
|
|
99
104
|
}
|
|
@@ -119,10 +124,10 @@ function walk(dir, depth, files) {
|
|
|
119
124
|
let entries;
|
|
120
125
|
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
121
126
|
for (const e of entries) {
|
|
122
|
-
if (
|
|
127
|
+
if (RUNTIME_PIN_SKIP_DIRS.has(e.name)) continue;
|
|
123
128
|
const full = join(dir, e.name);
|
|
124
129
|
if (e.isFile()) {
|
|
125
|
-
if (
|
|
130
|
+
if (isRuntimePinFile(e.name, depth)) files.push({ full, depth });
|
|
126
131
|
continue;
|
|
127
132
|
}
|
|
128
133
|
if (!e.isDirectory()) continue;
|
|
@@ -135,6 +140,12 @@ function walk(dir, depth, files) {
|
|
|
135
140
|
}
|
|
136
141
|
}
|
|
137
142
|
|
|
143
|
+
/** The release line a codename stands for (`lts/iron`, `iron` → "20"), or null. */
|
|
144
|
+
function aliasLine(product, value) {
|
|
145
|
+
const name = value.toLowerCase().replace(/^lts\//, "");
|
|
146
|
+
return Object.hasOwn(PRODUCTS[product].lineAliases || {}, name) ? PRODUCTS[product].lineAliases[name] : null;
|
|
147
|
+
}
|
|
148
|
+
|
|
138
149
|
const imageProduct = new Map(Object.entries(PRODUCTS).flatMap(([id, p]) => p.dockerImages.map((img) => [img, id])));
|
|
139
150
|
|
|
140
151
|
/**
|
|
@@ -151,8 +162,11 @@ export function parseImageRef(ref) {
|
|
|
151
162
|
const path = repo.toLowerCase().replace(/^(?:docker\.io\/)?(?:library\/)?/, "");
|
|
152
163
|
const product = imageProduct.get(path);
|
|
153
164
|
if (!product) return null;
|
|
154
|
-
|
|
155
|
-
|
|
165
|
+
// `8.6.0RC4-fpm` and `3.15.0rc1-slim` are pre-release images; `iron` and
|
|
166
|
+
// `jod-alpine` are Node.js LTS codenames.
|
|
167
|
+
const m = /^(\d+(?:\.\d+){0,2}(?:(?:RC|rc|a|b)\d+)?)(?:$|-)/.exec(tag);
|
|
168
|
+
const alias = m ? null : aliasLine(product, tag.split("-")[0]);
|
|
169
|
+
const v = m ? parseVersion(m[1]) : alias ? parseVersion(alias) : null;
|
|
156
170
|
return { product, version: v, kind: kindOf(product, v, false), tag: tag || "latest" };
|
|
157
171
|
}
|
|
158
172
|
|
|
@@ -179,7 +193,7 @@ function fromCompose(text, where) {
|
|
|
179
193
|
}
|
|
180
194
|
|
|
181
195
|
function versionLine(product, value, where, raw) {
|
|
182
|
-
const v = parseVersion(value);
|
|
196
|
+
const v = parseVersion(aliasLine(product, value) ?? value);
|
|
183
197
|
return { product, version: v, kind: kindOf(product, v, false), where, raw: clip(raw) };
|
|
184
198
|
}
|
|
185
199
|
|
package/lib/advisories/index.js
CHANGED
|
@@ -17,14 +17,17 @@
|
|
|
17
17
|
*
|
|
18
18
|
* A distro build (8.1.2-1ubuntu2.19, or any version behind a Server header
|
|
19
19
|
* that names a distribution) is never "affected": distributions backport fixes
|
|
20
|
-
* without changing the upstream number.
|
|
20
|
+
* without changing the upstream number. A banner with no distribution token
|
|
21
|
+
* for a product distributions backport (PHP/8.3.6 from Ubuntu 24.04 looks
|
|
22
|
+
* exactly like upstream 8.3.6) gives at most `potential`, with a note to check
|
|
23
|
+
* on the server; a pinned image or version file is upstream and stays `likely`. An advisory with a `condition` (an OS,
|
|
21
24
|
* a SAPI, a module) gives at most `potential`, since neither a banner nor a pin
|
|
22
25
|
* shows the condition. Matching uses only the pack's data and dates, never the
|
|
23
26
|
* scan's clock, so the same input and packs give the same findings.
|
|
24
27
|
*/
|
|
25
28
|
import { rulesOf } from "../rules/index.js";
|
|
26
29
|
import { PRODUCTS } from "./products.js";
|
|
27
|
-
import { compareVersions, compareBranches } from "./versions.js";
|
|
30
|
+
import { parseVersion, compareVersions, compareBranches } from "./versions.js";
|
|
28
31
|
import { evidenceFromResponse, evidenceFromRepo } from "./evidence.js";
|
|
29
32
|
|
|
30
33
|
export { evidenceFromResponse, evidenceFromRepo };
|
|
@@ -76,9 +79,13 @@ export function verdictFor(adv, ev, eol) {
|
|
|
76
79
|
const fix = branch ? adv.fixed.find((f) => f.branch === branch) : null;
|
|
77
80
|
const newest = adv.fixed.map((f) => f.branch).sort(compareBranches).at(-1);
|
|
78
81
|
const newerLine = branch && !fix && newest && compareBranches(branch, newest) > 0;
|
|
82
|
+
// The flaw came in on the oldest listed line (`introduced`), so a line older
|
|
83
|
+
// than that one never had it: httpd 2.2 and CVE-2021-41773 (2.4.49 only).
|
|
84
|
+
const oldest = adv.fixed.reduce((a, f) => (!a || compareBranches(f.branch, a.branch) < 0 ? f : a), null);
|
|
85
|
+
const olderLine = branch && !fix && oldest?.introduced && compareBranches(branch, oldest.branch) < 0;
|
|
79
86
|
|
|
80
87
|
if (ev.kind === "present") return { verdict: "check", why: "the version is not visible" };
|
|
81
|
-
if (newerLine) return { verdict: "none" };
|
|
88
|
+
if (newerLine || olderLine) return { verdict: "none" };
|
|
82
89
|
|
|
83
90
|
if (ev.kind === "branch") {
|
|
84
91
|
if (!fix && eol) return { verdict: "unsupported", why: `release line ${branch} is end of life` };
|
|
@@ -88,6 +95,15 @@ export function verdictFor(adv, ev, eol) {
|
|
|
88
95
|
// exact or distro: the upstream version is known.
|
|
89
96
|
if (fix) {
|
|
90
97
|
if (fix.introduced && compareVersions(ev.version, fix.introduced) < 0) return { verdict: "none" };
|
|
98
|
+
// A pre-release of the version after the fix may predate the fix: PHP-8.4
|
|
99
|
+
// says 8.4.28-dev from 8.4.27's RC until well after its release, while the
|
|
100
|
+
// fix is merged only at release. Two patches on, it is in. (A pre-release
|
|
101
|
+
// of the fixed version sorts below it and is affected.)
|
|
102
|
+
if (ev.version.pre && compareVersions(ev.version, fix.version) >= 0) {
|
|
103
|
+
const f = parseVersion(fix.version);
|
|
104
|
+
const safe = !f.letters && compareVersions(ev.version.upstream, `${f.major}.${f.minor}.${f.patch + 2}`) >= 0;
|
|
105
|
+
if (!safe) return { verdict: "check", fix, why: `it is a pre-release build (${ev.version.raw}), which may predate the fix` };
|
|
106
|
+
}
|
|
91
107
|
if (compareVersions(ev.version, fix.version) >= 0) return { verdict: "patched", fix };
|
|
92
108
|
if (ev.kind === "distro") return { verdict: "check", fix, why: "it is a distribution build, which may carry the fix as a backport under the same upstream version" };
|
|
93
109
|
return { verdict: "affected", fix };
|
|
@@ -97,6 +113,14 @@ export function verdictFor(adv, ev, eol) {
|
|
|
97
113
|
return { verdict: "check", why: `the advisory lists no fix for release line ${branch}` };
|
|
98
114
|
}
|
|
99
115
|
|
|
116
|
+
// An exact version from a banner with no distribution token, for a product
|
|
117
|
+
// distributions backport: it may be a patched package under the same number.
|
|
118
|
+
const unmarked = (ev, p) => ev.kind === "exact" && ev.banner === true && p.backports;
|
|
119
|
+
|
|
120
|
+
function unmarkedNote(p, label) {
|
|
121
|
+
return `Check on server: your distribution may have patched this under the same version number. The banner names no distribution, and Debian 11 to 13, Ubuntu 24.04 and later, and RHEL print plain upstream version numbers, so ${label} may be a distribution package. Run \`${p.checkCommand}\` on the server and ask the package manager which package provides it (\`dpkg -S\` or \`rpm -qf\` on the binary). For a package, look the CVE up in your distribution's security tracker.`;
|
|
122
|
+
}
|
|
123
|
+
|
|
100
124
|
function evidenceText(ev) {
|
|
101
125
|
return ev.raws.map((r) => `- ${r}`).join("\n");
|
|
102
126
|
}
|
|
@@ -122,6 +146,7 @@ export function matchRuntimeAdvisories(evidence, opts = {}) {
|
|
|
122
146
|
const checks = [];
|
|
123
147
|
const patched = [];
|
|
124
148
|
const start = findings.length;
|
|
149
|
+
const note = unmarked(ev, p) ? unmarkedNote(p, label) : "";
|
|
125
150
|
|
|
126
151
|
for (const adv of advisories.filter((a) => a.product === ev.product).sort(bySeverity)) {
|
|
127
152
|
const v = verdictFor(adv, ev, eol);
|
|
@@ -129,12 +154,13 @@ export function matchRuntimeAdvisories(evidence, opts = {}) {
|
|
|
129
154
|
findings.push({
|
|
130
155
|
...base,
|
|
131
156
|
severity: adv.severity,
|
|
132
|
-
confidence: adv.condition ? "potential" : "likely",
|
|
157
|
+
confidence: adv.condition || note ? "potential" : "likely",
|
|
133
158
|
title: `${label} is affected by ${adv.title}`,
|
|
134
159
|
description: [
|
|
135
160
|
`${label} (seen in ${ev.wheres.join(", ")}) is below ${v.fix.version}, the first fixed version on the ${v.fix.branch} line, for ${advLine(adv)}.`,
|
|
136
161
|
adv.kev ? `CISA lists it as exploited in the wild (Known Exploited Vulnerabilities catalog, added ${adv.kev}).` : "",
|
|
137
162
|
adv.condition ? `It only applies when: ${adv.condition}. A version alone cannot show that, so confirm it on the server.` : "",
|
|
163
|
+
note,
|
|
138
164
|
adv.description || "",
|
|
139
165
|
`Evidence:\n${evidenceText(ev)}`,
|
|
140
166
|
].filter(Boolean).join("\n\n"),
|
|
@@ -150,8 +176,9 @@ export function matchRuntimeAdvisories(evidence, opts = {}) {
|
|
|
150
176
|
title: `${label} is on an end-of-life line with no fix for ${adv.title}`,
|
|
151
177
|
description: [
|
|
152
178
|
`${label} (seen in ${ev.wheres.join(", ")}) is on release line ${branch}, which ${p.name} stopped supporting on ${eol.eol} (${eol.rule.source}). The advisory ${advLine(adv)} lists fixes only for ${adv.fixed.map((f) => f.branch).join(", ")}, so this line gets none. Whether the flaw exists on ${branch} is not stated; treat it as affected.`,
|
|
179
|
+
note,
|
|
153
180
|
`Evidence:\n${evidenceText(ev)}`,
|
|
154
|
-
].join("\n\n"),
|
|
181
|
+
].filter(Boolean).join("\n\n"),
|
|
155
182
|
remediation: `Move to a supported ${p.name} line (${adv.fixed.map((f) => `${f.branch}: ${f.version}+`).join(", ")}).`,
|
|
156
183
|
cves: adv.cves,
|
|
157
184
|
rule: adv.id,
|
|
@@ -186,13 +213,14 @@ export function matchRuntimeAdvisories(evidence, opts = {}) {
|
|
|
186
213
|
findings.push({
|
|
187
214
|
...base,
|
|
188
215
|
severity: distro ? "medium" : "high",
|
|
189
|
-
confidence: distro ? "needs-human-review" : "likely",
|
|
216
|
+
confidence: distro ? "needs-human-review" : note ? "potential" : "likely",
|
|
190
217
|
title: distro
|
|
191
218
|
? `${label} is a distribution build of an end-of-life ${p.name} line`
|
|
192
219
|
: `${label} is end of life: no more security fixes`,
|
|
193
220
|
description: [
|
|
194
221
|
`${p.name} ${branch} reached end of support on ${eol.eol} (${eol.rule.source}). Upstream publishes no security fixes for it, including for future advisories.`,
|
|
195
222
|
distro ? "This is a distribution build, and some distributions keep patching a line after upstream stops. Check your distribution's support period for this package." : "",
|
|
223
|
+
note,
|
|
196
224
|
`Evidence:\n${evidenceText(ev)}`,
|
|
197
225
|
].filter(Boolean).join("\n\n"),
|
|
198
226
|
remediation: `Upgrade to a supported ${p.name} release line.`,
|
|
@@ -205,7 +233,7 @@ export function matchRuntimeAdvisories(evidence, opts = {}) {
|
|
|
205
233
|
findings.push({
|
|
206
234
|
...base,
|
|
207
235
|
severity: "info",
|
|
208
|
-
confidence: ev.kind === "exact" ? "likely" : "potential",
|
|
236
|
+
confidence: ev.kind === "exact" && !ev.version.pre ? "likely" : "potential",
|
|
209
237
|
title: `${label} has the fix for ${patched.length === 1 ? patched[0].title : `${patched.length} known advisories`} (as advertised)`,
|
|
210
238
|
description: [
|
|
211
239
|
`The version ${ev.kind === "distro" ? "string" : "banner or pin"} is at or above the fixed version for: ${patched.map((a) => ids(a)).join(", ")}. This is what ${ev.wheres.join(", ")} says; a proxy or a stale banner can differ from what runs.`,
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The runtimes and servers runtime advisories can name, and how to recognize
|
|
3
|
-
* each one.
|
|
3
|
+
* each one. `backports`: distributions commonly ship it with security fixes
|
|
4
|
+
* backported under the upstream number, so a banner with no distribution
|
|
5
|
+
* token may still be a patched package (Debian 11-13, Ubuntu 24.04+ and RHEL
|
|
6
|
+
* print plain PHP versions; Debian's and RHEL's nginx add no OS token). Detection is code; which versions are affected is data, in signed
|
|
4
7
|
* rule packs (kind `runtime-advisory`, see lib/rules/pack.js).
|
|
5
8
|
*/
|
|
6
9
|
|
|
@@ -12,6 +15,7 @@ export const PRODUCTS = Object.freeze({
|
|
|
12
15
|
php: {
|
|
13
16
|
branchOf: minorLine,
|
|
14
17
|
name: "PHP",
|
|
18
|
+
backports: true,
|
|
15
19
|
// On the server itself, the version and any distro build string.
|
|
16
20
|
checkCommand: "php -v",
|
|
17
21
|
banner: /\bPHP\/(\d+\.\d+(?:\.\d+)?[^\s,;()]*)/i,
|
|
@@ -24,6 +28,7 @@ export const PRODUCTS = Object.freeze({
|
|
|
24
28
|
"apache-httpd": {
|
|
25
29
|
branchOf: minorLine,
|
|
26
30
|
name: "Apache HTTP Server",
|
|
31
|
+
backports: true,
|
|
27
32
|
checkCommand: "httpd -v (or apache2 -v)",
|
|
28
33
|
banner: /\bApache\/(\d+\.\d+\.\d+[^\s,;()]*)/,
|
|
29
34
|
// not Apache-Coyote (Tomcat) or Apache-HttpClient
|
|
@@ -35,6 +40,7 @@ export const PRODUCTS = Object.freeze({
|
|
|
35
40
|
nginx: {
|
|
36
41
|
branchOf: minorLine,
|
|
37
42
|
name: "nginx",
|
|
43
|
+
backports: true,
|
|
38
44
|
checkCommand: "nginx -v",
|
|
39
45
|
banner: /\bnginx\/(\d+\.\d+\.\d+[^\s,;()]*)/i,
|
|
40
46
|
present: /^nginx\b/i,
|
|
@@ -45,27 +51,48 @@ export const PRODUCTS = Object.freeze({
|
|
|
45
51
|
nodejs: {
|
|
46
52
|
branchOf: (v) => String(v.major),
|
|
47
53
|
name: "Node.js",
|
|
54
|
+
// Node.js has no version banner, and official images and binaries are upstream.
|
|
55
|
+
backports: false,
|
|
48
56
|
checkCommand: "node -v",
|
|
49
57
|
banner: null,
|
|
50
58
|
present: null,
|
|
51
59
|
dockerImages: ["node"],
|
|
52
60
|
toolVersions: ["nodejs", "node"],
|
|
53
61
|
versionFiles: [".nvmrc", ".node-version"],
|
|
62
|
+
// LTS codenames, as in nvm's `lts/iron` and the node image's `iron` tag
|
|
63
|
+
// (the `lts` field of https://nodejs.org/dist/index.json).
|
|
64
|
+
lineAliases: { argon: "4", boron: "6", carbon: "8", dubnium: "10", erbium: "12", fermium: "14", gallium: "16", hydrogen: "18", iron: "20", jod: "22", krypton: "24" },
|
|
54
65
|
},
|
|
55
66
|
python: {
|
|
56
67
|
branchOf: minorLine,
|
|
57
68
|
name: "Python",
|
|
69
|
+
backports: true,
|
|
58
70
|
checkCommand: "python3 --version",
|
|
59
|
-
// Werkzeug and http.server print "Python/3.12.1" in Server
|
|
60
|
-
|
|
71
|
+
// Werkzeug and http.server print "Python/3.12.1" in Server; aiohttp only
|
|
72
|
+
// "Python/3.12 aiohttp/3.9.5", which still names the release line
|
|
73
|
+
banner: /\bPython\/(\d+\.\d+(?:\.\d+)?[^\s,;()]*)/,
|
|
61
74
|
present: null,
|
|
62
75
|
dockerImages: ["python"],
|
|
63
76
|
toolVersions: ["python"],
|
|
64
77
|
versionFiles: [".python-version"],
|
|
65
78
|
},
|
|
79
|
+
mongodb: {
|
|
80
|
+
branchOf: minorLine,
|
|
81
|
+
name: "MongoDB",
|
|
82
|
+
// Read only from the official image and .tool-versions, which are upstream builds.
|
|
83
|
+
backports: false,
|
|
84
|
+
checkCommand: "mongod --version",
|
|
85
|
+
// A database: no HTTP banner. Seen only in what the repository pins.
|
|
86
|
+
banner: null,
|
|
87
|
+
present: null,
|
|
88
|
+
dockerImages: ["mongo"],
|
|
89
|
+
toolVersions: ["mongodb"],
|
|
90
|
+
versionFiles: [],
|
|
91
|
+
},
|
|
66
92
|
openssl: {
|
|
67
93
|
branchOf: (v) => (v.major >= 3 ? minorLine(v) : v.patch === null ? null : `${v.major}.${v.minor}.${v.patch}`),
|
|
68
94
|
name: "OpenSSL",
|
|
95
|
+
backports: true,
|
|
69
96
|
checkCommand: "openssl version",
|
|
70
97
|
banner: /\bOpenSSL\/(\d+\.\d+\.\d+[a-z]{0,2}[^\s,;()]*)/,
|
|
71
98
|
present: null,
|