cipher-security 2.0.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 (76) hide show
  1. package/bin/cipher.js +566 -0
  2. package/lib/api/billing.js +321 -0
  3. package/lib/api/compliance.js +693 -0
  4. package/lib/api/controls.js +1401 -0
  5. package/lib/api/index.js +49 -0
  6. package/lib/api/marketplace.js +467 -0
  7. package/lib/api/openai-proxy.js +383 -0
  8. package/lib/api/server.js +685 -0
  9. package/lib/autonomous/feedback-loop.js +554 -0
  10. package/lib/autonomous/framework.js +512 -0
  11. package/lib/autonomous/index.js +97 -0
  12. package/lib/autonomous/leaderboard.js +594 -0
  13. package/lib/autonomous/modes/architect.js +412 -0
  14. package/lib/autonomous/modes/blue.js +386 -0
  15. package/lib/autonomous/modes/incident.js +684 -0
  16. package/lib/autonomous/modes/privacy.js +369 -0
  17. package/lib/autonomous/modes/purple.js +294 -0
  18. package/lib/autonomous/modes/recon.js +250 -0
  19. package/lib/autonomous/parallel.js +587 -0
  20. package/lib/autonomous/researcher.js +583 -0
  21. package/lib/autonomous/runner.js +955 -0
  22. package/lib/autonomous/scheduler.js +615 -0
  23. package/lib/autonomous/task-parser.js +127 -0
  24. package/lib/autonomous/validators/forensic.js +266 -0
  25. package/lib/autonomous/validators/osint.js +216 -0
  26. package/lib/autonomous/validators/privacy.js +296 -0
  27. package/lib/autonomous/validators/purple.js +298 -0
  28. package/lib/autonomous/validators/sigma.js +248 -0
  29. package/lib/autonomous/validators/threat-model.js +363 -0
  30. package/lib/benchmark/agent.js +119 -0
  31. package/lib/benchmark/baselines.js +43 -0
  32. package/lib/benchmark/builder.js +143 -0
  33. package/lib/benchmark/config.js +35 -0
  34. package/lib/benchmark/coordinator.js +91 -0
  35. package/lib/benchmark/index.js +20 -0
  36. package/lib/benchmark/llm.js +58 -0
  37. package/lib/benchmark/models.js +137 -0
  38. package/lib/benchmark/reporter.js +103 -0
  39. package/lib/benchmark/runner.js +103 -0
  40. package/lib/benchmark/sandbox.js +96 -0
  41. package/lib/benchmark/scorer.js +32 -0
  42. package/lib/benchmark/solver.js +166 -0
  43. package/lib/benchmark/tools.js +62 -0
  44. package/lib/bot/bot.js +238 -0
  45. package/lib/brand.js +105 -0
  46. package/lib/commands.js +100 -0
  47. package/lib/complexity.js +377 -0
  48. package/lib/config.js +213 -0
  49. package/lib/gateway/client.js +309 -0
  50. package/lib/gateway/commands.js +991 -0
  51. package/lib/gateway/config-validate.js +109 -0
  52. package/lib/gateway/gateway.js +367 -0
  53. package/lib/gateway/index.js +62 -0
  54. package/lib/gateway/mode.js +309 -0
  55. package/lib/gateway/plugins.js +222 -0
  56. package/lib/gateway/prompt.js +214 -0
  57. package/lib/mcp/server.js +262 -0
  58. package/lib/memory/compressor.js +425 -0
  59. package/lib/memory/engine.js +763 -0
  60. package/lib/memory/evolution.js +668 -0
  61. package/lib/memory/index.js +58 -0
  62. package/lib/memory/orchestrator.js +506 -0
  63. package/lib/memory/retriever.js +515 -0
  64. package/lib/memory/synthesizer.js +333 -0
  65. package/lib/pipeline/async-scanner.js +510 -0
  66. package/lib/pipeline/binary-analysis.js +1043 -0
  67. package/lib/pipeline/dom-xss-scanner.js +435 -0
  68. package/lib/pipeline/github-actions.js +792 -0
  69. package/lib/pipeline/index.js +124 -0
  70. package/lib/pipeline/osint.js +498 -0
  71. package/lib/pipeline/sarif.js +373 -0
  72. package/lib/pipeline/scanner.js +880 -0
  73. package/lib/pipeline/template-manager.js +525 -0
  74. package/lib/pipeline/xss-scanner.js +353 -0
  75. package/lib/setup-wizard.js +288 -0
  76. package/package.json +31 -0
