penthera 1.2.0 → 1.5.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.
Files changed (101) hide show
  1. package/CHANGELOG.md +269 -0
  2. package/README.md +493 -45
  3. package/docs/METHODOLOGY.md +321 -0
  4. package/docs/compliance-cra.md +141 -0
  5. package/docs/compliance-nis2.md +3 -1
  6. package/docs/evidence.md +158 -0
  7. package/docs/examples/github-actions-deps.yml +105 -0
  8. package/docs/examples/github-actions-scan.yml +1 -1
  9. package/docs/examples/pr-dependency-review.yml +56 -0
  10. package/docs/funding/due-diligence-checklist.md +79 -0
  11. package/docs/funding/nlnet-ngi-zero-draft.md +273 -0
  12. package/docs/funding/partner-one-pager.md +84 -0
  13. package/docs/funding/sponsor-tiers.md +81 -0
  14. package/docs/index.md +12 -1
  15. package/docs/local-loop.md +276 -0
  16. package/docs/plan.md +100 -0
  17. package/docs/references.bib +539 -0
  18. package/docs/references.md +77 -0
  19. package/docs/release-plan.md +228 -0
  20. package/lib/adaptive-probes.js +8 -5
  21. package/lib/agents/discover.js +384 -0
  22. package/lib/agents/hidden-content.js +387 -0
  23. package/lib/agents/index.js +286 -0
  24. package/lib/agents/lock.js +115 -0
  25. package/lib/agents/owasp.js +83 -0
  26. package/lib/agents/rules.js +376 -0
  27. package/lib/compliance/cra.js +525 -0
  28. package/lib/crawler.js +5 -15
  29. package/lib/data/retire-jsrepository.json.gz +0 -0
  30. package/lib/data/retire-jsrepository.meta.json +7 -0
  31. package/lib/deps/capabilities.js +523 -0
  32. package/lib/deps/diff.js +84 -0
  33. package/lib/deps/index.js +440 -0
  34. package/lib/deps/license.js +329 -0
  35. package/lib/deps/lockfile.js +109 -0
  36. package/lib/deps/online.js +132 -0
  37. package/lib/deps/popular-packages.json +1 -0
  38. package/lib/deps/report.js +190 -0
  39. package/lib/deps/source.js +171 -0
  40. package/lib/deps/spdx.json +1 -0
  41. package/lib/deps/typosquat.js +113 -0
  42. package/lib/fix/apply.js +95 -0
  43. package/lib/fix/diff.js +199 -0
  44. package/lib/fix/index.js +129 -0
  45. package/lib/fix/model.js +108 -0
  46. package/lib/fix/recipes/cookies.js +130 -0
  47. package/lib/fix/recipes/cors.js +116 -0
  48. package/lib/fix/recipes/headers.js +250 -0
  49. package/lib/fix/recipes/rate-limit.js +211 -0
  50. package/lib/fix/recipes/secrets.js +110 -0
  51. package/lib/fix/util.js +231 -0
  52. package/lib/fuzzer.js +14 -8
  53. package/lib/hook/index.js +312 -0
  54. package/lib/hook/policy.js +62 -0
  55. package/lib/injections.js +19 -15
  56. package/lib/install/index.js +271 -0
  57. package/lib/install/policy.js +125 -0
  58. package/lib/jurisdictions/nl.json +9 -0
  59. package/lib/memory/history.js +52 -0
  60. package/lib/memory/index.js +111 -0
  61. package/lib/memory/repo-suppress.js +64 -0
  62. package/lib/memory/store.js +325 -0
  63. package/lib/memory/views.js +198 -0
  64. package/lib/params.js +4 -12
  65. package/lib/provenance.js +2 -0
  66. package/lib/recon.js +12 -20
  67. package/lib/references.js +31 -0
  68. package/lib/references.json +374 -0
  69. package/lib/remediation/index.js +28 -0
  70. package/lib/retirejs.js +50 -22
  71. package/lib/templates.js +7 -10
  72. package/lib/whitebox/aibom.js +384 -0
  73. package/lib/whitebox/frameworks.js +1 -1
  74. package/lib/whitebox/sbom.js +45 -10
  75. package/lib/whitebox/secrets.js +11 -1
  76. package/package.json +17 -4
  77. package/skills/penthera/SKILL.md +132 -22
  78. package/src/agent/git.js +87 -0
  79. package/src/agent/loop.js +519 -0
  80. package/src/agent/tests.js +48 -0
  81. package/src/cli/agent.js +183 -0
  82. package/src/cli/deps.js +104 -0
  83. package/src/cli/evidence.js +67 -0
  84. package/src/cli/fix.js +191 -0
  85. package/src/cli/hook.js +131 -0
  86. package/src/cli/install.js +225 -0
  87. package/src/cli/memory.js +146 -0
  88. package/src/cli/merge-results.js +2 -0
  89. package/src/cli/report-outputs.js +72 -0
  90. package/src/cli/run-scan.js +2 -0
  91. package/src/cli.js +212 -8
  92. package/src/report/audience.js +270 -0
  93. package/src/report/evidence.js +253 -0
  94. package/src/report/markdown.js +65 -2
  95. package/src/report/model.js +212 -0
  96. package/src/report/template.js +175 -0
  97. package/src/report/timeline.js +145 -0
  98. package/src/reporter.js +109 -5
  99. package/src/scan-url.js +31 -7
  100. package/src/utils/http.js +252 -11
  101. 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.
