chainlesschain 0.47.8 → 0.47.9
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/package.json +10 -8
- package/src/commands/activitypub.js +533 -0
- package/src/commands/compliance.js +597 -6
- package/src/commands/matrix.js +283 -0
- package/src/commands/mcp.js +344 -0
- package/src/commands/nostr.js +196 -7
- package/src/commands/social.js +265 -0
- package/src/index.js +2 -0
- package/src/lib/activitypub-bridge.js +623 -0
- package/src/lib/compliance-framework-reporter.js +600 -0
- package/src/lib/matrix-bridge.js +252 -0
- package/src/lib/mcp-registry.js +347 -0
- package/src/lib/mcp-scaffold.js +385 -0
- package/src/lib/nostr-bridge.js +214 -38
- package/src/lib/social-graph.js +408 -0
- package/src/lib/stix-parser.js +167 -0
- package/src/lib/threat-intel.js +268 -0
- package/src/lib/topic-classifier.js +400 -0
- package/src/lib/ueba.js +403 -0
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compliance Framework Reporter
|
|
3
|
+
*
|
|
4
|
+
* Turns raw compliance evidence + policies into framework-aware
|
|
5
|
+
* coverage reports. Each framework ships with a curated catalog of
|
|
6
|
+
* representative controls (NOT the full official standard — we
|
|
7
|
+
* don't pretend to replace a CPA audit). A control declares which
|
|
8
|
+
* evidence types and policy types count toward it, and the analyzer
|
|
9
|
+
* reports per-control: covered | partial | gap.
|
|
10
|
+
*
|
|
11
|
+
* Three renderers: markdown (default), html, json.
|
|
12
|
+
*
|
|
13
|
+
* Why a static catalog?
|
|
14
|
+
* - Reports should be deterministic and reviewable in diffs.
|
|
15
|
+
* - Evidence types used by `compliance-manager.collectEvidence` are
|
|
16
|
+
* already free-form — a static catalog gives us a fixed vocabulary
|
|
17
|
+
* to map them against.
|
|
18
|
+
* - The catalog can be extended per-project without touching code via
|
|
19
|
+
* `registerFrameworkTemplate(id, template)`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import crypto from "crypto";
|
|
23
|
+
|
|
24
|
+
/* ── Static framework catalogs ────────────────────────────── */
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* SOC 2 — Trust Services Criteria (AICPA 2017, revised 2022).
|
|
28
|
+
* Abbreviated to the most commonly audited CC (Common Criteria) items.
|
|
29
|
+
*/
|
|
30
|
+
const SOC2_TEMPLATE = {
|
|
31
|
+
id: "soc2",
|
|
32
|
+
name: "SOC 2",
|
|
33
|
+
version: "TSC 2017 (rev. 2022)",
|
|
34
|
+
category: "Attestation",
|
|
35
|
+
controls: [
|
|
36
|
+
{
|
|
37
|
+
id: "CC1.1",
|
|
38
|
+
title: "Control Environment — Commitment to Integrity",
|
|
39
|
+
category: "Governance",
|
|
40
|
+
requires: {
|
|
41
|
+
evidenceTypes: ["policy_document", "code_of_conduct"],
|
|
42
|
+
policyTypes: [],
|
|
43
|
+
},
|
|
44
|
+
description:
|
|
45
|
+
"The entity demonstrates a commitment to integrity and ethical values.",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "CC2.1",
|
|
49
|
+
title: "Communication — Information Quality",
|
|
50
|
+
category: "Governance",
|
|
51
|
+
requires: {
|
|
52
|
+
evidenceTypes: ["communication", "training_record"],
|
|
53
|
+
policyTypes: [],
|
|
54
|
+
},
|
|
55
|
+
description:
|
|
56
|
+
"Relevant and quality information supports internal controls.",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "CC5.1",
|
|
60
|
+
title: "Control Activities — Authorization",
|
|
61
|
+
category: "Access Control",
|
|
62
|
+
requires: {
|
|
63
|
+
evidenceTypes: ["access_review", "rbac_report"],
|
|
64
|
+
policyTypes: ["access_control"],
|
|
65
|
+
},
|
|
66
|
+
description:
|
|
67
|
+
"The entity selects and develops control activities to mitigate risks.",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "CC6.1",
|
|
71
|
+
title: "Logical Access — Identification & Authentication",
|
|
72
|
+
category: "Access Control",
|
|
73
|
+
requires: {
|
|
74
|
+
evidenceTypes: ["auth_log", "mfa_config"],
|
|
75
|
+
policyTypes: ["access_control"],
|
|
76
|
+
},
|
|
77
|
+
description:
|
|
78
|
+
"Logical access to data and software is restricted to authorized users.",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "CC6.6",
|
|
82
|
+
title: "Logical Access — Encryption in Transit",
|
|
83
|
+
category: "Data Protection",
|
|
84
|
+
requires: {
|
|
85
|
+
evidenceTypes: ["tls_scan", "network_diagram"],
|
|
86
|
+
policyTypes: ["encryption"],
|
|
87
|
+
},
|
|
88
|
+
description: "Data transmitted over external networks is encrypted.",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "CC6.7",
|
|
92
|
+
title: "Logical Access — Encryption at Rest",
|
|
93
|
+
category: "Data Protection",
|
|
94
|
+
requires: {
|
|
95
|
+
evidenceTypes: ["disk_encryption", "db_encryption"],
|
|
96
|
+
policyTypes: ["encryption"],
|
|
97
|
+
},
|
|
98
|
+
description: "Data at rest is protected via encryption.",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: "CC7.2",
|
|
102
|
+
title: "System Operations — Monitoring",
|
|
103
|
+
category: "Monitoring",
|
|
104
|
+
requires: {
|
|
105
|
+
evidenceTypes: ["siem_export", "audit_log"],
|
|
106
|
+
policyTypes: ["audit_trail"],
|
|
107
|
+
},
|
|
108
|
+
description:
|
|
109
|
+
"The entity monitors system components for anomalies and responds.",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "CC7.4",
|
|
113
|
+
title: "System Operations — Incident Response",
|
|
114
|
+
category: "Incident Response",
|
|
115
|
+
requires: {
|
|
116
|
+
evidenceTypes: ["incident_report", "runbook"],
|
|
117
|
+
policyTypes: [],
|
|
118
|
+
},
|
|
119
|
+
description: "Incidents are analyzed, contained, and remediated.",
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* ISO/IEC 27001:2022 — Annex A abbreviated snapshot.
|
|
126
|
+
* Picks representative items from clauses 5, 6, 8 of the 93-control list.
|
|
127
|
+
*/
|
|
128
|
+
const ISO27001_TEMPLATE = {
|
|
129
|
+
id: "iso27001",
|
|
130
|
+
name: "ISO/IEC 27001:2022",
|
|
131
|
+
version: "2022",
|
|
132
|
+
category: "Certification",
|
|
133
|
+
controls: [
|
|
134
|
+
{
|
|
135
|
+
id: "A.5.1",
|
|
136
|
+
title: "Policies for information security",
|
|
137
|
+
category: "Organizational",
|
|
138
|
+
requires: { evidenceTypes: ["policy_document"], policyTypes: [] },
|
|
139
|
+
description:
|
|
140
|
+
"An information security policy shall be defined, approved, published and communicated.",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: "A.5.15",
|
|
144
|
+
title: "Access control",
|
|
145
|
+
category: "Organizational",
|
|
146
|
+
requires: {
|
|
147
|
+
evidenceTypes: ["access_review", "rbac_report"],
|
|
148
|
+
policyTypes: ["access_control"],
|
|
149
|
+
},
|
|
150
|
+
description:
|
|
151
|
+
"Rules to control physical and logical access shall be established.",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: "A.5.23",
|
|
155
|
+
title: "Information security for use of cloud services",
|
|
156
|
+
category: "Organizational",
|
|
157
|
+
requires: {
|
|
158
|
+
evidenceTypes: ["cloud_config", "vendor_assessment"],
|
|
159
|
+
policyTypes: [],
|
|
160
|
+
},
|
|
161
|
+
description:
|
|
162
|
+
"Security of cloud services shall be established and managed.",
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: "A.8.5",
|
|
166
|
+
title: "Secure authentication",
|
|
167
|
+
category: "Technological",
|
|
168
|
+
requires: {
|
|
169
|
+
evidenceTypes: ["auth_log", "mfa_config"],
|
|
170
|
+
policyTypes: ["access_control"],
|
|
171
|
+
},
|
|
172
|
+
description: "Secure authentication technologies shall be implemented.",
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: "A.8.12",
|
|
176
|
+
title: "Data leakage prevention",
|
|
177
|
+
category: "Technological",
|
|
178
|
+
requires: {
|
|
179
|
+
evidenceTypes: ["dlp_scan", "egress_report"],
|
|
180
|
+
policyTypes: ["data_classification"],
|
|
181
|
+
},
|
|
182
|
+
description:
|
|
183
|
+
"DLP measures shall be applied to systems and networks processing sensitive data.",
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: "A.8.15",
|
|
187
|
+
title: "Logging",
|
|
188
|
+
category: "Technological",
|
|
189
|
+
requires: {
|
|
190
|
+
evidenceTypes: ["audit_log", "siem_export"],
|
|
191
|
+
policyTypes: ["audit_trail"],
|
|
192
|
+
},
|
|
193
|
+
description:
|
|
194
|
+
"Logs recording security events shall be produced and retained.",
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: "A.8.24",
|
|
198
|
+
title: "Use of cryptography",
|
|
199
|
+
category: "Technological",
|
|
200
|
+
requires: {
|
|
201
|
+
evidenceTypes: ["key_inventory", "disk_encryption"],
|
|
202
|
+
policyTypes: ["encryption"],
|
|
203
|
+
},
|
|
204
|
+
description:
|
|
205
|
+
"Rules for cryptographic controls shall be defined and implemented.",
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: "A.8.28",
|
|
209
|
+
title: "Secure coding",
|
|
210
|
+
category: "Technological",
|
|
211
|
+
requires: {
|
|
212
|
+
evidenceTypes: ["sast_report", "code_review"],
|
|
213
|
+
policyTypes: [],
|
|
214
|
+
},
|
|
215
|
+
description:
|
|
216
|
+
"Secure coding principles shall be applied to software development.",
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* GDPR — EU General Data Protection Regulation.
|
|
223
|
+
* Articles selected for the data-protection core.
|
|
224
|
+
*/
|
|
225
|
+
const GDPR_TEMPLATE = {
|
|
226
|
+
id: "gdpr",
|
|
227
|
+
name: "GDPR",
|
|
228
|
+
version: "Regulation (EU) 2016/679",
|
|
229
|
+
category: "Regulation",
|
|
230
|
+
controls: [
|
|
231
|
+
{
|
|
232
|
+
id: "Art.5",
|
|
233
|
+
title: "Principles relating to processing of personal data",
|
|
234
|
+
category: "Principles",
|
|
235
|
+
requires: {
|
|
236
|
+
evidenceTypes: ["data_map", "policy_document"],
|
|
237
|
+
policyTypes: ["retention", "data_classification"],
|
|
238
|
+
},
|
|
239
|
+
description:
|
|
240
|
+
"Personal data processed lawfully, fairly, transparently, purpose-limited, minimised, accurate.",
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
id: "Art.6",
|
|
244
|
+
title: "Lawfulness of processing",
|
|
245
|
+
category: "Principles",
|
|
246
|
+
requires: { evidenceTypes: ["consent_record", "dpia"], policyTypes: [] },
|
|
247
|
+
description:
|
|
248
|
+
"Processing requires a legal basis (consent, contract, legal obligation, vital interests, public task, legitimate interests).",
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
id: "Art.15",
|
|
252
|
+
title: "Right of access by the data subject",
|
|
253
|
+
category: "Data Subject Rights",
|
|
254
|
+
requires: { evidenceTypes: ["dsr_log"], policyTypes: [] },
|
|
255
|
+
description:
|
|
256
|
+
"The data subject shall have the right to obtain confirmation and access to personal data.",
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
id: "Art.17",
|
|
260
|
+
title: "Right to erasure ('right to be forgotten')",
|
|
261
|
+
category: "Data Subject Rights",
|
|
262
|
+
requires: {
|
|
263
|
+
evidenceTypes: ["dsr_log", "deletion_log"],
|
|
264
|
+
policyTypes: ["retention"],
|
|
265
|
+
},
|
|
266
|
+
description: "The data subject shall have the right to obtain erasure.",
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
id: "Art.25",
|
|
270
|
+
title: "Data protection by design and by default",
|
|
271
|
+
category: "Accountability",
|
|
272
|
+
requires: {
|
|
273
|
+
evidenceTypes: ["dpia", "architecture_review"],
|
|
274
|
+
policyTypes: ["data_classification"],
|
|
275
|
+
},
|
|
276
|
+
description:
|
|
277
|
+
"Controller shall implement appropriate technical and organisational measures.",
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
id: "Art.30",
|
|
281
|
+
title: "Records of processing activities",
|
|
282
|
+
category: "Accountability",
|
|
283
|
+
requires: { evidenceTypes: ["ropa", "data_map"], policyTypes: [] },
|
|
284
|
+
description:
|
|
285
|
+
"Each controller shall maintain a record of processing activities under its responsibility.",
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
id: "Art.32",
|
|
289
|
+
title: "Security of processing",
|
|
290
|
+
category: "Security",
|
|
291
|
+
requires: {
|
|
292
|
+
evidenceTypes: ["disk_encryption", "audit_log"],
|
|
293
|
+
policyTypes: ["encryption", "access_control"],
|
|
294
|
+
},
|
|
295
|
+
description:
|
|
296
|
+
"Implement appropriate technical and organisational measures to ensure a level of security.",
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
id: "Art.33",
|
|
300
|
+
title: "Notification of a personal data breach",
|
|
301
|
+
category: "Security",
|
|
302
|
+
requires: {
|
|
303
|
+
evidenceTypes: ["incident_report", "breach_log"],
|
|
304
|
+
policyTypes: [],
|
|
305
|
+
},
|
|
306
|
+
description:
|
|
307
|
+
"Controller shall notify the supervisory authority within 72 hours of becoming aware.",
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
export const FRAMEWORK_TEMPLATES = Object.freeze({
|
|
313
|
+
soc2: SOC2_TEMPLATE,
|
|
314
|
+
iso27001: ISO27001_TEMPLATE,
|
|
315
|
+
gdpr: GDPR_TEMPLATE,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Custom templates registered at runtime (for project-specific frameworks).
|
|
319
|
+
const _customTemplates = new Map();
|
|
320
|
+
|
|
321
|
+
export function registerFrameworkTemplate(id, template) {
|
|
322
|
+
if (!id) throw new Error("Framework id is required");
|
|
323
|
+
if (!template || !Array.isArray(template.controls)) {
|
|
324
|
+
throw new Error("Template must have a controls array");
|
|
325
|
+
}
|
|
326
|
+
_customTemplates.set(id, template);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function unregisterFrameworkTemplate(id) {
|
|
330
|
+
return _customTemplates.delete(id);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function getFrameworkTemplate(id) {
|
|
334
|
+
return _customTemplates.get(id) || FRAMEWORK_TEMPLATES[id] || null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function listFrameworks() {
|
|
338
|
+
return [
|
|
339
|
+
...Object.keys(FRAMEWORK_TEMPLATES),
|
|
340
|
+
..._customTemplates.keys(),
|
|
341
|
+
].filter((v, i, arr) => arr.indexOf(v) === i);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/* ── Gap analysis ─────────────────────────────────────────── */
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Analyze framework coverage against a set of evidence + policies.
|
|
348
|
+
*
|
|
349
|
+
* @param {string} frameworkId
|
|
350
|
+
* @param {{ evidence: Array<{type,description}>, policies: Array<{type,name}> }} opts
|
|
351
|
+
* @returns coverage report object (see shape below)
|
|
352
|
+
*/
|
|
353
|
+
export function analyzeCoverage(frameworkId, opts = {}) {
|
|
354
|
+
const template = getFrameworkTemplate(frameworkId);
|
|
355
|
+
if (!template) {
|
|
356
|
+
throw new Error(`Unknown framework: ${frameworkId}`);
|
|
357
|
+
}
|
|
358
|
+
const evidence = Array.isArray(opts.evidence) ? opts.evidence : [];
|
|
359
|
+
const policies = Array.isArray(opts.policies) ? opts.policies : [];
|
|
360
|
+
|
|
361
|
+
const evidenceByType = new Map();
|
|
362
|
+
for (const e of evidence) {
|
|
363
|
+
if (!e?.type) continue;
|
|
364
|
+
const bucket = evidenceByType.get(e.type) || [];
|
|
365
|
+
bucket.push(e);
|
|
366
|
+
evidenceByType.set(e.type, bucket);
|
|
367
|
+
}
|
|
368
|
+
const policyByType = new Map();
|
|
369
|
+
for (const p of policies) {
|
|
370
|
+
if (!p?.type) continue;
|
|
371
|
+
const bucket = policyByType.get(p.type) || [];
|
|
372
|
+
bucket.push(p);
|
|
373
|
+
policyByType.set(p.type, bucket);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const controls = template.controls.map((ctrl) => {
|
|
377
|
+
const need = ctrl.requires || {};
|
|
378
|
+
const evTypes = need.evidenceTypes || [];
|
|
379
|
+
const polTypes = need.policyTypes || [];
|
|
380
|
+
|
|
381
|
+
const matchedEvidence = [];
|
|
382
|
+
const matchedPolicies = [];
|
|
383
|
+
for (const t of evTypes) {
|
|
384
|
+
const rows = evidenceByType.get(t);
|
|
385
|
+
if (rows) matchedEvidence.push(...rows);
|
|
386
|
+
}
|
|
387
|
+
for (const t of polTypes) {
|
|
388
|
+
const rows = policyByType.get(t);
|
|
389
|
+
if (rows) matchedPolicies.push(...rows);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const needsEvidence = evTypes.length > 0;
|
|
393
|
+
const needsPolicy = polTypes.length > 0;
|
|
394
|
+
const hasEvidence =
|
|
395
|
+
needsEvidence && evTypes.some((t) => evidenceByType.has(t));
|
|
396
|
+
const hasPolicy = needsPolicy && polTypes.some((t) => policyByType.has(t));
|
|
397
|
+
|
|
398
|
+
// Semantics:
|
|
399
|
+
// - No requirements at all → trivially covered.
|
|
400
|
+
// - One requirement (ev OR pol) → covered iff that side has a match,
|
|
401
|
+
// otherwise gap (partial needs *something*).
|
|
402
|
+
// - Both requirements → covered iff both match, partial iff exactly
|
|
403
|
+
// one matches, gap if neither.
|
|
404
|
+
let status;
|
|
405
|
+
if (!needsEvidence && !needsPolicy) {
|
|
406
|
+
status = "covered";
|
|
407
|
+
} else if (needsEvidence && needsPolicy) {
|
|
408
|
+
if (hasEvidence && hasPolicy) status = "covered";
|
|
409
|
+
else if (hasEvidence || hasPolicy) status = "partial";
|
|
410
|
+
else status = "gap";
|
|
411
|
+
} else if (needsEvidence) {
|
|
412
|
+
status = hasEvidence ? "covered" : "gap";
|
|
413
|
+
} else {
|
|
414
|
+
status = hasPolicy ? "covered" : "gap";
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
id: ctrl.id,
|
|
419
|
+
title: ctrl.title,
|
|
420
|
+
category: ctrl.category,
|
|
421
|
+
description: ctrl.description,
|
|
422
|
+
requires: need,
|
|
423
|
+
status,
|
|
424
|
+
matchedEvidence,
|
|
425
|
+
matchedPolicies,
|
|
426
|
+
};
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const counts = { covered: 0, partial: 0, gap: 0 };
|
|
430
|
+
for (const c of controls) counts[c.status] += 1;
|
|
431
|
+
|
|
432
|
+
const total = controls.length || 1;
|
|
433
|
+
// Partial counts half. Gap counts zero.
|
|
434
|
+
const score = Math.round(
|
|
435
|
+
((counts.covered + counts.partial * 0.5) / total) * 100,
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
id: crypto.randomUUID(),
|
|
440
|
+
framework: template.id,
|
|
441
|
+
frameworkName: template.name,
|
|
442
|
+
version: template.version,
|
|
443
|
+
generatedAt: new Date().toISOString(),
|
|
444
|
+
score,
|
|
445
|
+
counts,
|
|
446
|
+
total: controls.length,
|
|
447
|
+
controls,
|
|
448
|
+
summary:
|
|
449
|
+
`${counts.covered}/${controls.length} controls covered, ` +
|
|
450
|
+
`${counts.partial} partial, ${counts.gap} gaps`,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/* ── Renderers ────────────────────────────────────────────── */
|
|
455
|
+
|
|
456
|
+
const STATUS_BADGE = {
|
|
457
|
+
covered: "✅",
|
|
458
|
+
partial: "⚠️",
|
|
459
|
+
gap: "❌",
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
export function renderMarkdown(report) {
|
|
463
|
+
const lines = [];
|
|
464
|
+
lines.push(`# ${report.frameworkName} Compliance Report`);
|
|
465
|
+
lines.push("");
|
|
466
|
+
lines.push(`- **Framework:** ${report.frameworkName} (${report.version})`);
|
|
467
|
+
lines.push(`- **Score:** ${report.score}/100`);
|
|
468
|
+
lines.push(`- **Summary:** ${report.summary}`);
|
|
469
|
+
lines.push(`- **Generated:** ${report.generatedAt}`);
|
|
470
|
+
lines.push("");
|
|
471
|
+
lines.push(
|
|
472
|
+
`| Status | Count |`,
|
|
473
|
+
`|--------|-------|`,
|
|
474
|
+
`| ✅ Covered | ${report.counts.covered} |`,
|
|
475
|
+
`| ⚠️ Partial | ${report.counts.partial} |`,
|
|
476
|
+
`| ❌ Gap | ${report.counts.gap} |`,
|
|
477
|
+
);
|
|
478
|
+
lines.push("");
|
|
479
|
+
lines.push(`## Controls`);
|
|
480
|
+
lines.push("");
|
|
481
|
+
lines.push(`| Control | Title | Category | Status | Evidence | Policies |`);
|
|
482
|
+
lines.push(`|---------|-------|----------|--------|----------|----------|`);
|
|
483
|
+
for (const c of report.controls) {
|
|
484
|
+
lines.push(
|
|
485
|
+
`| ${c.id} | ${c.title} | ${c.category} | ${STATUS_BADGE[c.status]} ${c.status} | ${c.matchedEvidence.length} | ${c.matchedPolicies.length} |`,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const gaps = report.controls.filter((c) => c.status !== "covered");
|
|
490
|
+
if (gaps.length > 0) {
|
|
491
|
+
lines.push("");
|
|
492
|
+
lines.push(`## Remediation`);
|
|
493
|
+
lines.push("");
|
|
494
|
+
for (const c of gaps) {
|
|
495
|
+
const needEv = (c.requires.evidenceTypes || []).filter(
|
|
496
|
+
(t) => !c.matchedEvidence.some((e) => e.type === t),
|
|
497
|
+
);
|
|
498
|
+
const needPol = (c.requires.policyTypes || []).filter(
|
|
499
|
+
(t) => !c.matchedPolicies.some((p) => p.type === t),
|
|
500
|
+
);
|
|
501
|
+
lines.push(`### ${c.id} — ${c.title}`);
|
|
502
|
+
lines.push("");
|
|
503
|
+
lines.push(`${c.description}`);
|
|
504
|
+
lines.push("");
|
|
505
|
+
if (needEv.length > 0) {
|
|
506
|
+
lines.push(`- Missing evidence: \`${needEv.join("`, `")}\``);
|
|
507
|
+
}
|
|
508
|
+
if (needPol.length > 0) {
|
|
509
|
+
lines.push(`- Missing policies: \`${needPol.join("`, `")}\``);
|
|
510
|
+
}
|
|
511
|
+
lines.push("");
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return lines.join("\n");
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function _htmlEscape(s) {
|
|
519
|
+
return String(s)
|
|
520
|
+
.replace(/&/g, "&")
|
|
521
|
+
.replace(/</g, "<")
|
|
522
|
+
.replace(/>/g, ">")
|
|
523
|
+
.replace(/"/g, """)
|
|
524
|
+
.replace(/'/g, "'");
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export function renderHTML(report) {
|
|
528
|
+
const rows = report.controls
|
|
529
|
+
.map(
|
|
530
|
+
(c) =>
|
|
531
|
+
`<tr class="status-${c.status}">` +
|
|
532
|
+
`<td>${_htmlEscape(c.id)}</td>` +
|
|
533
|
+
`<td>${_htmlEscape(c.title)}</td>` +
|
|
534
|
+
`<td>${_htmlEscape(c.category)}</td>` +
|
|
535
|
+
`<td>${STATUS_BADGE[c.status]} ${c.status}</td>` +
|
|
536
|
+
`<td>${c.matchedEvidence.length}</td>` +
|
|
537
|
+
`<td>${c.matchedPolicies.length}</td>` +
|
|
538
|
+
`</tr>`,
|
|
539
|
+
)
|
|
540
|
+
.join("\n");
|
|
541
|
+
return `<!doctype html>
|
|
542
|
+
<html>
|
|
543
|
+
<head>
|
|
544
|
+
<meta charset="UTF-8">
|
|
545
|
+
<title>${_htmlEscape(report.frameworkName)} Compliance Report</title>
|
|
546
|
+
<style>
|
|
547
|
+
body { font-family: system-ui, sans-serif; max-width: 960px; margin: 2em auto; padding: 0 1em; }
|
|
548
|
+
table { border-collapse: collapse; width: 100%; }
|
|
549
|
+
th, td { border: 1px solid #ddd; padding: 6px 10px; text-align: left; }
|
|
550
|
+
th { background: #f5f5f5; }
|
|
551
|
+
.status-covered { background: #e6f9ea; }
|
|
552
|
+
.status-partial { background: #fff8e1; }
|
|
553
|
+
.status-gap { background: #ffebee; }
|
|
554
|
+
.summary { margin: 1em 0; }
|
|
555
|
+
</style>
|
|
556
|
+
</head>
|
|
557
|
+
<body>
|
|
558
|
+
<h1>${_htmlEscape(report.frameworkName)} Compliance Report</h1>
|
|
559
|
+
<div class="summary">
|
|
560
|
+
<p><strong>Framework:</strong> ${_htmlEscape(report.frameworkName)} (${_htmlEscape(report.version)})</p>
|
|
561
|
+
<p><strong>Score:</strong> ${report.score}/100</p>
|
|
562
|
+
<p><strong>Summary:</strong> ${_htmlEscape(report.summary)}</p>
|
|
563
|
+
<p><strong>Generated:</strong> ${_htmlEscape(report.generatedAt)}</p>
|
|
564
|
+
</div>
|
|
565
|
+
<table>
|
|
566
|
+
<thead>
|
|
567
|
+
<tr><th>Control</th><th>Title</th><th>Category</th><th>Status</th><th>Evidence</th><th>Policies</th></tr>
|
|
568
|
+
</thead>
|
|
569
|
+
<tbody>
|
|
570
|
+
${rows}
|
|
571
|
+
</tbody>
|
|
572
|
+
</table>
|
|
573
|
+
</body>
|
|
574
|
+
</html>
|
|
575
|
+
`;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export function renderJSON(report) {
|
|
579
|
+
return JSON.stringify(report, null, 2);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* One-shot convenience: analyze + render in the requested format.
|
|
584
|
+
*/
|
|
585
|
+
export function generateFrameworkReport(frameworkId, opts = {}) {
|
|
586
|
+
const { format = "markdown", ...rest } = opts;
|
|
587
|
+
const analysis = analyzeCoverage(frameworkId, rest);
|
|
588
|
+
let body;
|
|
589
|
+
if (format === "markdown" || format === "md") body = renderMarkdown(analysis);
|
|
590
|
+
else if (format === "html") body = renderHTML(analysis);
|
|
591
|
+
else if (format === "json") body = renderJSON(analysis);
|
|
592
|
+
else throw new Error(`Unknown format: ${format}`);
|
|
593
|
+
return { analysis, body, format };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/* ── Reset (for testing) ──────────────────────────────────── */
|
|
597
|
+
|
|
598
|
+
export function _resetState() {
|
|
599
|
+
_customTemplates.clear();
|
|
600
|
+
}
|