@@ -0,0 +1,693 @@
1
+ // Copyright (c) 2026 defconxt. All rights reserved.
2
+ // Licensed under AGPL-3.0 — see LICENSE file for details.
3
+
4
+ /**
5
+ * CIPHER Compliance Engine — automated compliance report generation.
6
+ *
7
+ * Maps security findings and scan results to regulatory frameworks,
8
+ * generates gap analyses, compares reports, and exports CSV/JSON/Markdown.
9
+ */
10
+
11
+ import { randomUUID } from 'node:crypto';
12
+ import { CONTROL_MAPS, CATEGORY_KEYWORDS, SEVERITY_WEIGHTS } from './controls.js';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Enums (frozen objects with string constants)
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /** Supported compliance frameworks — 39 values. */
19
+ export const ComplianceFramework = Object.freeze({
20
+ SOC2: 'SOC2',
21
+ PCI_DSS: 'PCI_DSS',
22
+ HIPAA: 'HIPAA',
23
+ NIST_800_53: 'NIST_800_53',
24
+ CIS: 'CIS',
25
+ ISO_27001: 'ISO_27001',
26
+ GDPR: 'GDPR',
27
+ CMMC: 'CMMC',
28
+ NIST_CSF: 'NIST_CSF',
29
+ NIST_AI_RMF: 'NIST_AI_RMF',
30
+ FEDRAMP: 'FEDRAMP',
31
+ OWASP_TOP_10: 'OWASP_TOP_10',
32
+ MITRE_ATTACK: 'MITRE_ATTACK',
33
+ IEC_62443: 'IEC_62443',
34
+ NIS2: 'NIS2',
35
+ DORA: 'DORA',
36
+ CISA_KEV: 'CISA_KEV',
37
+ EU_AI_ACT: 'EU_AI_ACT',
38
+ SWIFT_CSP: 'SWIFT_CSP',
39
+ NIST_800_171: 'NIST_800_171',
40
+ ISO_27017: 'ISO_27017',
41
+ ISO_27018: 'ISO_27018',
42
+ ISO_27701: 'ISO_27701',
43
+ CSA_CCM: 'CSA_CCM',
44
+ NERC_CIP: 'NERC_CIP',
45
+ HITRUST_CSF: 'HITRUST_CSF',
46
+ CIS_CLOUD: 'CIS_CLOUD',
47
+ NIST_800_82: 'NIST_800_82',
48
+ OWASP_MASVS: 'OWASP_MASVS',
49
+ SANS_TOP_25: 'SANS_TOP_25',
50
+ PTES: 'PTES',
51
+ OSSTMM: 'OSSTMM',
52
+ SOX_IT: 'SOX_IT',
53
+ COBIT: 'COBIT',
54
+ ITIL_SECURITY: 'ITIL_SECURITY',
55
+ SSDF: 'SSDF',
56
+ OWASP_ASVS: 'OWASP_ASVS',
57
+ NIST_PRIVACY: 'NIST_PRIVACY',
58
+ CCPA: 'CCPA',
59
+ });
60
+
61
+ /** Assessment outcome for a single control — 5 values. */
62
+ export const ControlStatus = Object.freeze({
63
+ PASS: 'PASS',
64
+ FAIL: 'FAIL',
65
+ PARTIAL: 'PARTIAL',
66
+ NOT_APPLICABLE: 'NOT_APPLICABLE',
67
+ NOT_TESTED: 'NOT_TESTED',
68
+ });
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Data classes
72
+ // ---------------------------------------------------------------------------
73
+
74
+ /** Result of evaluating one control against collected evidence. */
75
+ export class ControlAssessment {
76
+ /**
77
+ * @param {object} opts
78
+ * @param {string} opts.controlId
79
+ * @param {string} opts.controlName
80
+ * @param {string} opts.framework
81
+ * @param {string} opts.status
82
+ * @param {string[]} [opts.evidence]
83
+ * @param {string[]} [opts.gaps]
84
+ * @param {string[]} [opts.recommendations]
85
+ * @param {string} [opts.severity]
86
+ * @param {string} [opts.testedAt]
87
+ */
88
+ constructor({
89
+ controlId,
90
+ controlName,
91
+ framework,
92
+ status,
93
+ evidence = [],
94
+ gaps = [],
95
+ recommendations = [],
96
+ severity = 'info',
97
+ testedAt = '',
98
+ }) {
99
+ this.controlId = controlId;
100
+ this.controlName = controlName;
101
+ this.framework = framework;
102
+ this.status = status;
103
+ this.evidence = evidence;
104
+ this.gaps = gaps;
105
+ this.recommendations = recommendations;
106
+ this.severity = severity;
107
+ this.testedAt = testedAt || new Date().toISOString();
108
+ }
109
+ }
110
+
111
+ /** Full compliance report for a single framework assessment. */
112
+ export class ComplianceReport {
113
+ /**
114
+ * @param {object} opts
115
+ * @param {string} opts.reportId
116
+ * @param {string} opts.framework
117
+ * @param {string} opts.scope
118
+ * @param {string} [opts.assessor]
119
+ * @param {number} [opts.controlsTotal]
120
+ * @param {number} [opts.controlsPass]
121
+ * @param {number} [opts.controlsFail]
122
+ * @param {number} [opts.controlsPartial]
123
+ * @param {number} [opts.controlsNa]
124
+ * @param {number} [opts.overallScore]
125
+ * @param {ControlAssessment[]} [opts.assessments]
126
+ * @param {string} [opts.executiveSummary]
127
+ * @param {string} [opts.createdAt]
128
+ */
129
+ constructor({
130
+ reportId,
131
+ framework,
132
+ scope,
133
+ assessor = 'CIPHER Automated Assessment',
134
+ controlsTotal = 0,
135
+ controlsPass = 0,
136
+ controlsFail = 0,
137
+ controlsPartial = 0,
138
+ controlsNa = 0,
139
+ overallScore = 0,
140
+ assessments = [],
141
+ executiveSummary = '',
142
+ createdAt = '',
143
+ }) {
144
+ this.reportId = reportId;
145
+ this.framework = framework;
146
+ this.scope = scope;
147
+ this.assessor = assessor;
148
+ this.controlsTotal = controlsTotal;
149
+ this.controlsPass = controlsPass;
150
+ this.controlsFail = controlsFail;
151
+ this.controlsPartial = controlsPartial;
152
+ this.controlsNa = controlsNa;
153
+ this.overallScore = overallScore;
154
+ this.assessments = assessments;
155
+ this.executiveSummary = executiveSummary;
156
+ this.createdAt = createdAt || new Date().toISOString();
157
+ }
158
+
159
+ /** Serialise the report to a plain object. */
160
+ toDict() {
161
+ return {
162
+ report_id: this.reportId,
163
+ framework: this.framework,
164
+ scope: this.scope,
165
+ assessor: this.assessor,
166
+ controls_total: this.controlsTotal,
167
+ controls_pass: this.controlsPass,
168
+ controls_fail: this.controlsFail,
169
+ controls_partial: this.controlsPartial,
170
+ controls_na: this.controlsNa,
171
+ overall_score: Math.round(this.overallScore * 100) / 100,
172
+ executive_summary: this.executiveSummary,
173
+ created_at: this.createdAt,
174
+ assessments: this.assessments.map((a) => ({
175
+ control_id: a.controlId,
176
+ control_name: a.controlName,
177
+ framework: a.framework,
178
+ status: a.status,
179
+ evidence: a.evidence,
180
+ gaps: a.gaps,
181
+ recommendations: a.recommendations,
182
+ severity: a.severity,
183
+ tested_at: a.testedAt,
184
+ })),
185
+ };
186
+ }
187
+
188
+ /** Serialise the report to a JSON string. */
189
+ toJson() {
190
+ return JSON.stringify(this.toDict(), null, 2);
191
+ }
192
+
193
+ /** Render the report as a Markdown document. */
194
+ toMarkdown() {
195
+ const statusIcon = {
196
+ [ControlStatus.PASS]: '✅',
197
+ [ControlStatus.FAIL]: '❌',
198
+ [ControlStatus.PARTIAL]: '⚠️',
199
+ [ControlStatus.NOT_APPLICABLE]: '➖',
200
+ [ControlStatus.NOT_TESTED]: '❓',
201
+ };
202
+
203
+ const lines = [
204
+ `# Compliance Report — ${this.framework}`,
205
+ '',
206
+ `**Report ID:** ${this.reportId} `,
207
+ `**Scope:** ${this.scope} `,
208
+ `**Assessor:** ${this.assessor} `,
209
+ `**Created:** ${this.createdAt} `,
210
+ '',
211
+ '## Executive Summary',
212
+ '',
213
+ this.executiveSummary || '_No summary generated._',
214
+ '',
215
+ '## Score Overview',
216
+ '',
217
+ '| Metric | Value |',
218
+ '|--------|-------|',
219
+ `| Overall Score | ${this.overallScore.toFixed(1)}% |`,
220
+ `| Total Controls | ${this.controlsTotal} |`,
221
+ `| Pass | ${this.controlsPass} |`,
222
+ `| Fail | ${this.controlsFail} |`,
223
+ `| Partial | ${this.controlsPartial} |`,
224
+ `| N/A | ${this.controlsNa} |`,
225
+ '',
226
+ '## Control Assessments',
227
+ '',
228
+ ];
229
+
230
+ for (const a of this.assessments) {
231
+ const icon = statusIcon[a.status] || '';
232
+ lines.push(`### ${icon} ${a.controlId} — ${a.controlName}`);
233
+ lines.push('');
234
+ lines.push(`**Status:** ${a.status} | **Severity:** ${a.severity}`);
235
+ lines.push('');
236
+ if (a.evidence.length) {
237
+ lines.push('**Evidence:**');
238
+ for (const e of a.evidence) lines.push(`- ${e}`);
239
+ lines.push('');
240
+ }
241
+ if (a.gaps.length) {
242
+ lines.push('**Gaps:**');
243
+ for (const g of a.gaps) lines.push(`- ${g}`);
244
+ lines.push('');
245
+ }
246
+ if (a.recommendations.length) {
247
+ lines.push('**Recommendations:**');
248
+ for (const r of a.recommendations) lines.push(`- ${r}`);
249
+ lines.push('');
250
+ }
251
+ }
252
+
253
+ return lines.join('\n');
254
+ }
255
+ }
256
+
257
+ // ---------------------------------------------------------------------------
258
+ // Engine
259
+ // ---------------------------------------------------------------------------
260
+
261
+ /** Map security findings to compliance controls and generate reports. */
262
+ export class ComplianceEngine {
263
+ constructor() {
264
+ // Build category → control ID index per framework
265
+ /** @type {Map<string, Map<string, string[]>>} */
266
+ this._categoryToControls = new Map();
267
+ for (const [fw, controls] of Object.entries(CONTROL_MAPS)) {
268
+ const catMap = new Map();
269
+ for (const ctrl of controls) {
270
+ const existing = catMap.get(ctrl.category) || [];
271
+ existing.push(ctrl.id);
272
+ catMap.set(ctrl.category, existing);
273
+ }
274
+ this._categoryToControls.set(fw, catMap);
275
+ }
276
+ }
277
+
278
+ // -- public API -----------------------------------------------------------
279
+
280
+ /**
281
+ * Map a list of security findings to framework controls.
282
+ *
283
+ * Each finding dict should have at least:
284
+ * - title (string)
285
+ * - severity (string): critical/high/medium/low/info
286
+ * Optional keys: description, category, remediation, evidence, location
287
+ *
288
+ * @param {object[]} findings
289
+ * @param {string} framework - ComplianceFramework value
290
+ * @param {string} [scope='']
291
+ * @returns {ComplianceReport}
292
+ */
293
+ assessFromFindings(findings, framework, scope = '') {
294
+ const controls = CONTROL_MAPS[framework];
295
+ if (!controls) {
296
+ throw new Error(`Unknown compliance framework: ${framework}`);
297
+ }
298
+ const now = new Date().toISOString();
299
+
300
+ // Build map: controlId → matched findings
301
+ const ctrlFindings = new Map();
302
+ for (const ctrl of controls) {
303
+ ctrlFindings.set(ctrl.id, []);
304
+ }
305
+
306
+ for (const finding of findings) {
307
+ if (!finding || typeof finding !== 'object') continue;
308
+ const matchedIds = this._mapFindingToControls(finding, framework);
309
+ for (const cid of matchedIds) {
310
+ const arr = ctrlFindings.get(cid);
311
+ if (arr) arr.push(finding);
312
+ }
313
+ }
314
+
315
+ const assessments = [];
316
+ for (const ctrl of controls) {
317
+ const matched = ctrlFindings.get(ctrl.id);
318
+ if (!matched || matched.length === 0) {
319
+ assessments.push(
320
+ new ControlAssessment({
321
+ controlId: ctrl.id,
322
+ controlName: ctrl.name,
323
+ framework,
324
+ status: ControlStatus.NOT_TESTED,
325
+ severity: 'info',
326
+ testedAt: now,
327
+ }),
328
+ );
329
+ continue;
330
+ }
331
+
332
+ // Determine status from findings
333
+ const severities = matched.map((f) => (f.severity || 'info').toLowerCase());
334
+ const worst = ComplianceEngine._worstSeverity(severities);
335
+ const hasCritical = severities.includes('critical') || severities.includes('high');
336
+ const hasMedium = severities.includes('medium');
337
+
338
+ let status;
339
+ if (hasCritical) {
340
+ status = ControlStatus.FAIL;
341
+ } else if (hasMedium) {
342
+ status = ControlStatus.PARTIAL;
343
+ } else {
344
+ status = ControlStatus.PASS;
345
+ }
346
+
347
+ const evidence = matched.map((f) => f.title || 'untitled');
348
+ const gaps = matched
349
+ .filter((f) => ['critical', 'high', 'medium'].includes((f.severity || '').toLowerCase()))
350
+ .map((f) => f.description || f.title || '');
351
+ const recs = matched.filter((f) => f.remediation).map((f) => f.remediation);
352
+
353
+ assessments.push(
354
+ new ControlAssessment({
355
+ controlId: ctrl.id,
356
+ controlName: ctrl.name,
357
+ framework,
358
+ status,
359
+ evidence,
360
+ gaps,
361
+ recommendations: recs,
362
+ severity: worst,
363
+ testedAt: now,
364
+ }),
365
+ );
366
+ }
367
+
368
+ return this._buildReport(assessments, framework, scope);
369
+ }
370
+
371
+ /**
372
+ * Map scan results to compliance controls.
373
+ *
374
+ * @param {object} scanResults - { scope, findings, metadata }
375
+ * @param {string} framework - ComplianceFramework value
376
+ * @returns {ComplianceReport}
377
+ */
378
+ assessFromScan(scanResults, framework) {
379
+ const findings = scanResults?.findings || [];
380
+ const scope = scanResults?.scope || 'scan target';
381
+ return this.assessFromFindings(findings, framework, scope);
382
+ }
383
+
384
+ /**
385
+ * Return a prioritised remediation plan from failing controls.
386
+ * @param {ComplianceReport} report
387
+ * @returns {object}
388
+ */
389
+ generateGapAnalysis(report) {
390
+ const severityOrder = ['critical', 'high', 'medium', 'low', 'info'];
391
+ const failing = report.assessments.filter(
392
+ (a) => a.status === ControlStatus.FAIL || a.status === ControlStatus.PARTIAL,
393
+ );
394
+ failing.sort((a, b) => {
395
+ const idxA = severityOrder.indexOf(a.severity);
396
+ const idxB = severityOrder.indexOf(b.severity);
397
+ return (idxA === -1 ? 99 : idxA) - (idxB === -1 ? 99 : idxB);
398
+ });
399
+
400
+ const remediationItems = failing.map((a, idx) => ({
401
+ priority: idx + 1,
402
+ control_id: a.controlId,
403
+ control_name: a.controlName,
404
+ status: a.status,
405
+ severity: a.severity,
406
+ gaps: a.gaps,
407
+ recommendations: a.recommendations,
408
+ }));
409
+
410
+ const notTested = report.assessments.filter((a) => a.status === ControlStatus.NOT_TESTED);
411
+
412
+ return {
413
+ framework: report.framework,
414
+ report_id: report.reportId,
415
+ overall_score: Math.round(report.overallScore * 100) / 100,
416
+ total_gaps: failing.length,
417
+ remediation_plan: remediationItems,
418
+ untested_controls: notTested.map((a) => ({
419
+ control_id: a.controlId,
420
+ control_name: a.controlName,
421
+ })),
422
+ summary:
423
+ `${failing.length} controls require remediation; ` +
424
+ `${notTested.length} controls have not been tested. ` +
425
+ `Current score: ${report.overallScore.toFixed(1)}%.`,
426
+ };
427
+ }
428
+
429
+ /**
430
+ * Diff two reports of the same framework to show progress over time.
431
+ * @param {ComplianceReport} reportA
432
+ * @param {ComplianceReport} reportB
433
+ * @returns {object}
434
+ */
435
+ compareReports(reportA, reportB) {
436
+ const mapA = new Map(reportA.assessments.map((a) => [a.controlId, a]));
437
+ const mapB = new Map(reportB.assessments.map((a) => [a.controlId, a]));
438
+
439
+ const allIds = [...new Set([...mapA.keys(), ...mapB.keys()])].sort();
440
+
441
+ const statusRank = {
442
+ [ControlStatus.FAIL]: 0,
443
+ [ControlStatus.PARTIAL]: 1,
444
+ [ControlStatus.NOT_TESTED]: 2,
445
+ [ControlStatus.NOT_APPLICABLE]: 3,
446
+ [ControlStatus.PASS]: 4,
447
+ };
448
+
449
+ const improved = [];
450
+ const regressed = [];
451
+ const unchanged = [];
452
+ const newControls = [];
453
+ const removedControls = [];
454
+
455
+ for (const cid of allIds) {
456
+ const aCtrl = mapA.get(cid);
457
+ const bCtrl = mapB.get(cid);
458
+
459
+ if (aCtrl && !bCtrl) {
460
+ removedControls.push(cid);
461
+ } else if (bCtrl && !aCtrl) {
462
+ newControls.push(cid);
463
+ } else if (aCtrl && bCtrl) {
464
+ const rankA = statusRank[aCtrl.status] ?? -1;
465
+ const rankB = statusRank[bCtrl.status] ?? -1;
466
+ if (rankB > rankA) {
467
+ improved.push({ control_id: cid, from: aCtrl.status, to: bCtrl.status });
468
+ } else if (rankB < rankA) {
469
+ regressed.push({ control_id: cid, from: aCtrl.status, to: bCtrl.status });
470
+ } else {
471
+ unchanged.push(cid);
472
+ }
473
+ }
474
+ }
475
+
476
+ const scoreDelta = reportB.overallScore - reportA.overallScore;
477
+
478
+ return {
479
+ report_a_id: reportA.reportId,
480
+ report_b_id: reportB.reportId,
481
+ framework: reportA.framework,
482
+ score_a: Math.round(reportA.overallScore * 100) / 100,
483
+ score_b: Math.round(reportB.overallScore * 100) / 100,
484
+ score_delta: Math.round(scoreDelta * 100) / 100,
485
+ improved,
486
+ regressed,
487
+ unchanged_count: unchanged.length,
488
+ new_controls: newControls,
489
+ removed_controls: removedControls,
490
+ summary:
491
+ `Score changed from ${reportA.overallScore.toFixed(1)}% to ` +
492
+ `${reportB.overallScore.toFixed(1)}% (Δ ${scoreDelta >= 0 ? '+' : ''}${scoreDelta.toFixed(1)}%). ` +
493
+ `${improved.length} improved, ${regressed.length} regressed.`,
494
+ };
495
+ }
496
+
497
+ /**
498
+ * Export report as CSV for import into GRC tools.
499
+ * @param {ComplianceReport} report
500
+ * @returns {string}
501
+ */
502
+ exportCsv(report) {
503
+ const escape = (val) => {
504
+ const s = String(val);
505
+ if (s.includes(',') || s.includes('"') || s.includes('\n')) {
506
+ return `"${s.replace(/"/g, '""')}"`;
507
+ }
508
+ return s;
509
+ };
510
+
511
+ const headers = [
512
+ 'Report ID',
513
+ 'Framework',
514
+ 'Control ID',
515
+ 'Control Name',
516
+ 'Status',
517
+ 'Severity',
518
+ 'Evidence',
519
+ 'Gaps',
520
+ 'Recommendations',
521
+ 'Tested At',
522
+ ];
523
+
524
+ const rows = [headers.map(escape).join(',')];
525
+ for (const a of report.assessments) {
526
+ rows.push(
527
+ [
528
+ report.reportId,
529
+ report.framework,
530
+ a.controlId,
531
+ a.controlName,
532
+ a.status,
533
+ a.severity,
534
+ a.evidence.join('; '),
535
+ a.gaps.join('; '),
536
+ a.recommendations.join('; '),
537
+ a.testedAt,
538
+ ]
539
+ .map(escape)
540
+ .join(','),
541
+ );
542
+ }
543
+ return rows.join('\n') + '\n';
544
+ }
545
+
546
+ // -- private helpers ------------------------------------------------------
547
+
548
+ /**
549
+ * Match a finding to relevant control IDs based on category keywords.
550
+ * @param {object} finding
551
+ * @param {string} framework
552
+ * @returns {string[]}
553
+ */
554
+ _mapFindingToControls(finding, framework) {
555
+ const text = [
556
+ String(finding.title || ''),
557
+ String(finding.description || ''),
558
+ String(finding.category || ''),
559
+ ]
560
+ .join(' ')
561
+ .toLowerCase();
562
+
563
+ const matchedCategories = new Set();
564
+ for (const [category, keywords] of Object.entries(CATEGORY_KEYWORDS)) {
565
+ for (const kw of keywords) {
566
+ if (text.includes(kw)) {
567
+ matchedCategories.add(category);
568
+ break;
569
+ }
570
+ }
571
+ }
572
+
573
+ // Fallback: try explicit category field
574
+ if (matchedCategories.size === 0) {
575
+ const explicit = (finding.category || '').toLowerCase();
576
+ if (CATEGORY_KEYWORDS[explicit]) {
577
+ matchedCategories.add(explicit);
578
+ }
579
+ }
580
+
581
+ const catMap = this._categoryToControls.get(framework);
582
+ if (!catMap) return [];
583
+
584
+ const controlIds = [];
585
+ const seen = new Set();
586
+ for (const cat of matchedCategories) {
587
+ for (const cid of catMap.get(cat) || []) {
588
+ if (!seen.has(cid)) {
589
+ seen.add(cid);
590
+ controlIds.push(cid);
591
+ }
592
+ }
593
+ }
594
+
595
+ return controlIds;
596
+ }
597
+
598
+ /**
599
+ * Weighted compliance score (0-100).
600
+ *
601
+ * PASS = full weight, PARTIAL = 50%, FAIL/NOT_TESTED = 0, NA excluded.
602
+ * @param {ControlAssessment[]} assessments
603
+ * @returns {number}
604
+ */
605
+ _calculateScore(assessments) {
606
+ let totalWeight = 0;
607
+ let earnedWeight = 0;
608
+
609
+ for (const a of assessments) {
610
+ if (a.status === ControlStatus.NOT_APPLICABLE) continue;
611
+ const weight = SEVERITY_WEIGHTS[a.severity] ?? 0.5;
612
+ totalWeight += weight;
613
+ if (a.status === ControlStatus.PASS) {
614
+ earnedWeight += weight;
615
+ } else if (a.status === ControlStatus.PARTIAL) {
616
+ earnedWeight += weight * 0.5;
617
+ }
618
+ }
619
+
620
+ if (totalWeight === 0) return 0;
621
+ return (earnedWeight / totalWeight) * 100;
622
+ }
623
+
624
+ /**
625
+ * Return the most severe level from a list.
626
+ * @param {string[]} severities
627
+ * @returns {string}
628
+ */
629
+ static _worstSeverity(severities) {
630
+ const order = ['critical', 'high', 'medium', 'low', 'info'];
631
+ for (const level of order) {
632
+ if (severities.includes(level)) return level;
633
+ }
634
+ return 'info';
635
+ }
636
+
637
+ /**
638
+ * Assemble a ComplianceReport from a list of assessments.
639
+ * @param {ControlAssessment[]} assessments
640
+ * @param {string} framework
641
+ * @param {string} scope
642
+ * @returns {ComplianceReport}
643
+ */
644
+ _buildReport(assessments, framework, scope) {
645
+ const score = this._calculateScore(assessments);
646
+ const counts = {
647
+ [ControlStatus.PASS]: 0,
648
+ [ControlStatus.FAIL]: 0,
649
+ [ControlStatus.PARTIAL]: 0,
650
+ [ControlStatus.NOT_APPLICABLE]: 0,
651
+ [ControlStatus.NOT_TESTED]: 0,
652
+ };
653
+ for (const a of assessments) {
654
+ counts[a.status] = (counts[a.status] || 0) + 1;
655
+ }
656
+
657
+ const total = assessments.length;
658
+ const passCount = counts[ControlStatus.PASS];
659
+ const failCount = counts[ControlStatus.FAIL];
660
+ const partialCount = counts[ControlStatus.PARTIAL];
661
+ const naCount = counts[ControlStatus.NOT_APPLICABLE];
662
+
663
+ const summaryParts = [
664
+ `Assessment of ${total} ${framework} controls completed.`,
665
+ `${passCount} passed, ${failCount} failed, ${partialCount} partial, ${naCount} N/A.`,
666
+ `Overall compliance score: ${score.toFixed(1)}%.`,
667
+ ];
668
+ if (failCount) {
669
+ const criticalFails = assessments.filter(
670
+ (a) => a.status === ControlStatus.FAIL && a.severity === 'critical',
671
+ );
672
+ if (criticalFails.length) {
673
+ summaryParts.push(
674
+ `CRITICAL: ${criticalFails.length} controls have critical failures requiring immediate remediation.`,
675
+ );
676
+ }
677
+ }
678
+
679
+ return new ComplianceReport({
680
+ reportId: randomUUID(),
681
+ framework,
682
+ scope: scope || 'Full environment',
683
+ controlsTotal: total,
684
+ controlsPass: passCount,
685
+ controlsFail: failCount,
686
+ controlsPartial: partialCount,
687
+ controlsNa: naCount,
688
+ overallScore: Math.round(score * 100) / 100,
689
+ assessments,
690
+ executiveSummary: summaryParts.join(' '),
691
+ });
692
+ }
693
+ }