@@ -0,0 +1,141 @@
1
+ # Compliance: the Cyber Resilience Act (CRA)
2
+
3
+ Back to the [README](../README.md). NIS2 lives in
4
+ [compliance-nis2.md](compliance-nis2.md).
5
+
6
+ The Cyber Resilience Act, [Regulation (EU) 2024/2847](https://eur-lex.europa.eu/eli/reg/2024/2847/oj/eng),
7
+ puts security duties on manufacturers of products with digital elements, which
8
+ includes software you sell or make available in the EU. Most of it applies from
9
+ 11 December 2027. The reporting duty in **Article 14 has applied since
10
+ 11 September 2026** (Art. 71(2)), and it also covers products placed on the
11
+ market before December 2027 (Art. 69(3)).
12
+
13
+ Penthera helps with two pieces of CRA evidence, offline:
14
+
15
+ | Command | What you get |
16
+ |---------|--------------|
17
+ | `--cra-init cra.json` then `--cra cra.json` | Article 14 timeline with real due dates, overdue flags, and drafts of every report |
18
+ | `--repo . --aibom aibom.cdx.json` | CycloneDX 1.6 AI-BOM: AI SDKs, models referenced in code, MCP servers, skills, prompt files |
19
+
20
+ Penthera does not make you CRA compliant and does not certify anything. The
21
+ drafts are not the ENISA platform's official form, and none of this is legal
22
+ advice.
23
+
24
+ ## Article 14 reporting helper
25
+
26
+ ### What the regulation asks
27
+
28
+ Checked on 2026-10-19 against the Official Journal text and the
29
+ [ENISA Single Reporting Platform (SRP) FAQ](https://www.enisa.europa.eu/topics/product-security/vulnerability-services/eu-incident-response-and-cyber-crisis-management/single-reporting-platform-srp/frequently-asked-questions).
30
+
31
+ | | Actively exploited vulnerability (Art. 14(1)-(2)) | Severe incident (Art. 14(3)-(5)) |
32
+ |---|---|---|
33
+ | Early warning | within 24 hours of becoming aware, with the Member States where the product is available | within 24 hours, including whether it is suspected to be caused by unlawful or malicious acts, plus Member States |
34
+ | Notification | within 72 hours: the product, the general nature of the exploit and the vulnerability, corrective or mitigating measures taken and ones users can take, how sensitive the information is | within 72 hours: nature of the incident, an initial assessment, measures taken and ones users can take, sensitivity |
35
+ | Final report | no later than **14 days after a corrective or mitigating measure is available**: description with severity and impact, any malicious actor, details of the security update | within **one month after the incident notification**: detailed description with severity and impact, type of threat or root cause, applied and ongoing mitigations |
36
+
37
+ - **Where to send it.** Through the [ENISA Single Reporting Platform](https://portal.cra-srp.enisa.europa.eu/),
38
+ using the end-point of the CSIRT designated as coordinator in the Member State
39
+ of your main establishment. ENISA receives it at the same time (Art. 14(1),
40
+ 14(3), 14(7)). For the Netherlands that CSIRT is **NCSC-NL**, per ENISA's
41
+ [list of CSIRTs designated as coordinators](https://www.enisa.europa.eu/topics/product-security/vulnerability-services/eu-incident-response-and-cyber-crisis-management/single-reporting-platform-srp/list-of-csirts-designated-as-coordinators).
42
+ - **Inform your users.** After becoming aware, inform impacted users (and where
43
+ appropriate all users), with mitigations they can deploy, where appropriate
44
+ in a machine-readable format (Art. 14(8)).
45
+ - **One notification** per vulnerability or incident, even across subsidiaries
46
+ (ENISA SRP FAQ). The CSIRT may ask for an intermediate report (Art. 14(6)).
47
+ - An *actively exploited vulnerability* is one with reliable evidence that a
48
+ malicious actor exploited it without the system owner's permission (Art. 3(42)).
49
+ An incident is *severe* if it can harm the availability, authenticity,
50
+ integrity or confidentiality of sensitive or important data or functions, or
51
+ can lead to malicious code in the product or a user's systems (Art. 14(5)).
52
+
53
+ ### Use it
54
+
55
+ ```bash
56
+ penthera --cra-init cra.json # set type ("vulnerability" or "incident") and aware_at
57
+ penthera --cra cra.json # writes cra-reports.md and prints the deadlines
58
+ ```
59
+
60
+ `cra-reports.md` holds the timeline (UTC and your local time), a status per
61
+ report (`due in 21h 30m`, `submitted`, `submitted late`, `OVERDUE`,
62
+ `waiting for a fix`), a draft of each report with `NEEDS INPUT` for every fact
63
+ still missing, and a draft user advisory for Art. 14(8). Record what you've sent
64
+ in `submitted.early_warning` / `submitted.notification` / `submitted.final_report`
65
+ and re-run; for a severe incident the final report then counts from the real
66
+ submission time instead of the 72-hour deadline.
67
+
68
+ **Prefill from your scan and SBOM.** Name the affected dependency in
69
+ `component.name` and point `prefill` at Penthera output (paths relative to
70
+ `cra.json`):
71
+
72
+ ```json
73
+ {
74
+ "type": "vulnerability",
75
+ "aware_at": "2026-10-24T09:00:00+02:00",
76
+ "component": { "name": "jquery" },
77
+ "prefill": { "scan": "scan.json", "sbom": "sbom.cdx.json" }
78
+ }
79
+ ```
80
+
81
+ ```bash
82
+ penthera https://staging.example.com --repo . --sbom sbom.cdx.json -o scan.json
83
+ ```
84
+
85
+ From the SBOM it fills the product name and version and the component's version
86
+ and purl; from the scan, matching CVE ids, the scanner severity, and the finding
87
+ titles. Prefilled values are marked *verify* in the draft, and anything you wrote
88
+ yourself is never overwritten. A full example lives in `tests/fixtures/cra/`.
89
+
90
+ ### How the deadlines are counted
91
+
92
+ - **Hours are elapsed hours** from `aware_at`. If the clocks change in between,
93
+ the local due time shifts (24 hours from 09:00 CEST on 24 October 2026 is
94
+ 08:00 CET on 25 October).
95
+ - **Days and months are calendar periods** in your `timezone` (default
96
+ `Europe/Amsterdam`), ending at the same clock time. One month from 31 January
97
+ ends on 28 (or 29) February, following
98
+ [Regulation 1182/71](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:31971R1182) Art. 3(2)(c).
99
+ - A timestamp without an offset is read in `timezone`. Use ISO 8601.
100
+ - This is the **strict reading**. Regulation 1182/71 can give a little more
101
+ time (a period runs to the end of the hour or day, and a days or months period
102
+ that ends on a weekend or public holiday moves to the next working day).
103
+ Don't plan on that; report early.
104
+
105
+ The helper warns if `aware_at` is before 11 September 2026, if
106
+ `main_establishment` is not the Netherlands (so NCSC-NL is not your CSIRT), and
107
+ if the type or time zone is unknown.
108
+
109
+ ## AI-BOM (CycloneDX 1.6)
110
+
111
+ Customers and auditors increasingly ask what AI a product contains: which
112
+ providers get your data, which models, which agent tooling. `--aibom`
113
+ inventories it from the repo, offline:
114
+
115
+ ```bash
116
+ penthera --repo . --aibom aibom.cdx.json
117
+ ```
118
+
119
+ | Found | How | CycloneDX |
120
+ |-------|-----|-----------|
121
+ | AI SDKs: `openai`, `@anthropic-ai/sdk`, `@google/genai`, `ai` / `@ai-sdk/*`, `langchain` / `@langchain/*`, `llamaindex`, `ollama`, `@huggingface/transformers`; on PyPI `anthropic`, `transformers`, `torch`, `langchain-*`, `llama-index-*`, `litellm`, `mcp` and others | lockfile, `package.json`, `requirements.txt`, `pyproject.toml` | `library` with purl |
122
+ | Models: `claude-*`, `gpt-*`, `o3`, `text-embedding-*`, `gemini-*`, `gemma*`, `llama*`, `mistral-*`, `deepseek-*`, `qwen*`, `phi-*`, `grok-*`, Hugging Face ids (`meta-llama/Llama-3.1-8B-Instruct`) | whole string literals in code and `model:` keys in YAML/TOML; HF ids only for known orgs or next to a loader call | `machine-learning-model` with supplier, `modelCard` stub, `evidence.occurrences` (file and line) |
123
+ | MCP servers | `lib/agents/discover.js` (`.mcp.json`, `.cursor/`, `.vscode/mcp.json`, ...) | `services` with transport, package, endpoint and trust boundary; env var *names* only, never values or args |
124
+ | Agent Skills | `SKILL.md` folders | `data` with SHA-256 |
125
+ | Prompt files | `*.prompt`, `*.prompty`, `*.prompt.md`, anything under a `prompts/` folder | `data` with SHA-256 |
126
+
127
+ Every item hangs off the root component in `dependencies`. The BOM is only the
128
+ AI slice; use `--sbom` for all dependencies.
129
+
130
+ Spec references: the [CycloneDX 1.6 JSON reference](https://cyclonedx.org/docs/1.6/json/),
131
+ the [ML-BOM capability page](https://cyclonedx.org/capabilities/mlbom/), and the
132
+ [official schema](https://github.com/CycloneDX/specification/blob/master/schema/bom-1.6.schema.json).
133
+ That schema (with its SPDX and JSF dependencies) is vendored in
134
+ `tests/fixtures/cyclonedx/`, and the tests validate every generated AI-BOM
135
+ against it without network. Penthera emits 1.6 (ML-BOM first appeared in 1.5);
136
+ CycloneDX 1.7 is also published and is a later upgrade.
137
+
138
+ **Limits.** A model string in code means the model is referenced, not that it
139
+ runs in production; the evidence lines let a reviewer check. Model ids built at
140
+ runtime (`` `claude-${tier}` ``) are not detected. The `modelCard` is a stub:
141
+ fill in task, datasets and considerations from the provider's model card.
@@ -1,6 +1,8 @@
1
1
  # Compliance: NIS2 and the cyberwetgeving
2
2
 
3
- Back to the [README](../README.md).
3
+ Back to the [README](../README.md). Shipping a product? The Cyber Resilience
4
+ Act's Article 14 reporting helper and the AI-BOM are in
5
+ [compliance-cra.md](compliance-cra.md).
4
6
 
5
7
  The EU NIS2 directive and its national transpositions (the Cyberbeveiligingswet
6
8
  in the Netherlands, the NIS2-wet in Belgium) put a duty of care, the *zorgplicht*,
@@ -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).