cipher-security 5.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.
- package/bin/cipher.js +465 -0
- package/lib/api/billing.js +321 -0
- package/lib/api/compliance.js +693 -0
- package/lib/api/controls.js +1401 -0
- package/lib/api/index.js +49 -0
- package/lib/api/marketplace.js +467 -0
- package/lib/api/openai-proxy.js +383 -0
- package/lib/api/server.js +685 -0
- package/lib/autonomous/feedback-loop.js +554 -0
- package/lib/autonomous/framework.js +512 -0
- package/lib/autonomous/index.js +97 -0
- package/lib/autonomous/leaderboard.js +594 -0
- package/lib/autonomous/modes/architect.js +412 -0
- package/lib/autonomous/modes/blue.js +386 -0
- package/lib/autonomous/modes/incident.js +684 -0
- package/lib/autonomous/modes/privacy.js +369 -0
- package/lib/autonomous/modes/purple.js +294 -0
- package/lib/autonomous/modes/recon.js +250 -0
- package/lib/autonomous/parallel.js +587 -0
- package/lib/autonomous/researcher.js +583 -0
- package/lib/autonomous/runner.js +955 -0
- package/lib/autonomous/scheduler.js +615 -0
- package/lib/autonomous/task-parser.js +127 -0
- package/lib/autonomous/validators/forensic.js +266 -0
- package/lib/autonomous/validators/osint.js +216 -0
- package/lib/autonomous/validators/privacy.js +296 -0
- package/lib/autonomous/validators/purple.js +298 -0
- package/lib/autonomous/validators/sigma.js +248 -0
- package/lib/autonomous/validators/threat-model.js +363 -0
- package/lib/benchmark/agent.js +119 -0
- package/lib/benchmark/baselines.js +43 -0
- package/lib/benchmark/builder.js +143 -0
- package/lib/benchmark/config.js +35 -0
- package/lib/benchmark/coordinator.js +91 -0
- package/lib/benchmark/index.js +20 -0
- package/lib/benchmark/llm.js +58 -0
- package/lib/benchmark/models.js +137 -0
- package/lib/benchmark/reporter.js +103 -0
- package/lib/benchmark/runner.js +103 -0
- package/lib/benchmark/sandbox.js +96 -0
- package/lib/benchmark/scorer.js +32 -0
- package/lib/benchmark/solver.js +166 -0
- package/lib/benchmark/tools.js +62 -0
- package/lib/bot/bot.js +130 -0
- package/lib/commands.js +99 -0
- package/lib/complexity.js +377 -0
- package/lib/config.js +213 -0
- package/lib/gateway/client.js +309 -0
- package/lib/gateway/commands.js +830 -0
- package/lib/gateway/config-validate.js +109 -0
- package/lib/gateway/gateway.js +367 -0
- package/lib/gateway/index.js +62 -0
- package/lib/gateway/mode.js +309 -0
- package/lib/gateway/plugins.js +222 -0
- package/lib/gateway/prompt.js +214 -0
- package/lib/mcp/server.js +262 -0
- package/lib/memory/compressor.js +425 -0
- package/lib/memory/engine.js +763 -0
- package/lib/memory/evolution.js +668 -0
- package/lib/memory/index.js +58 -0
- package/lib/memory/orchestrator.js +506 -0
- package/lib/memory/retriever.js +515 -0
- package/lib/memory/synthesizer.js +333 -0
- package/lib/pipeline/async-scanner.js +510 -0
- package/lib/pipeline/binary-analysis.js +1043 -0
- package/lib/pipeline/dom-xss-scanner.js +435 -0
- package/lib/pipeline/github-actions.js +792 -0
- package/lib/pipeline/index.js +124 -0
- package/lib/pipeline/osint.js +498 -0
- package/lib/pipeline/sarif.js +373 -0
- package/lib/pipeline/scanner.js +880 -0
- package/lib/pipeline/template-manager.js +525 -0
- package/lib/pipeline/xss-scanner.js +353 -0
- package/lib/setup-wizard.js +229 -0
- package/package.json +30 -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
|
+
}
|