penthera 1.1.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 (74) hide show
  1. package/CHANGELOG.md +168 -0
  2. package/LICENSE +21 -0
  3. package/README.md +561 -0
  4. package/SECURITY.md +31 -0
  5. package/bin/penthera.js +3 -0
  6. package/bin/scan.js +11 -0
  7. package/docs/index.md +60 -0
  8. package/docs/owasp-wstg-coverage.md +79 -0
  9. package/lib/adaptive-probes.js +334 -0
  10. package/lib/attack-surface.js +149 -0
  11. package/lib/audit-loop.js +170 -0
  12. package/lib/blackbox/adaptive-scan.js +137 -0
  13. package/lib/blackbox/client-auth.js +60 -0
  14. package/lib/blackbox/email-dns.js +135 -0
  15. package/lib/blackbox/headers.js +91 -0
  16. package/lib/blackbox/idor.js +113 -0
  17. package/lib/blackbox/jwt.js +73 -0
  18. package/lib/blackbox/oauth.js +50 -0
  19. package/lib/blackbox/openapi.js +227 -0
  20. package/lib/compliance/incident.js +169 -0
  21. package/lib/compliance/index.js +163 -0
  22. package/lib/compliance/policy-pack.js +293 -0
  23. package/lib/compliance/questionnaire.js +113 -0
  24. package/lib/compliance/readiness.js +229 -0
  25. package/lib/compliance/self-assessment.js +66 -0
  26. package/lib/crawler.js +257 -0
  27. package/lib/fuzzer.js +233 -0
  28. package/lib/injections.js +676 -0
  29. package/lib/jurisdictions/nl.json +48 -0
  30. package/lib/knowledge-graph.js +232 -0
  31. package/lib/machine.js +814 -0
  32. package/lib/nuclei-loader.js +283 -0
  33. package/lib/owasp-wstg.js +204 -0
  34. package/lib/params.js +235 -0
  35. package/lib/plugins.js +70 -0
  36. package/lib/provenance.js +82 -0
  37. package/lib/recon.js +193 -0
  38. package/lib/remediation/index.js +219 -0
  39. package/lib/retirejs.js +239 -0
  40. package/lib/supply-chain.js +131 -0
  41. package/lib/templates.js +364 -0
  42. package/lib/tls.js +145 -0
  43. package/lib/whitebox/frameworks.js +139 -0
  44. package/lib/whitebox/sbom.js +144 -0
  45. package/lib/whitebox/secrets.js +111 -0
  46. package/package.json +76 -0
  47. package/pentest.config.example.js +82 -0
  48. package/scripts/package-skill.sh +16 -0
  49. package/skills/penthera/SKILL.md +272 -0
  50. package/skills/penthera/references/authorization.md +36 -0
  51. package/skills/penthera/references/output-and-ci.md +109 -0
  52. package/skills/penthera/references/profiles-and-flags.md +74 -0
  53. package/skills/penthera/references/remediation.md +267 -0
  54. package/skills/penthera/references/reporting.md +113 -0
  55. package/skills/penthera/references/secure-defaults.md +41 -0
  56. package/skills/penthera/references/troubleshooting.md +100 -0
  57. package/skills/penthera/scripts/preflight.sh +66 -0
  58. package/skills/penthera/scripts/validate-report.mjs +53 -0
  59. package/skills/penthera/scripts/validate-report.sh +8 -0
  60. package/src/cli/ansi.js +70 -0
  61. package/src/cli/baseline.js +37 -0
  62. package/src/cli/merge-results.js +71 -0
  63. package/src/cli/onboarding.js +216 -0
  64. package/src/cli/profiles.js +53 -0
  65. package/src/cli/prompt.js +64 -0
  66. package/src/cli/run-scan.js +152 -0
  67. package/src/cli.js +536 -0
  68. package/src/report/markdown.js +182 -0
  69. package/src/reporter.js +512 -0
  70. package/src/scan-repo.js +205 -0
  71. package/src/scan-url.js +563 -0
  72. package/src/utils/auth.js +13 -0
  73. package/src/utils/http.js +38 -0
  74. package/src/utils/url.js +54 -0
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Compliance coverage mapping.
3
+ *
4
+ * Maps a Penthera scan result to a regulatory framework's technical measures,
5
+ * based on which probes ACTUALLY ran (result.executedProbes) and what they
6
+ * found. This is automated technical EVIDENCE toward a duty of care, not a
7
+ * compliance determination, an audit, or a certification. Organisational
8
+ * measures (governance, incident process, training) require human process.
9
+ * See the README "Compliance: NIS2 and the cyberwetgeving" section.
10
+ *
11
+ * Frameworks are declarative so national transpositions and other regimes
12
+ * (CyFun, ISO 27001) can be added as data without touching the engine.
13
+ */
14
+
15
+ // NIS2 Directive, Article 21(2) risk-management measures. National transpositions
16
+ //, NL Cyberbeveiligingswet, BE NIS2-wet, implement these same measures.
17
+ //
18
+ // level = the inherent ceiling of what a scanner can contribute to the measure:
19
+ // strong, automated technical testing is the heart of it
20
+ // partial, a scanner covers part of it
21
+ // evidence, output feeds it, but there is no direct test
22
+ // planned, not implemented yet
23
+ // out-of-scope, organisational/physical; a scanner cannot do it
24
+ const NIS2_MEASURES = [
25
+ { id: "a", title: "Risk analysis & information-security policy", level: "evidence",
26
+ probes: [], categories: [],
27
+ note: "Findings, severity, and baseline history feed your risk analysis." },
28
+ { id: "b", title: "Incident handling", level: "planned",
29
+ probes: [], categories: [],
30
+ note: "Incident-report helper drafts the required 24h/72h/1-month reports (--incident); incident detection itself is not automated." },
31
+ { id: "c", title: "Business continuity, backup & crisis management", level: "out-of-scope",
32
+ probes: [], categories: [],
33
+ note: "Organisational, outside a scanner's reach." },
34
+ { id: "d", title: "Supply-chain security", level: "partial",
35
+ probes: ["retirejs", "secret-scanning"],
36
+ categories: ["cve", "secrets", "js-vulnerability"],
37
+ note: "JS dependency CVEs (Retire.js), leaked credentials (secret scan), a CycloneDX SBOM (--sbom), and passive supplier rating (--suppliers)." },
38
+ { id: "e", title: "Secure acquisition, development & maintenance; vulnerability handling", level: "strong",
39
+ probes: ["reachability", "endpoint-discovery", "openapi", "sensitive-files", "cors",
40
+ "security-headers", "email-dns", "sqli", "ssti", "ssrf", "xss", "cmdi", "open-redirect",
41
+ "api-fuzzing", "risky-patterns"],
42
+ categories: ["*"],
43
+ note: "The core scan: black- and white-box testing mapped to OWASP WSTG, with SARIF and baseline regression." },
44
+ { id: "f", title: "Assessing the effectiveness of measures", level: "evidence",
45
+ probes: [], categories: [],
46
+ note: "Baseline diff plus remediation/audit-loop tracking (--history): time-to-fix, ageing, and drift over time." },
47
+ { id: "g", title: "Cyber hygiene & training", level: "evidence",
48
+ probes: [], categories: [],
49
+ note: "Remediation playbook and secure-defaults guidance feed hygiene (guidance, not a training programme)." },
50
+ { id: "h", title: "Cryptography & encryption", level: "partial",
51
+ probes: ["tls"], categories: ["tls", "cookie"],
52
+ note: "TLS protocol/cipher/certificate audit, HSTS, cookie Secure (transport layer only)." },
53
+ { id: "i", title: "HR security, access control & asset management", level: "partial",
54
+ probes: ["auth-hardening", "client-auth", "jwt", "idor", "oauth", "trust-boundary"],
55
+ categories: ["auth", "auth-bypass"],
56
+ note: "Access control tested (auth hardening, JWT, IDOR/BOLA, OAuth, trust boundaries). HR & asset management out of scope." },
57
+ { id: "j", title: "Multi-factor authentication & secure communications", level: "planned",
58
+ probes: [], categories: [],
59
+ note: "An MFA probe is on the roadmap." },
60
+ ];
61
+
62
+ const FRAMEWORKS = {
63
+ nis2: {
64
+ id: "nis2",
65
+ name: "NIS2 Directive, Article 21(2) duty of care",
66
+ measures: NIS2_MEASURES,
67
+ },
68
+ };
69
+
70
+ /** List supported framework ids. */
71
+ export function listFrameworks() {
72
+ return Object.keys(FRAMEWORKS);
73
+ }
74
+
75
+ /**
76
+ * Build a per-scan compliance coverage object for a framework.
77
+ * Returns null for an unknown framework id.
78
+ */
79
+ export function buildComplianceCoverage(result, frameworkId = "nis2") {
80
+ const fw = FRAMEWORKS[String(frameworkId).toLowerCase()];
81
+ if (!fw) return null;
82
+
83
+ const executed = new Set(result?.executedProbes || []);
84
+ const findings = result?.findings || [];
85
+
86
+ const measures = fw.measures.map((m) => {
87
+ const probesRun = m.probes.filter((p) => executed.has(p));
88
+ const relevant = m.categories.includes("*")
89
+ ? findings.filter((f) => f.severity !== "info")
90
+ : findings.filter((f) => m.categories.includes(f.category));
91
+
92
+ let status;
93
+ if (m.level === "out-of-scope") status = "out-of-scope";
94
+ else if (m.level === "planned") status = "planned";
95
+ else if (m.level === "evidence") status = "evidence";
96
+ else if (probesRun.length > 0) status = relevant.length > 0 ? "tested-findings" : "tested-clean";
97
+ else status = "not-run";
98
+
99
+ return {
100
+ id: m.id,
101
+ title: m.title,
102
+ ceiling: m.level,
103
+ status,
104
+ probesRun,
105
+ findings: relevant.length,
106
+ note: m.note,
107
+ };
108
+ });
109
+
110
+ const testable = measures.filter((m) => m.ceiling === "strong" || m.ceiling === "partial");
111
+ const coveredThisScan = testable.filter(
112
+ (m) => m.status === "tested-clean" || m.status === "tested-findings",
113
+ );
114
+
115
+ return {
116
+ framework: fw.id,
117
+ name: fw.name,
118
+ measures,
119
+ summary: {
120
+ total: measures.length,
121
+ testable: testable.length,
122
+ coveredThisScan: coveredThisScan.length,
123
+ findings: findings.filter((f) => f.severity !== "info").length,
124
+ },
125
+ };
126
+ }
127
+
128
+ const STATUS_LABEL = {
129
+ "tested-clean": "Tested, no gaps found",
130
+ "tested-findings": "Tested, gaps found",
131
+ "not-run": "Not run this scan",
132
+ evidence: "Evidence only",
133
+ planned: "Planned",
134
+ "out-of-scope": "Out of scope",
135
+ };
136
+
137
+ /** Render a compliance coverage object as a Markdown section. */
138
+ export function formatComplianceMarkdown(coverage) {
139
+ if (!coverage) return "";
140
+ const L = [];
141
+ L.push(`## Compliance mapping, ${coverage.name}`);
142
+ L.push("");
143
+ L.push(
144
+ "> Automated technical **evidence** toward the duty of care, **not** a compliance " +
145
+ "determination, an audit, or a certification. Organisational measures (governance, " +
146
+ "incident process, supply-chain contracts, training) require human process. Consult " +
147
+ "your national authority (NCSC in the Netherlands, CCB in Belgium) or a qualified jurist.",
148
+ );
149
+ L.push("");
150
+ L.push(
151
+ `This scan exercised technical coverage for **${coverage.summary.coveredThisScan}** of ` +
152
+ `**${coverage.summary.testable}** testable Article 21 measures.`,
153
+ );
154
+ L.push("");
155
+ L.push("| Art. 21(2) | Measure | This scan | Findings |");
156
+ L.push("|-----------|---------|-----------|----------|");
157
+ for (const m of coverage.measures) {
158
+ const label = STATUS_LABEL[m.status] || m.status;
159
+ L.push(`| **(${m.id})** | ${m.title} | ${label} | ${m.findings || "-"} |`);
160
+ }
161
+ L.push("");
162
+ return L.join("\n");
163
+ }
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Proportionate policy-pack generator.
3
+ *
4
+ * NIS2 expects an in-scope entity to have basic written policies. For a small
5
+ * company these should be short, real, and used, not enterprise binders. This
6
+ * module emits editable Markdown starting points for the core policies, pre-
7
+ * filled where the scan/self-assessment gives us something concrete (the
8
+ * incident plan carries the jurisdiction's real deadlines; the vulnerability
9
+ * policy references the Penthera scan cadence).
10
+ *
11
+ * These are TEMPLATES, not legal documents. Every file says so.
12
+ */
13
+
14
+ function header(title, ctx) {
15
+ return [
16
+ `# ${title}`,
17
+ "",
18
+ `**Organisation:** ${ctx.org} `,
19
+ `**Owner:** _(name a responsible person)_ `,
20
+ `**Version:** 0.1 (draft) `,
21
+ `**Last reviewed:** ${ctx.date} `,
22
+ `**Next review:** _(set a date, at least yearly)_`,
23
+ "",
24
+ ].join("\n");
25
+ }
26
+
27
+ function footer() {
28
+ return [
29
+ "",
30
+ "---",
31
+ "",
32
+ "> **Editable template, not legal advice and not a certification.** Adapt it to",
33
+ "> how your organisation actually works, then keep it short and used. An honest",
34
+ "> two-page policy that people follow beats a long one nobody reads.",
35
+ "> Generated by Penthera.",
36
+ "",
37
+ ].join("\n");
38
+ }
39
+
40
+ /** Optional one-line note about current readiness for this measure. */
41
+ function readinessNote(ctx, measure) {
42
+ const m = ctx.readiness?.measures?.find((x) => x.id === measure);
43
+ if (!m) return "";
44
+ const gaps = (m.gaps || []).filter((g) => g.type !== "unanswered").map((g) => g.question);
45
+ const gapLine = gaps.length ? ` Gaps to close: ${gaps.join("; ")}.` : "";
46
+ return `\n> _Current readiness for this measure (from your latest Penthera run): **${m.status}**.${gapLine}_\n`;
47
+ }
48
+
49
+ const POLICIES = [
50
+ {
51
+ id: "information-security", measure: "a", filename: "information-security-policy.md",
52
+ title: "Information Security Policy",
53
+ render: (ctx) => header("Information Security Policy", ctx) + readinessNote(ctx, "a") + `
54
+ ## Purpose
55
+
56
+ This policy states how ${ctx.org} protects the confidentiality, integrity, and
57
+ availability of its information and systems, proportionate to our size and risk.
58
+
59
+ ## Our commitments
60
+
61
+ - We identify our most important systems and data and the main risks to them, and review this at least yearly.
62
+ - We take appropriate, proportionate technical and organisational measures to manage those risks (see the supporting policies: access control, backup, incident response, supplier security, vulnerability management).
63
+ - Management is accountable for cyber-risk and reviews it regularly.
64
+ - Every staff member is responsible for following this policy and reporting security concerns.
65
+
66
+ ## Risk approach
67
+
68
+ We keep a short risk list: for each key asset, the main threats, current measures, and any accepted gaps. It is reviewed after significant changes or incidents.
69
+
70
+ ## Responsibilities
71
+
72
+ - **Management:** owns cyber-risk, approves this policy, ensures resources.
73
+ - **Security owner:** maintains the policies, runs/reviews scans, tracks remediation.
74
+ - **All staff:** follow the policies, complete awareness guidance, report issues.
75
+
76
+ ## Review
77
+
78
+ Reviewed at least annually and after any significant incident or change.
79
+ ` + footer(),
80
+ },
81
+ {
82
+ id: "access-control", measure: "i", filename: "access-control-policy.md",
83
+ title: "Access Control Policy",
84
+ render: (ctx) => header("Access Control Policy", ctx) + readinessNote(ctx, "i") + `
85
+ ## Purpose
86
+
87
+ Ensure people and systems have only the access they need, and that access is removed promptly when no longer needed.
88
+
89
+ ## Our commitments
90
+
91
+ - **Least privilege:** access is granted based on role and business need, not convenience.
92
+ - **Joiners/movers/leavers:** access is provisioned on start, changed on role change, and revoked on the last working day (keep a simple checklist).
93
+ - **Multi-factor authentication (MFA)** is enforced for email, admin/privileged accounts, and remote access.
94
+ - **Shared accounts are avoided;** where unavoidable, credentials are stored in a password manager and rotated.
95
+ - **Access is reviewed** periodically (e.g. quarterly) to catch stale accounts.
96
+
97
+ ## Asset & account inventory
98
+
99
+ We keep a simple inventory of systems, who has admin access, and what data each holds. It is updated when systems or people change.
100
+
101
+ ## Responsibilities
102
+
103
+ - **Security owner:** maintains the inventory and runs access reviews.
104
+ - **Managers:** request/confirm access changes for their team.
105
+
106
+ ## Review
107
+
108
+ Reviewed at least annually.
109
+ ` + footer(),
110
+ },
111
+ {
112
+ id: "incident-response", measure: "b", filename: "incident-response-plan.md",
113
+ title: "Incident Response Plan",
114
+ render: (ctx) => {
115
+ const j = ctx.jurisdiction;
116
+ const deadlines = (j?.incidentDeadlines || []).map(
117
+ (d) => `- **Within ${d.within}** of ${d.of} → notify ${d.to}: ${d.content}`,
118
+ ).join("\n") || "- _(add your jurisdiction's reporting deadlines)_";
119
+ return header("Incident Response Plan", ctx) + readinessNote(ctx, "b") + `
120
+ ## Purpose
121
+
122
+ A simple, usable plan for detecting, handling, and reporting security incidents.
123
+
124
+ ## Roles & contacts (fill in)
125
+
126
+ - **Incident lead:** _(name, phone)_
127
+ - **Deputy:** _(name, phone)_
128
+ - **Comms/legal:** _(name)_
129
+ - **Competent authority / CSIRT:** ${j?.authorities?.csirt || "_(your CSIRT)_"} (supervisor: ${j?.authorities?.supervisor || "_(your supervisor)_"})
130
+
131
+ ## Steps
132
+
133
+ 1. **Detect & record**, anyone who spots something tells the incident lead. Log time, what was seen, systems affected.
134
+ 2. **Contain**, limit the damage (isolate systems, rotate credentials, revoke access).
135
+ 3. **Assess**, how significant is it? Data affected, users affected, cross-border impact?
136
+ 4. **Report**, meet the regulatory deadlines below.
137
+ 5. **Recover**, restore from clean backups; confirm the threat is gone.
138
+ 6. **Learn**, short post-incident review; update measures and this plan.
139
+
140
+ ## Regulatory reporting deadlines
141
+
142
+ ${deadlines}
143
+
144
+ ${j?.penalties ? `> ${j.penalties}\n` : ""}
145
+ > Penthera's incident-report helper can draft the early-warning / notification /
146
+ > final-report documents against these deadlines.
147
+
148
+ ## Review
149
+
150
+ Reviewed at least annually and exercised with a simple tabletop test where possible.
151
+ ` + footer();
152
+ },
153
+ },
154
+ {
155
+ id: "backup-continuity", measure: "c", filename: "backup-continuity-policy.md",
156
+ title: "Backup & Business Continuity Policy",
157
+ render: (ctx) => header("Backup & Business Continuity Policy", ctx) + readinessNote(ctx, "c") + `
158
+ ## Purpose
159
+
160
+ Ensure we can recover important data and keep operating after data loss, ransomware, or an outage.
161
+
162
+ ## Our commitments
163
+
164
+ - **Regular backups** of important data and systems (automated where possible).
165
+ - **Off-site or immutable copies** so backups survive ransomware and accidental deletion.
166
+ - **Restore testing:** we test restoring from backup at least once a year, an untested backup is not a backup.
167
+ - **Recovery targets:** we know roughly how quickly we must recover (RTO) and how much data we can afford to lose (RPO).
168
+
169
+ ## What we back up (fill in)
170
+
171
+ - _(list key systems/data, where each is backed up, and how often)_
172
+
173
+ ## Responsibilities
174
+
175
+ - **Security/ops owner:** runs backups, performs the annual restore test, records the result.
176
+
177
+ ## Review
178
+
179
+ Reviewed at least annually.
180
+ ` + footer(),
181
+ },
182
+ {
183
+ id: "acceptable-use", measure: "g", filename: "acceptable-use-policy.md",
184
+ title: "Acceptable Use & Awareness Policy",
185
+ render: (ctx) => header("Acceptable Use & Awareness Policy", ctx) + readinessNote(ctx, "g") + `
186
+ ## Purpose
187
+
188
+ Set clear, proportionate expectations for how staff use company systems, and keep basic security awareness alive.
189
+
190
+ ## Our commitments
191
+
192
+ - **Strong, unique passwords** stored in a password manager; **MFA** where offered.
193
+ - **Phishing awareness:** be cautious with links/attachments; verify unexpected payment or access requests out-of-band.
194
+ - **Devices** are kept updated, locked when unattended, and encrypted.
195
+ - **Data handling:** company data stays in approved systems; no sensitive data in unapproved tools.
196
+ - **Reporting:** report anything suspicious to the security owner immediately, no blame for honest reports.
197
+
198
+ ## Awareness
199
+
200
+ New joiners get a short security briefing; everyone gets occasional reminders (e.g. a phishing note each quarter).
201
+
202
+ ## Review
203
+
204
+ Reviewed at least annually.
205
+ ` + footer(),
206
+ },
207
+ {
208
+ id: "supplier-security", measure: "d", filename: "supplier-security-policy.md",
209
+ title: "Supplier Security Policy",
210
+ render: (ctx) => header("Supplier Security Policy", ctx) + readinessNote(ctx, "d") + `
211
+ ## Purpose
212
+
213
+ Manage the security risk from suppliers and vendors, especially those with access to our data or systems.
214
+
215
+ ## Our commitments
216
+
217
+ - **Supplier inventory:** we keep a list of key suppliers and what access/data each has.
218
+ - **Assessment:** before onboarding a supplier with access to sensitive data, we check their security (questionnaire, certification such as ISO 27001/SOC 2, or Penthera's passive rating for the technical part).
219
+ - **Contracts:** where proportionate, supplier contracts include security and breach-notification clauses.
220
+ - **Review:** supplier risk is reviewed periodically and when a supplier's role changes.
221
+
222
+ ## Supplier list (fill in)
223
+
224
+ | Supplier | What they access | Security check done | Notes |
225
+ |----------|------------------|---------------------|-------|
226
+ | _(name)_ | _(data/systems)_ | _(yes/no + how)_ | |
227
+
228
+ ## Review
229
+
230
+ Reviewed at least annually.
231
+ ` + footer(),
232
+ },
233
+ {
234
+ id: "vulnerability-management", measure: "e", filename: "vulnerability-management-policy.md",
235
+ title: "Vulnerability Management Policy",
236
+ render: (ctx) => header("Vulnerability Management Policy", ctx) + readinessNote(ctx, "e") + `
237
+ ## Purpose
238
+
239
+ Find and fix security weaknesses before they are exploited, and handle reports responsibly.
240
+
241
+ ## Our commitments
242
+
243
+ - **Regular scanning:** we run Penthera against our applications and repositories on a set cadence (e.g. before each release and monthly), and track findings to closure.
244
+ - **Dependency updates:** we keep dependencies patched (e.g. Dependabot / \`npm audit\`) and act on known-vulnerable versions.
245
+ - **Prioritisation:** we fix by real risk (severity × exposure), not raw count, and record time-to-fix.
246
+ - **Disclosure:** we provide a way to report vulnerabilities to us (a \`SECURITY.md\` / security contact) and respond promptly.
247
+ - **Verification:** fixes are confirmed by re-scanning against a baseline before they're considered closed.
248
+
249
+ ## Cadence (fill in)
250
+
251
+ - Application scan: _(e.g. every release + monthly)_
252
+ - Dependency review: _(e.g. weekly automated)_
253
+ - Review of open findings: _(e.g. monthly)_
254
+
255
+ ## Review
256
+
257
+ Reviewed at least annually.
258
+ ` + footer(),
259
+ },
260
+ ];
261
+
262
+ /** Build the policy pack as [{ filename, content }]. */
263
+ export function buildPolicyPack(ctx = {}) {
264
+ const context = {
265
+ org: ctx.org || "Your organisation",
266
+ date: ctx.date || "",
267
+ jurisdiction: ctx.jurisdiction || null,
268
+ readiness: ctx.readiness || null,
269
+ };
270
+ return POLICIES.map((p) => ({
271
+ id: p.id,
272
+ measure: p.measure,
273
+ filename: p.filename,
274
+ title: p.title,
275
+ content: p.render(context),
276
+ }));
277
+ }
278
+
279
+ /** Index/README for the pack. */
280
+ export function policyPackIndex(pack, ctx = {}) {
281
+ const L = [];
282
+ L.push(`# Policy pack, ${ctx.org || "Your organisation"}`);
283
+ L.push("");
284
+ L.push("Proportionate, editable starting points for the core policies NIS2 expects. **Templates, not legal advice.** Fill in the blanks, keep them short and used, and review at least yearly.");
285
+ L.push("");
286
+ L.push("| Policy | Art. 21(2) |");
287
+ L.push("|--------|-----------|");
288
+ for (const p of pack) L.push(`| [${p.title}](${p.filename}) | (${p.measure}) |`);
289
+ L.push("");
290
+ return L.join("\n");
291
+ }
292
+
293
+ export { POLICIES };
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Customer security-questionnaire answerer (the wedge).
3
+ *
4
+ * Takes a pasted/exported questionnaire (one question per line) and drafts an
5
+ * answer for each by mapping it to the readiness report's evidence: what the
6
+ * scan measured plus what the owner attested. Questions it can't map are marked
7
+ * NEEDS HUMAN INPUT rather than guessed.
8
+ *
9
+ * Offline and deterministic. This drafts a response grounded in evidence; it is
10
+ * not a certification, and the owner must review every answer before sending.
11
+ */
12
+
13
+ // Keyword -> NIS2 measure. First match wins; order specific before general.
14
+ const RULES = [
15
+ { re: /\b(mfa|multi.?factor|two.?factor|2fa)\b/i, measure: "j" },
16
+ { re: /\b(spf|dkim|dmarc)\b|email (auth|spoof|security)/i, measure: "j" },
17
+ { re: /encrypt.*(at.?rest|storage|database|disk)|data.?at.?rest/i, measure: "h" },
18
+ { re: /\b(tls|https|ssl)\b|encrypt.*(transit|transport)|in transit/i, measure: "h" },
19
+ { re: /incident (response|handling|management|report)|breach notif|report.*breach/i, measure: "b" },
20
+ { re: /backup|business continuity|disaster recovery|\bdr\b|resilien/i, measure: "c" },
21
+ { re: /supplier|vendor|third.?part|supply.?chain|sub.?processor|sbom/i, measure: "d" },
22
+ { re: /access control|least privilege|\brbac\b|authoris|authoriz|joiner|leaver|offboard|privileged access/i, measure: "i" },
23
+ { re: /asset (inventory|management|register)|inventory of/i, measure: "i" },
24
+ { re: /vulnerab|patch|penetration|pentest|secure (develop|coding|sdlc)|dependency|cve/i, measure: "e" },
25
+ { re: /risk (assessment|analysis|management|register)|security polic|infosec|isms/i, measure: "a" },
26
+ { re: /training|awareness|phishing|security education/i, measure: "g" },
27
+ { re: /effectiveness|periodic review|internal audit|security metrics|kpi/i, measure: "f" },
28
+ { re: /secret|hardcoded credential|key management|api key/i, measure: "d" },
29
+ ];
30
+
31
+ const STATUS_ANSWER = {
32
+ met: "Yes",
33
+ partial: "Partial",
34
+ gap: "In progress",
35
+ "n/a": "N/A",
36
+ };
37
+
38
+ /** Parse a questionnaire file: one question per line; blanks and # comments skipped. */
39
+ export function parseQuestionnaire(text) {
40
+ return String(text || "").split(/\r?\n/)
41
+ .map((l) => l.trim())
42
+ .filter((l) => l && !l.startsWith("#"))
43
+ // strip a leading numbering/bullet ("1. ", "- ", "* ", "Q3: ")
44
+ .map((l) => l.replace(/^(\d+[.)]\s*|[-*]\s*|q\d+[:.]\s*)/i, "").trim())
45
+ .filter(Boolean);
46
+ }
47
+
48
+ /**
49
+ * Answer a questionnaire from a readiness report.
50
+ * @param {string[]} questions
51
+ * @param {object} readiness - from buildReadiness()
52
+ */
53
+ export function answerQuestionnaire(questions, readiness) {
54
+ const byId = new Map((readiness?.measures || []).map((m) => [m.id, m]));
55
+
56
+ const answers = questions.map((question) => {
57
+ const rule = RULES.find((r) => r.re.test(question));
58
+ if (!rule) {
59
+ return { question, measure: null, answer: "NEEDS HUMAN INPUT", evidence: [] };
60
+ }
61
+ const m = byId.get(rule.measure);
62
+ if (!m) {
63
+ return { question, measure: rule.measure, answer: "NEEDS HUMAN INPUT", evidence: [] };
64
+ }
65
+ const evidence = (m.evidence || []).map((e) =>
66
+ e.source === "scan" ? `scan:${e.method}` : `attested:${e.answer}`,
67
+ );
68
+ return {
69
+ question,
70
+ measure: rule.measure,
71
+ measureTitle: m.title,
72
+ status: m.status,
73
+ answer: STATUS_ANSWER[m.status] || "NEEDS HUMAN INPUT",
74
+ evidence,
75
+ };
76
+ });
77
+
78
+ const summary = { answered: 0, needsHuman: 0 };
79
+ for (const a of answers) {
80
+ if (a.answer === "NEEDS HUMAN INPUT") summary.needsHuman++;
81
+ else summary.answered++;
82
+ }
83
+ return { total: answers.length, summary, answers };
84
+ }
85
+
86
+ /** Render the answered questionnaire as Markdown. */
87
+ export function formatQuestionnaireMarkdown(result) {
88
+ if (!result) return "";
89
+ const L = [];
90
+ L.push("# Security questionnaire, draft response");
91
+ L.push("");
92
+ L.push("> Draft answers mapped from a Penthera scan and self-assessment. **Review every answer before sending.** Items marked NEEDS HUMAN INPUT could not be mapped and must be answered by a person. This is a drafting aid, not a certification.");
93
+ L.push("");
94
+ L.push(`**${result.total}** questions: ${result.summary.answered} drafted, ${result.summary.needsHuman} need human input.`);
95
+ L.push("");
96
+ L.push("| Question | Draft answer | Basis (Art. 21 / evidence) |");
97
+ L.push("|----------|--------------|----------------------------|");
98
+ for (const a of result.answers) {
99
+ const basis = a.measure
100
+ ? `(${a.measure}) ${a.status || "?"}${a.evidence?.length ? `: ${a.evidence.slice(0, 3).join(", ")}` : ""}`
101
+ : "unmapped";
102
+ L.push(`| ${a.question.replace(/\|/g, "\\|")} | ${a.answer} | ${basis.replace(/\|/g, "\\|")} |`);
103
+ }
104
+ L.push("");
105
+ const gaps = result.answers.filter((a) => a.answer === "NEEDS HUMAN INPUT").map((a) => a.question);
106
+ if (gaps.length) {
107
+ L.push("## Needs human input");
108
+ L.push("");
109
+ gaps.forEach((q) => L.push(`- ${q}`));
110
+ L.push("");
111
+ }
112
+ return L.join("\n");
113
+ }