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.
- package/CHANGELOG.md +168 -0
- package/LICENSE +21 -0
- package/README.md +561 -0
- package/SECURITY.md +31 -0
- package/bin/penthera.js +3 -0
- package/bin/scan.js +11 -0
- package/docs/index.md +60 -0
- package/docs/owasp-wstg-coverage.md +79 -0
- package/lib/adaptive-probes.js +334 -0
- package/lib/attack-surface.js +149 -0
- package/lib/audit-loop.js +170 -0
- package/lib/blackbox/adaptive-scan.js +137 -0
- package/lib/blackbox/client-auth.js +60 -0
- package/lib/blackbox/email-dns.js +135 -0
- package/lib/blackbox/headers.js +91 -0
- package/lib/blackbox/idor.js +113 -0
- package/lib/blackbox/jwt.js +73 -0
- package/lib/blackbox/oauth.js +50 -0
- package/lib/blackbox/openapi.js +227 -0
- package/lib/compliance/incident.js +169 -0
- package/lib/compliance/index.js +163 -0
- package/lib/compliance/policy-pack.js +293 -0
- package/lib/compliance/questionnaire.js +113 -0
- package/lib/compliance/readiness.js +229 -0
- package/lib/compliance/self-assessment.js +66 -0
- package/lib/crawler.js +257 -0
- package/lib/fuzzer.js +233 -0
- package/lib/injections.js +676 -0
- package/lib/jurisdictions/nl.json +48 -0
- package/lib/knowledge-graph.js +232 -0
- package/lib/machine.js +814 -0
- package/lib/nuclei-loader.js +283 -0
- package/lib/owasp-wstg.js +204 -0
- package/lib/params.js +235 -0
- package/lib/plugins.js +70 -0
- package/lib/provenance.js +82 -0
- package/lib/recon.js +193 -0
- package/lib/remediation/index.js +219 -0
- package/lib/retirejs.js +239 -0
- package/lib/supply-chain.js +131 -0
- package/lib/templates.js +364 -0
- package/lib/tls.js +145 -0
- package/lib/whitebox/frameworks.js +139 -0
- package/lib/whitebox/sbom.js +144 -0
- package/lib/whitebox/secrets.js +111 -0
- package/package.json +76 -0
- package/pentest.config.example.js +82 -0
- package/scripts/package-skill.sh +16 -0
- package/skills/penthera/SKILL.md +272 -0
- package/skills/penthera/references/authorization.md +36 -0
- package/skills/penthera/references/output-and-ci.md +109 -0
- package/skills/penthera/references/profiles-and-flags.md +74 -0
- package/skills/penthera/references/remediation.md +267 -0
- package/skills/penthera/references/reporting.md +113 -0
- package/skills/penthera/references/secure-defaults.md +41 -0
- package/skills/penthera/references/troubleshooting.md +100 -0
- package/skills/penthera/scripts/preflight.sh +66 -0
- package/skills/penthera/scripts/validate-report.mjs +53 -0
- package/skills/penthera/scripts/validate-report.sh +8 -0
- package/src/cli/ansi.js +70 -0
- package/src/cli/baseline.js +37 -0
- package/src/cli/merge-results.js +71 -0
- package/src/cli/onboarding.js +216 -0
- package/src/cli/profiles.js +53 -0
- package/src/cli/prompt.js +64 -0
- package/src/cli/run-scan.js +152 -0
- package/src/cli.js +536 -0
- package/src/report/markdown.js +182 -0
- package/src/reporter.js +512 -0
- package/src/scan-repo.js +205 -0
- package/src/scan-url.js +563 -0
- package/src/utils/auth.js +13 -0
- package/src/utils/http.js +38 -0
- 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
|
+
}
|