penthera 1.2.0 → 1.4.0
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 +212 -0
- package/README.md +379 -39
- package/docs/METHODOLOGY.md +321 -0
- package/docs/evidence.md +158 -0
- package/docs/examples/github-actions-deps.yml +105 -0
- package/docs/examples/github-actions-scan.yml +1 -1
- package/docs/examples/pr-dependency-review.yml +56 -0
- package/docs/funding/due-diligence-checklist.md +79 -0
- package/docs/funding/nlnet-ngi-zero-draft.md +273 -0
- package/docs/funding/partner-one-pager.md +84 -0
- package/docs/funding/sponsor-tiers.md +81 -0
- package/docs/index.md +12 -1
- package/docs/local-loop.md +276 -0
- package/docs/references.bib +539 -0
- package/docs/references.md +77 -0
- package/docs/release-plan.md +228 -0
- package/lib/adaptive-probes.js +8 -5
- package/lib/agents/discover.js +384 -0
- package/lib/agents/hidden-content.js +387 -0
- package/lib/agents/index.js +286 -0
- package/lib/agents/lock.js +115 -0
- package/lib/agents/owasp.js +83 -0
- package/lib/agents/rules.js +376 -0
- package/lib/crawler.js +5 -15
- package/lib/data/retire-jsrepository.json.gz +0 -0
- package/lib/data/retire-jsrepository.meta.json +7 -0
- package/lib/deps/capabilities.js +523 -0
- package/lib/deps/diff.js +84 -0
- package/lib/deps/index.js +416 -0
- package/lib/deps/lockfile.js +106 -0
- package/lib/deps/online.js +132 -0
- package/lib/deps/popular-packages.json +1 -0
- package/lib/deps/report.js +173 -0
- package/lib/deps/source.js +171 -0
- package/lib/deps/typosquat.js +113 -0
- package/lib/fix/apply.js +95 -0
- package/lib/fix/diff.js +199 -0
- package/lib/fix/index.js +129 -0
- package/lib/fix/model.js +108 -0
- package/lib/fix/recipes/cookies.js +130 -0
- package/lib/fix/recipes/cors.js +116 -0
- package/lib/fix/recipes/headers.js +250 -0
- package/lib/fix/recipes/rate-limit.js +211 -0
- package/lib/fix/recipes/secrets.js +110 -0
- package/lib/fix/util.js +231 -0
- package/lib/fuzzer.js +14 -8
- package/lib/injections.js +19 -15
- package/lib/install/index.js +271 -0
- package/lib/install/policy.js +117 -0
- package/lib/memory/history.js +52 -0
- package/lib/memory/index.js +111 -0
- package/lib/memory/repo-suppress.js +64 -0
- package/lib/memory/store.js +325 -0
- package/lib/memory/views.js +198 -0
- package/lib/params.js +4 -12
- package/lib/provenance.js +2 -0
- package/lib/recon.js +12 -20
- package/lib/references.js +31 -0
- package/lib/references.json +374 -0
- package/lib/remediation/index.js +28 -0
- package/lib/retirejs.js +50 -22
- package/lib/templates.js +7 -10
- package/lib/whitebox/frameworks.js +1 -1
- package/package.json +16 -4
- package/skills/penthera/SKILL.md +103 -22
- package/src/agent/git.js +87 -0
- package/src/agent/loop.js +519 -0
- package/src/agent/tests.js +48 -0
- package/src/cli/agent.js +183 -0
- package/src/cli/deps.js +84 -0
- package/src/cli/evidence.js +67 -0
- package/src/cli/fix.js +191 -0
- package/src/cli/install.js +225 -0
- package/src/cli/memory.js +146 -0
- package/src/cli/merge-results.js +2 -0
- package/src/cli/report-outputs.js +72 -0
- package/src/cli/run-scan.js +2 -0
- package/src/cli.js +152 -6
- package/src/report/audience.js +270 -0
- package/src/report/evidence.js +253 -0
- package/src/report/markdown.js +65 -2
- package/src/report/model.js +212 -0
- package/src/report/template.js +175 -0
- package/src/report/timeline.js +145 -0
- package/src/reporter.js +109 -5
- package/src/scan-url.js +31 -7
- package/src/utils/http.js +252 -11
- package/src/utils/url.js +23 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# Methodology
|
|
2
|
+
|
|
3
|
+
This document describes what Penthera tests, how it decides that something is a
|
|
4
|
+
finding, how sure it is, and where its limits are. Each design choice points to
|
|
5
|
+
the research or standard it rests on. Citation keys such as `[bau2010state]`
|
|
6
|
+
refer to [references.md](references.md) and [references.bib](references.bib).
|
|
7
|
+
|
|
8
|
+
Measured accuracy on the bundled benchmark is in
|
|
9
|
+
[bench/RESULTS.md](../bench/RESULTS.md); the method behind those numbers is in
|
|
10
|
+
[bench/README.md](../bench/README.md).
|
|
11
|
+
|
|
12
|
+
## Contents
|
|
13
|
+
|
|
14
|
+
- [Threat model](#threat-model)
|
|
15
|
+
- [Black-box and white-box: what each can find](#black-box-and-white-box-what-each-can-find)
|
|
16
|
+
- [Confidence model](#confidence-model)
|
|
17
|
+
- [False-positive controls](#false-positive-controls)
|
|
18
|
+
- [Non-destructive by default](#non-destructive-by-default)
|
|
19
|
+
- [Offline and no telemetry](#offline-and-no-telemetry)
|
|
20
|
+
- [How new attack techniques are adopted](#how-new-attack-techniques-are-adopted)
|
|
21
|
+
- [Citations in reports](#citations-in-reports)
|
|
22
|
+
- [Known limitations](#known-limitations)
|
|
23
|
+
|
|
24
|
+
## Threat model
|
|
25
|
+
|
|
26
|
+
Penthera is built for small teams shipping web apps and APIs, often with AI
|
|
27
|
+
coding tools, who need a first line of evidence before a human review or a
|
|
28
|
+
paid penetration test. The threat model follows the structure in Shostack
|
|
29
|
+
[shostack2014threat]: what are we building, what can go wrong, what do we do
|
|
30
|
+
about it, and did we do a good job.
|
|
31
|
+
|
|
32
|
+
**Assets.** User data behind the API, credentials and API keys (including AI
|
|
33
|
+
provider keys), the session and identity layer, and the integrity of the code
|
|
34
|
+
and dependencies that get deployed.
|
|
35
|
+
|
|
36
|
+
**Adversaries in scope.**
|
|
37
|
+
|
|
38
|
+
1. A remote, unauthenticated attacker on the internet who can send arbitrary
|
|
39
|
+
HTTP requests to the deployed app. This is what the black-box scan models.
|
|
40
|
+
2. An authenticated user trying to reach another user's data (broken object
|
|
41
|
+
level authorization, the top item in the OWASP API Security Top 10
|
|
42
|
+
[owasp2023api]). Penthera tests this when given credentials.
|
|
43
|
+
3. Anyone who obtains a copy of the source: a leaked repository, a public fork,
|
|
44
|
+
a compromised CI log. Meli et al. found secrets leaked in over 100,000
|
|
45
|
+
public GitHub repositories, with thousands of new, unique secrets leaked
|
|
46
|
+
every day [meli2019bad]. This is what the
|
|
47
|
+
white-box secret scan models.
|
|
48
|
+
4. A supply-chain attacker who publishes or compromises a dependency
|
|
49
|
+
[ladisa2023sok, ohm2020backstabber, zimmermann2019small], including
|
|
50
|
+
packages that code-generating models invent and attackers then register
|
|
51
|
+
[spracklen2025package].
|
|
52
|
+
|
|
53
|
+
**Out of scope.** Physical access, insider threats with production access,
|
|
54
|
+
denial of service, social engineering, and anything that needs exploitation
|
|
55
|
+
beyond a single benign probe. Penthera does not try to replace a human tester;
|
|
56
|
+
it tries to find the issues a scanner can find reliably and say clearly which
|
|
57
|
+
ones it cannot judge.
|
|
58
|
+
|
|
59
|
+
**Penthera itself is in the threat model.** A security tool runs with the
|
|
60
|
+
user's credentials and network position, so a compromised scanner is a
|
|
61
|
+
compromised machine. Thompson's point that you cannot trust code you did not
|
|
62
|
+
build yourself [thompson1984reflections] applies to scanners and their rule
|
|
63
|
+
feeds. This is why Penthera fetches no rules at run time
|
|
64
|
+
([below](#how-new-attack-techniques-are-adopted)).
|
|
65
|
+
|
|
66
|
+
STRIDE, the threat categories that Shostack's book builds on, originates at
|
|
67
|
+
Microsoft [kohnfelder1999threats]. Penthera's probes map onto it roughly as:
|
|
68
|
+
spoofing (auth, JWT, OAuth, email SPF/DMARC), tampering (injection, CORS),
|
|
69
|
+
repudiation (not tested), information disclosure (exposed files, secrets,
|
|
70
|
+
verbose errors), denial of service (not tested, only rate limiting is
|
|
71
|
+
checked), and elevation of privilege (IDOR/BOLA, client-side-only auth).
|
|
72
|
+
|
|
73
|
+
## Black-box and white-box: what each can find
|
|
74
|
+
|
|
75
|
+
Penthera runs two kinds of analysis and reports them together.
|
|
76
|
+
|
|
77
|
+
### Black-box (URL scan)
|
|
78
|
+
|
|
79
|
+
The scanner sees only HTTP responses. Its tests are those of the OWASP Web
|
|
80
|
+
Security Testing Guide [owaspwstg42]; each probe's WSTG IDs are listed in
|
|
81
|
+
[owasp-wstg-coverage.md](owasp-wstg-coverage.md).
|
|
82
|
+
|
|
83
|
+
What black-box testing finds well: properties that are visible in every
|
|
84
|
+
response (missing security headers, TLS configuration, cookie flags, CORS
|
|
85
|
+
policy), files and interfaces that should not be public, and injection flaws
|
|
86
|
+
whose effect shows up in the response or its timing. Protocol checks follow
|
|
87
|
+
the relevant specifications: HSTS [hodges2012rfc6797], JWT
|
|
88
|
+
[jones2015rfc7519, sheffer2020rfc8725], and OAuth redirect and PKCE handling
|
|
89
|
+
[lodderstedt2025rfc9700, sakimura2015rfc7636, fett2016comprehensive].
|
|
90
|
+
|
|
91
|
+
What it misses, and why. Two classic evaluations of black-box scanners point
|
|
92
|
+
the same way. Bau et al. tested eight commercial scanners against a custom
|
|
93
|
+
vulnerable application and older versions of real ones. As a group the tools
|
|
94
|
+
did reasonably well, but second-order flaws, stored XSS and stored SQL
|
|
95
|
+
injection, were missed by many of them [bau2010state]. Doupé et al. tested
|
|
96
|
+
eleven commercial and open-source scanners against WackoPicko, a realistic
|
|
97
|
+
purpose-built app with sixteen planted flaws. Eight of the sixteen were missed
|
|
98
|
+
by every scanner, including stored SQL injection, multi-step stored XSS,
|
|
99
|
+
forceful browsing and a business logic flaw. They concluded that crawling
|
|
100
|
+
matters as much as detection: a scanner cannot test a page it never reaches,
|
|
101
|
+
and losing track of state (logins, multi-step forms) is a main reason it
|
|
102
|
+
doesn't [doupe2010johnny].
|
|
103
|
+
|
|
104
|
+
Penthera's design responds to each point:
|
|
105
|
+
|
|
106
|
+
- **Crawl coverage.** Endpoint discovery uses a wordlist plus the app's own
|
|
107
|
+
OpenAPI document when one is exposed, and records exactly which probes ran.
|
|
108
|
+
The WSTG coverage table in every report lists tests as run or not run, so a
|
|
109
|
+
missing check is never presented as a pass.
|
|
110
|
+
- **State and logic.** Penthera does not claim to find business logic flaws,
|
|
111
|
+
which no scanner in the Doupé et al. study found either.
|
|
112
|
+
IDOR/BOLA results are labelled `needs-human-review` because a scanner cannot
|
|
113
|
+
know whether user A is allowed to read object B.
|
|
114
|
+
- **Stored and second-order flaws.** Not tested. They need a write followed by
|
|
115
|
+
a read elsewhere, which conflicts with the non-destructive default.
|
|
116
|
+
|
|
117
|
+
### White-box (repository scan)
|
|
118
|
+
|
|
119
|
+
The scanner reads source files. It finds hardcoded secrets, route handlers and
|
|
120
|
+
their authentication boundaries, and risky code patterns.
|
|
121
|
+
|
|
122
|
+
Secret detection uses provider-specific prefixes (for example `AKIA` for AWS,
|
|
123
|
+
`sk-ant-` for Anthropic) plus a small number of assignment patterns such as
|
|
124
|
+
`api_key = "..."` with a placeholder filter. Meli et al. took the same
|
|
125
|
+
conservative route, restricting detection to private keys and API keys with
|
|
126
|
+
distinctive formats [meli2019bad]. The cost is recall: a secret with no
|
|
127
|
+
recognisable format is invisible to this approach. SecretBench, a labelled
|
|
128
|
+
dataset of real secrets across many formats, is the natural yardstick for that
|
|
129
|
+
gap [basak2023secretbench]. The benchmark here includes an unprefixed signing
|
|
130
|
+
key that Penthera misses for this reason.
|
|
131
|
+
|
|
132
|
+
What white-box scanning cannot see: runtime configuration (headers added by a
|
|
133
|
+
CDN, a WAF in front of the app), secrets injected at deploy time, and anything
|
|
134
|
+
in dependencies that is only known from a vulnerability database.
|
|
135
|
+
|
|
136
|
+
### Combining them
|
|
137
|
+
|
|
138
|
+
Running both against the same app catches cases neither sees alone: a route
|
|
139
|
+
the repository marks as public that the black-box scan confirms is reachable,
|
|
140
|
+
or a header the code sets that the edge strips. The report keeps the source of
|
|
141
|
+
each finding (`source`, `method`) so a reader can tell which side produced it.
|
|
142
|
+
|
|
143
|
+
## Confidence model
|
|
144
|
+
|
|
145
|
+
Severity says how bad a finding would be if real. Confidence says how likely it
|
|
146
|
+
is to be real. Penthera reports both, and never lets a heuristic look like a
|
|
147
|
+
fact. The labels are defined in
|
|
148
|
+
[lib/provenance.js](../lib/provenance.js):
|
|
149
|
+
|
|
150
|
+
| Label | Meaning | Typical sources |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `confirmed` | Deterministic evidence. The header is absent, the certificate expired, the key matches a provider format. | Header audit, TLS check, cookie audit, secret scan, OpenAPI exposure |
|
|
153
|
+
| `likely` | A strong signal with a small false-positive chance. | Injection probes with a verified canary, auth and JWT probes, framework checks |
|
|
154
|
+
| `potential` | A heuristic or one-shot signal that needs corroboration. | Fuzzer, adaptive probes |
|
|
155
|
+
| `needs-human-review` | Exploitability cannot be judged from outside. | IDOR/BOLA, client-side-only auth |
|
|
156
|
+
|
|
157
|
+
Every finding also carries `source`, `method` and `collected_at`, and these
|
|
158
|
+
travel into SARIF so the label survives into GitHub code scanning.
|
|
159
|
+
|
|
160
|
+
The benchmark reports precision per confidence label, which is the check that
|
|
161
|
+
the labels mean what they say. On the current run all `likely` and
|
|
162
|
+
`needs-human-review` findings were true positives, the one `potential` finding
|
|
163
|
+
was a false positive, and the two `confirmed` false positives are cases where
|
|
164
|
+
the evidence is real but the judgement is wrong (a health endpoint that is
|
|
165
|
+
public by design, AWS's published example key). See
|
|
166
|
+
[bench/RESULTS.md](../bench/RESULTS.md).
|
|
167
|
+
|
|
168
|
+
## False-positive controls
|
|
169
|
+
|
|
170
|
+
Black-box scanners lose trust through false positives faster than through
|
|
171
|
+
misses. Specific controls in the probes:
|
|
172
|
+
|
|
173
|
+
- **Randomized canaries.** The SSTI probe multiplies two random four-digit
|
|
174
|
+
numbers and looks for the seven- or eight-digit product, and only when the
|
|
175
|
+
raw payload and the literal expression are absent from the response
|
|
176
|
+
([lib/injections.js](../lib/injections.js), `buildSstiProbes`). The common
|
|
177
|
+
fixed `7*7 = 49` canary fires on any page that happens to contain "49". XSS
|
|
178
|
+
probes use a per-probe random reflection marker (`makeCanary`).
|
|
179
|
+
- **Context before payload.** The XSS probe first checks where its marker is
|
|
180
|
+
reflected (HTML body, attribute, script) and only sends payloads that would
|
|
181
|
+
execute in that context, then checks they came back unencoded. This is the
|
|
182
|
+
approach of context-aware tools such as Dalfox.
|
|
183
|
+
- **Baseline comparison.** Error-based SQL injection is only reported when the
|
|
184
|
+
database error appears in the injected response and not in the baseline.
|
|
185
|
+
- **Timing re-sampling.** A single slow response proves nothing: garbage
|
|
186
|
+
collection, cold starts and network jitter all look like an injected sleep.
|
|
187
|
+
Time-based SQL and command injection candidates are re-sent and only
|
|
188
|
+
reported when the median delay holds (`confirmTimingDelay`). Crosby et al.
|
|
189
|
+
showed that network jitter dominates single remote timing measurements and
|
|
190
|
+
that filtering over many samples is what makes them usable
|
|
191
|
+
[crosby2009opportunities], building on Kocher's original timing attacks
|
|
192
|
+
[kocher1996timing]. Penthera's delays are seconds, not microseconds, so a
|
|
193
|
+
median of three samples is enough.
|
|
194
|
+
- **Host-checked redirects.** An open redirect is reported only if the
|
|
195
|
+
`Location` header actually resolves to the attacker host, not when the
|
|
196
|
+
payload merely appears in a same-site redirect's query string.
|
|
197
|
+
- **Context-aware suppression.** On localhost and private hosts, findings that
|
|
198
|
+
do not apply (no HTTPS, missing `Secure` on cookies) are suppressed.
|
|
199
|
+
|
|
200
|
+
The benchmark's decoy target exercises each of these with an endpoint built to
|
|
201
|
+
fool a naive scanner. Writing it found one real problem in the decoy itself: a
|
|
202
|
+
"safe" redirect that accepted `/\evil.com`, which browsers treat as
|
|
203
|
+
`//evil.com`. Penthera reported it correctly.
|
|
204
|
+
|
|
205
|
+
## Non-destructive by default
|
|
206
|
+
|
|
207
|
+
The `quick` and `standard` profiles send only requests a normal browser or API
|
|
208
|
+
client would send: GETs, `OPTIONS`, and invalid login attempts. They do not
|
|
209
|
+
send attack payloads, create or modify data, or exploit anything they find.
|
|
210
|
+
|
|
211
|
+
Payload-based testing (`--deep`, `--fuzz`, `--adaptive`, or `--profile deep`)
|
|
212
|
+
is opt-in and gated behind a confirmation prompt. Even then each probe sends a
|
|
213
|
+
benign marker (a random string, arithmetic, a short sleep, `id`) rather than a
|
|
214
|
+
working exploit, and stops at the first confirmation per endpoint. The
|
|
215
|
+
adaptive engine may send unexpected HTTP methods, including `DELETE`, to
|
|
216
|
+
routes with placeholder IDs; it is therefore never part of a default run.
|
|
217
|
+
|
|
218
|
+
This follows Saltzer and Schroeder's fail-safe defaults and least privilege
|
|
219
|
+
[saltzer1975protection]: the default does the least that is still useful, and
|
|
220
|
+
anything riskier needs an explicit decision. NIST SP 800-115 makes the same
|
|
221
|
+
distinction between non-intrusive assessment and penetration testing, and
|
|
222
|
+
recommends written rules of engagement before the latter
|
|
223
|
+
[scarfone2008sp800115].
|
|
224
|
+
|
|
225
|
+
## Offline and no telemetry
|
|
226
|
+
|
|
227
|
+
Penthera does not phone home. It sends no usage data, analytics or crash
|
|
228
|
+
reports, and it has no accounts. Reports are written to local files.
|
|
229
|
+
|
|
230
|
+
Network traffic goes to the target you name, with two documented exceptions:
|
|
231
|
+
|
|
232
|
+
1. `--recon` (and the `deep` profile) queries public OSINT sources (crt.sh,
|
|
233
|
+
the Wayback Machine, AlienVault OTX) with the target domain. It is skipped
|
|
234
|
+
for localhost.
|
|
235
|
+
2. The Retire.js check downloads the public Retire.js vulnerability database
|
|
236
|
+
from GitHub to compare JavaScript library versions locally. This is data,
|
|
237
|
+
not code: it is parsed as JSON and never executed. It is the one place a
|
|
238
|
+
detection input arrives at run time, and the roadmap's `--offline` egress
|
|
239
|
+
guard replaces it with a vendored copy shipped in releases.
|
|
240
|
+
|
|
241
|
+
The benchmark enforces offline operation mechanically: it wraps `fetch` so any
|
|
242
|
+
request to a non-loopback host is refused and counted, and fails if the count
|
|
243
|
+
is not zero.
|
|
244
|
+
|
|
245
|
+
These choices follow economy of mechanism and complete mediation
|
|
246
|
+
[saltzer1975protection]: fewer network paths means fewer things to audit, and
|
|
247
|
+
a tool that talks only to the target is easy to reason about in a regulated
|
|
248
|
+
environment.
|
|
249
|
+
|
|
250
|
+
## How new attack techniques are adopted
|
|
251
|
+
|
|
252
|
+
Attack techniques change quickly. In the last few years: indirect prompt
|
|
253
|
+
injection against LLM-integrated apps [greshake2023signed, perez2022ignore,
|
|
254
|
+
liu2023prompt], package hallucination and slopsquatting
|
|
255
|
+
[spracklen2025package], and attacks on agent tooling and MCP servers
|
|
256
|
+
catalogued by OWASP [owasp2024llmtop10, owasp2025agentic, owasp2025mcptop10]
|
|
257
|
+
and MITRE ATLAS [mitreatlas]. A scanner that does not keep up becomes a
|
|
258
|
+
false sense of security.
|
|
259
|
+
|
|
260
|
+
Penthera keeps up through reviewed releases, not through live updates:
|
|
261
|
+
|
|
262
|
+
- **Rules ship as code in a release.** New probes, patterns and templates are
|
|
263
|
+
added by pull request, reviewed, tested against the benchmark, and published
|
|
264
|
+
in a versioned release with a CHANGELOG entry. There is no rule server, no
|
|
265
|
+
background update, and no command that fetches rules from the internet.
|
|
266
|
+
- **Every new rule carries a source and a date.** A rule added after v1.2.0
|
|
267
|
+
records where the technique comes from (a paper, an advisory, an OWASP or
|
|
268
|
+
ATLAS entry, an incident write-up) and the date it was added. Categories are
|
|
269
|
+
already linked to their sources in [lib/references.json](../lib/references.json);
|
|
270
|
+
the built-in rules that predate this policy are being backfilled.
|
|
271
|
+
- **Every new rule comes with a benchmark case.** A detection rule is added
|
|
272
|
+
together with a labelled true positive and, where a false positive is
|
|
273
|
+
plausible, a decoy in [bench/](../bench/). The CI floor in the benchmark
|
|
274
|
+
stops a rule that adds false positives.
|
|
275
|
+
- **Your own templates stay local.** `--nuclei <path>` and `--templates <dirs>`
|
|
276
|
+
load YAML templates from directories you choose. Penthera does not download
|
|
277
|
+
them.
|
|
278
|
+
|
|
279
|
+
Why not auto-update? A rule feed is a code path from someone else's server
|
|
280
|
+
into a tool that runs with your network access and often your credentials.
|
|
281
|
+
Supply-chain research shows that update channels and package registries are
|
|
282
|
+
exactly where attackers insert themselves [ladisa2023sok, ohm2020backstabber,
|
|
283
|
+
zimmermann2019small], and that securing an update channel needs signed
|
|
284
|
+
metadata, key rotation and explicit trust delegation [samuel2010survivable,
|
|
285
|
+
torresarias2019intoto]. Shipping rules inside the signed, versioned package
|
|
286
|
+
reuses the trust you already place in the release, and gives a clear audit
|
|
287
|
+
trail: the rules you ran are the rules in the version you installed.
|
|
288
|
+
|
|
289
|
+
## Citations in reports
|
|
290
|
+
|
|
291
|
+
Each finding category maps to one to three references in
|
|
292
|
+
[lib/references.json](../lib/references.json). They appear:
|
|
293
|
+
|
|
294
|
+
- in the Markdown report, as a "Why this matters / Sources" line under each
|
|
295
|
+
finding and a "Sources" line under each action-plan item;
|
|
296
|
+
- in SARIF [oasis2023sarif], as the rule's `helpUri` (first source) and
|
|
297
|
+
`help` text (all sources), and as `properties.references` (the BibTeX keys).
|
|
298
|
+
|
|
299
|
+
[references.md](references.md) and the `sources` block of `references.json`
|
|
300
|
+
are generated from [references.bib](references.bib) by
|
|
301
|
+
`node scripts/build-references.mjs`, and a test fails if they drift.
|
|
302
|
+
|
|
303
|
+
## Known limitations
|
|
304
|
+
|
|
305
|
+
- No authenticated crawling beyond the bearer token or cookie you pass, so
|
|
306
|
+
pages behind multi-step flows are not reached [doupe2010johnny].
|
|
307
|
+
- No stored or second-order injection testing [bau2010state].
|
|
308
|
+
- No DOM-based XSS analysis; Penthera does not run JavaScript
|
|
309
|
+
[lekies2013flows, stock2014precise].
|
|
310
|
+
- No business logic testing. IDOR/BOLA findings need a human to confirm.
|
|
311
|
+
- Secret detection is pattern-based and skips Markdown and other
|
|
312
|
+
documentation files, so unprefixed secrets and secrets in notes are missed.
|
|
313
|
+
- The bundled benchmark was written by the same people as the scanner. Its
|
|
314
|
+
numbers measure regressions and the false-positive controls, not accuracy on
|
|
315
|
+
arbitrary applications. Independent benchmarks such as the OWASP Benchmark
|
|
316
|
+
[owaspbenchmark] and the approach of Antunes and Vieira
|
|
317
|
+
[antunes2010benchmarking, antunes2015assessing] are the model for a fairer
|
|
318
|
+
comparison, and scoring against labelled third-party targets (Juice Shop,
|
|
319
|
+
crAPI) is the next step.
|
|
320
|
+
- Compliance output (NIS2 [eu2022nis2]) is evidence toward a duty of care, not
|
|
321
|
+
a certification.
|
package/docs/evidence.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Evidence bundles and audience reports
|
|
2
|
+
|
|
3
|
+
Penthera can write the results of one scan as an **evidence bundle**: every
|
|
4
|
+
report, a manifest with the SHA-256 of each file, and an optional SSH
|
|
5
|
+
signature over the manifest. An auditor checks the bundle with one command.
|
|
6
|
+
No hosted service is involved, and nothing needs to be installed beyond Node
|
|
7
|
+
(or `shasum` and `jq`) and OpenSSH 8.0 or later.
|
|
8
|
+
|
|
9
|
+
## For auditors: verify a bundle
|
|
10
|
+
|
|
11
|
+
You need the bundle directory and an **allowed signers file** that names the
|
|
12
|
+
key you expect the bundle to be signed with. Get that public key from the
|
|
13
|
+
organisation through a channel other than the bundle itself (their security
|
|
14
|
+
page, an email you can authenticate, or in person).
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx penthera evidence verify ./evidence --allowed-signers allowed_signers
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Exit code `0` means every file matches the manifest and the manifest was signed
|
|
21
|
+
by a key in `allowed_signers`. Exit code `1` means something changed, is
|
|
22
|
+
missing, was added, or the signature does not check out; the output says which.
|
|
23
|
+
|
|
24
|
+
An `allowed_signers` file is one line per key:
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
security@example.com namespaces="penthera-evidence" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Without Penthera
|
|
31
|
+
|
|
32
|
+
The bundle does not depend on Penthera to be checked. With standard tools:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cd evidence
|
|
36
|
+
# 1. The manifest was signed by the expected key (namespace penthera-evidence)
|
|
37
|
+
ssh-keygen -Y verify -f ../allowed_signers -I security@example.com \
|
|
38
|
+
-n penthera-evidence -s manifest.json.sig < manifest.json
|
|
39
|
+
# 2. Every artifact matches its hash in the manifest
|
|
40
|
+
jq -r '.artifacts[] | "\(.sha256) \(.path)"' manifest.json | shasum -a 256 -c -
|
|
41
|
+
# 3. Nothing else is in the directory
|
|
42
|
+
ls
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`penthera evidence verify` does the same three checks, and also rejects
|
|
46
|
+
manifest paths that would leave the directory (`../`, absolute paths) and
|
|
47
|
+
symlinked files.
|
|
48
|
+
|
|
49
|
+
### What the checks prove, and what they do not
|
|
50
|
+
|
|
51
|
+
- **Hashes** prove that the files have not changed since the manifest was
|
|
52
|
+
written. Without a signature they prove nothing about who wrote it: anyone
|
|
53
|
+
can rewrite a file and its hash together.
|
|
54
|
+
- **The signature** proves that the holder of the signing key produced this
|
|
55
|
+
manifest. It does not prove that the scan was run against the system you
|
|
56
|
+
care about, or with the options you expect. Read the manifest's `command`,
|
|
57
|
+
`target`, `scan` and `git` fields, and the auditor report's scope section.
|
|
58
|
+
- Without `--allowed-signers`, `verify` still checks that the signature is
|
|
59
|
+
valid but cannot tell you whose key made it. It reports
|
|
60
|
+
`valid-unknown-signer` as a warning.
|
|
61
|
+
- A scan is a snapshot. The bundle is evidence of what Penthera saw at
|
|
62
|
+
`generated_at`, not a statement that the system is secure.
|
|
63
|
+
|
|
64
|
+
## For the team producing evidence
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
penthera https://staging.example.com --repo . --framework nis2 \
|
|
68
|
+
--evidence evidence/2026-10-19 --sign-key ~/.ssh/id_ed25519
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- `--evidence <dir>` writes the bundle. The directory must be new or empty, so
|
|
72
|
+
a bundle never mixes two runs.
|
|
73
|
+
- `--sign-key <key>` signs `manifest.json` with
|
|
74
|
+
`ssh-keygen -Y sign -n penthera-evidence`. Use the private key path, or the
|
|
75
|
+
`.pub` path when the key lives in `ssh-agent` or on a hardware token. A
|
|
76
|
+
passphrase prompt appears in the terminal.
|
|
77
|
+
- `--sbom <file>` and `--readiness` add their outputs to the bundle.
|
|
78
|
+
- Give auditors the public key out of band, as an `allowed_signers` line.
|
|
79
|
+
|
|
80
|
+
### What is in a bundle
|
|
81
|
+
|
|
82
|
+
| File | Contents |
|
|
83
|
+
|------|----------|
|
|
84
|
+
| `report.json` | The full scan result |
|
|
85
|
+
| `report.md` | The default Markdown report |
|
|
86
|
+
| `report.sarif` | SARIF 2.1.0 |
|
|
87
|
+
| `report-developer.md` | Findings with file:line, fix recipe ids or diffs, and sources |
|
|
88
|
+
| `report-owner.md` | Plain-language summary, what to do first, owner actions, trend |
|
|
89
|
+
| `report-auditor.md` | Scope and method, confidence and provenance, egress, inconclusive probes, suppressions, fix timeline, NIS2 and OWASP mapping |
|
|
90
|
+
| `report-custom.md` | Your `--report-template` output, when given |
|
|
91
|
+
| `nis2-readiness.md` | With `--readiness` |
|
|
92
|
+
| `sbom.cdx.json` | CycloneDX SBOM, with `--sbom` |
|
|
93
|
+
| `manifest.json` | Tool version, command line (secrets redacted), timestamp, target, git commit, SHA-256 and size of every file above |
|
|
94
|
+
| `manifest.json.sig` | SSH signature over `manifest.json`, with `--sign-key` |
|
|
95
|
+
|
|
96
|
+
The command line in the manifest has the values of `--auth-cookie`,
|
|
97
|
+
`--auth-bearer` and any token, secret or password flag replaced with
|
|
98
|
+
`[REDACTED]`, and credentials in URLs masked.
|
|
99
|
+
|
|
100
|
+
## Audience reports
|
|
101
|
+
|
|
102
|
+
`--audience developer|owner|auditor` picks who the Markdown report is for. It
|
|
103
|
+
applies to `--markdown <file>`; without `--markdown` the report goes to stdout.
|
|
104
|
+
The default report (no `--audience`) is unchanged.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
penthera https://example.com --repo . --audience owner > for-the-owner.md
|
|
108
|
+
penthera https://example.com --repo . --audience auditor --markdown audit.md
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
When a scan used `--memory`, the owner report shows the trend across recent
|
|
112
|
+
scans. When the result has an agent loop run (`loopRun`) or memory has fix
|
|
113
|
+
events, the developer, owner and auditor reports show a fix timeline:
|
|
114
|
+
found → fixed → verified, with time to fix.
|
|
115
|
+
|
|
116
|
+
## Your own template
|
|
117
|
+
|
|
118
|
+
`--report-template file.md` renders your own Markdown from the same data the
|
|
119
|
+
audience reports use. The language is deliberately tiny:
|
|
120
|
+
|
|
121
|
+
```markdown
|
|
122
|
+
# Security status for {{summary.target}}
|
|
123
|
+
|
|
124
|
+
{{summary.critical}} critical, {{summary.high}} high ({{summary.total}} total).
|
|
125
|
+
|
|
126
|
+
{{#each findings}}
|
|
127
|
+
- [{{severity}}] {{title}}{{#if location}} at {{location}}{{/if}} ({{confidence}})
|
|
128
|
+
{{/each}}
|
|
129
|
+
|
|
130
|
+
{{#if suppressed}}
|
|
131
|
+
Set aside: {{#each suppressed}}{{title}} by {{actor}}: {{reason}}; {{/each}}
|
|
132
|
+
{{else}}
|
|
133
|
+
Nothing suppressed.
|
|
134
|
+
{{/if}}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
- `{{path.to.value}}` prints a value. Values are HTML- and Markdown-escaped,
|
|
138
|
+
and newlines become spaces, so a finding title cannot add HTML, a heading, a
|
|
139
|
+
list item or a table cell.
|
|
140
|
+
- `{{#each list}}…{{/each}}` loops; inside, `{{this}}` is the item and
|
|
141
|
+
`{{@index}}` its 0-based index. Names not found on the item are looked up in
|
|
142
|
+
the enclosing scope.
|
|
143
|
+
- `{{#if value}}…{{else}}…{{/if}}`; an empty list counts as false.
|
|
144
|
+
|
|
145
|
+
There are no helpers, no expressions, no raw output, no partials and no file
|
|
146
|
+
includes. Paths containing `__proto__`, `constructor` or `prototype` are
|
|
147
|
+
rejected, and only own properties are read. A template that uses anything else
|
|
148
|
+
fails with a line number before the scan starts.
|
|
149
|
+
|
|
150
|
+
Top-level fields: `tool`, `target`, `timestamp`, `duration`, `modes`,
|
|
151
|
+
`profile`, `offline`, `probes`, `summary` (`critical`, `high`, `medium`,
|
|
152
|
+
`low`, `info`, `serious`, `total`, `suppressed`, `inconclusive`), `findings`
|
|
153
|
+
(`fid`, `title`, `severity`, `confidence`, `source`, `method`, `collectedAt`,
|
|
154
|
+
`category`, `location`, `file`, `line`, `url`, `description`, `recipe`,
|
|
155
|
+
`fixText`, `sources`, `owasp`, `wstg`), `suppressed` (`title`, `severity`,
|
|
156
|
+
`reason`, `actor`, `by`, `expires`), `inconclusive`, `egress`, `actions`,
|
|
157
|
+
`topActions`, `ownerActions`, `nis2`, `owasp`, `trend` and `timeline`. The
|
|
158
|
+
model is built in [`src/report/model.js`](../src/report/model.js).
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Example: comment a dependency capability diff on every PR that changes the lockfile.
|
|
2
|
+
#
|
|
3
|
+
# Copy this file into YOUR app's repo as `.github/workflows/penthera-deps.yml`.
|
|
4
|
+
# It installs the base and PR dependency trees with `--ignore-scripts` (no
|
|
5
|
+
# package code runs), then `penthera deps --diff` reports packages that are new
|
|
6
|
+
# or that gained capabilities (network, shell, install scripts, whole
|
|
7
|
+
# environment, dynamic code, ...) and posts that as one PR comment, updated in
|
|
8
|
+
# place on every push. Penthera itself runs offline; nothing is sent anywhere
|
|
9
|
+
# except the comment to your own PR.
|
|
10
|
+
#
|
|
11
|
+
# Pin PENTHERA_VERSION to a release you have reviewed (and, ideally, the actions
|
|
12
|
+
# below to commit SHAs) so this job never runs code you have not looked at.
|
|
13
|
+
#
|
|
14
|
+
# Fork PRs get a read-only token, so the comment step is skipped for them; the
|
|
15
|
+
# report is still uploaded as an artifact. Do not switch this to
|
|
16
|
+
# `pull_request_target`: that would run with write access next to untrusted code.
|
|
17
|
+
|
|
18
|
+
name: Dependency capability diff
|
|
19
|
+
|
|
20
|
+
on:
|
|
21
|
+
pull_request:
|
|
22
|
+
paths:
|
|
23
|
+
- package-lock.json
|
|
24
|
+
- npm-shrinkwrap.json
|
|
25
|
+
|
|
26
|
+
permissions:
|
|
27
|
+
contents: read
|
|
28
|
+
|
|
29
|
+
jobs:
|
|
30
|
+
deps:
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
permissions:
|
|
33
|
+
contents: read
|
|
34
|
+
pull-requests: write
|
|
35
|
+
env:
|
|
36
|
+
PENTHERA_VERSION: latest # pin, e.g. 1.3.0
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v7
|
|
39
|
+
with:
|
|
40
|
+
path: head
|
|
41
|
+
persist-credentials: false
|
|
42
|
+
|
|
43
|
+
- uses: actions/checkout@v7
|
|
44
|
+
with:
|
|
45
|
+
ref: ${{ github.event.pull_request.base.sha }}
|
|
46
|
+
path: base
|
|
47
|
+
persist-credentials: false
|
|
48
|
+
|
|
49
|
+
- uses: actions/setup-node@v6
|
|
50
|
+
with:
|
|
51
|
+
node-version: 22
|
|
52
|
+
|
|
53
|
+
- name: Install both trees without running install scripts
|
|
54
|
+
run: |
|
|
55
|
+
(cd head && npm ci --ignore-scripts --no-audit --no-fund)
|
|
56
|
+
(cd base && npm ci --ignore-scripts --no-audit --no-fund)
|
|
57
|
+
|
|
58
|
+
- name: Diff dependency capabilities
|
|
59
|
+
id: deps
|
|
60
|
+
# Run from the temp dir so `npx` can't pick up a `penthera` binary that
|
|
61
|
+
# the PR's own node_modules might provide.
|
|
62
|
+
working-directory: ${{ runner.temp }}
|
|
63
|
+
run: |
|
|
64
|
+
set +e
|
|
65
|
+
npx --yes "penthera@${PENTHERA_VERSION}" deps "$GITHUB_WORKSPACE/head" \
|
|
66
|
+
--diff "$GITHUB_WORKSPACE/base" \
|
|
67
|
+
--pr-comment "$RUNNER_TEMP/deps-comment.md" \
|
|
68
|
+
--sarif "$RUNNER_TEMP/deps.sarif" \
|
|
69
|
+
-o "$RUNNER_TEMP/deps.json"
|
|
70
|
+
code=$?
|
|
71
|
+
set -e
|
|
72
|
+
echo "code=$code" >> "$GITHUB_OUTPUT"
|
|
73
|
+
# 1 = critical/high findings (reported in the comment); 2 = scan error.
|
|
74
|
+
if [ "$code" -eq 2 ]; then exit 2; fi
|
|
75
|
+
|
|
76
|
+
- name: Comment on the PR (updated in place)
|
|
77
|
+
if: github.event.pull_request.head.repo.full_name == github.repository
|
|
78
|
+
env:
|
|
79
|
+
GH_TOKEN: ${{ github.token }}
|
|
80
|
+
REPO: ${{ github.repository }}
|
|
81
|
+
PR: ${{ github.event.pull_request.number }}
|
|
82
|
+
run: |
|
|
83
|
+
body="$RUNNER_TEMP/deps-comment.md"
|
|
84
|
+
id=$(gh api "repos/$REPO/issues/$PR/comments" --paginate \
|
|
85
|
+
--jq '.[] | select(.body | startswith("<!-- penthera-deps -->")) | .id' | head -n1)
|
|
86
|
+
if [ -n "$id" ]; then
|
|
87
|
+
gh api -X PATCH "repos/$REPO/issues/comments/$id" -F body=@"$body" > /dev/null
|
|
88
|
+
else
|
|
89
|
+
gh pr comment "$PR" --repo "$REPO" --body-file "$body"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
- uses: actions/upload-artifact@v7
|
|
93
|
+
if: always()
|
|
94
|
+
with:
|
|
95
|
+
name: penthera-deps
|
|
96
|
+
path: |
|
|
97
|
+
${{ runner.temp }}/deps-comment.md
|
|
98
|
+
${{ runner.temp }}/deps.json
|
|
99
|
+
${{ runner.temp }}/deps.md
|
|
100
|
+
${{ runner.temp }}/deps.sarif
|
|
101
|
+
|
|
102
|
+
# Optional: block the merge when critical or high findings appear.
|
|
103
|
+
# - name: Fail on critical/high
|
|
104
|
+
# if: steps.deps.outputs.code == '1'
|
|
105
|
+
# run: exit 1
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Example: comment a dependency capability diff on every PR that changes the lockfile.
|
|
2
|
+
#
|
|
3
|
+
# Copy this file into YOUR repo as `.github/workflows/pr-dependency-review.yml`.
|
|
4
|
+
# The Penthera action reports packages that are new or that gained capabilities
|
|
5
|
+
# (network, shell, install scripts, whole environment, dynamic code, ...) in one
|
|
6
|
+
# PR comment, updated in place on every push, and uploads SARIF to code scanning.
|
|
7
|
+
# Penthera runs offline from the action's own checkout: no npx, no registry
|
|
8
|
+
# lookups, no telemetry. The only API call is the comment on your own PR.
|
|
9
|
+
#
|
|
10
|
+
# Pin the action to a release commit SHA. Find it with:
|
|
11
|
+
# git ls-remote https://github.com/danoszz/penthera refs/tags/v1.4.0
|
|
12
|
+
#
|
|
13
|
+
# Fork PRs get a read-only token: the comment is skipped and the diff goes to the
|
|
14
|
+
# job summary instead. Do not switch this to `pull_request_target`; that would
|
|
15
|
+
# run with write access next to untrusted code.
|
|
16
|
+
|
|
17
|
+
name: Dependency review
|
|
18
|
+
|
|
19
|
+
on:
|
|
20
|
+
pull_request:
|
|
21
|
+
paths:
|
|
22
|
+
- package-lock.json
|
|
23
|
+
- npm-shrinkwrap.json
|
|
24
|
+
|
|
25
|
+
permissions:
|
|
26
|
+
contents: read
|
|
27
|
+
pull-requests: write
|
|
28
|
+
security-events: write
|
|
29
|
+
|
|
30
|
+
jobs:
|
|
31
|
+
penthera:
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
35
|
+
|
|
36
|
+
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
|
37
|
+
with:
|
|
38
|
+
node-version: 22
|
|
39
|
+
|
|
40
|
+
# Optional but recommended: Penthera reads package code from node_modules
|
|
41
|
+
# and npm's cache. Installing both trees with --ignore-scripts (no package
|
|
42
|
+
# code runs) lets it inspect every changed version. Without this step the
|
|
43
|
+
# comment lists upgrades it could not compare.
|
|
44
|
+
- name: Install base and PR trees without running scripts
|
|
45
|
+
env:
|
|
46
|
+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
47
|
+
run: |
|
|
48
|
+
git fetch --no-tags --depth=1 origin "$BASE_SHA"
|
|
49
|
+
git worktree add --detach "$RUNNER_TEMP/base" "$BASE_SHA"
|
|
50
|
+
(cd "$RUNNER_TEMP/base" && npm ci --ignore-scripts --no-audit --no-fund)
|
|
51
|
+
npm ci --ignore-scripts --no-audit --no-fund
|
|
52
|
+
|
|
53
|
+
- uses: danoszz/penthera@0000000000000000000000000000000000000000 # v1.4.0 (replace with the release commit SHA)
|
|
54
|
+
with:
|
|
55
|
+
mode: deps
|
|
56
|
+
fail-on: none # or high / critical to block the merge
|