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,248 @@
1
+ // Copyright (c) 2026 defconxt. All rights reserved.
2
+ // Licensed under AGPL-3.0 — see LICENSE file for details.
3
+ // CIPHER is a trademark of defconxt.
4
+
5
+ /**
6
+ * Sigma rule validator for BLUE mode agent output.
7
+ *
8
+ * Ported from autonomous/validators/sigma.py.
9
+ * Validates LLM-generated Sigma detection rules for structural correctness:
10
+ * required fields, recommended fields, value constraints, detection structure,
11
+ * logsource structure, and ATT&CK tag format.
12
+ *
13
+ * Uses yaml npm package's YAML.parseAllDocuments() for multi-document YAML.
14
+ * Uses findall+join pattern for code fence stripping (per KNOWLEDGE.md).
15
+ *
16
+ * @module autonomous/validators/sigma
17
+ */
18
+
19
+ import YAML from 'yaml';
20
+ import { ValidationResult } from '../framework.js';
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Constants
24
+ // ---------------------------------------------------------------------------
25
+
26
+ const REQUIRED_FIELDS = ['title', 'logsource', 'detection'];
27
+
28
+ const RECOMMENDED_FIELDS = [
29
+ 'id', 'status', 'description', 'level', 'tags', 'falsepositives',
30
+ ];
31
+
32
+ const VALID_LEVELS = ['critical', 'high', 'medium', 'low', 'informational'];
33
+
34
+ const VALID_STATUSES = ['stable', 'test', 'experimental', 'deprecated', 'unsupported'];
35
+
36
+ // Matches ATT&CK technique IDs like attack.t1059 or attack.t1059.001
37
+ const ATTACK_TECHNIQUE_RE = /attack\.t\d{4}(\.\d{3})?/;
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Helpers
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /**
44
+ * Extract YAML content from markdown code fences, or return text as-is.
45
+ * Uses findall+join (not replace) to discard surrounding prose.
46
+ *
47
+ * @param {string} text
48
+ * @returns {string}
49
+ */
50
+ export function stripCodeFences(text) {
51
+ const matches = [...text.matchAll(/```(?:ya?ml)?\s*\n(.*?)```/gs)].map(m => m[1]);
52
+ if (matches.length > 0) {
53
+ return matches.join('\n---\n');
54
+ }
55
+ return text;
56
+ }
57
+
58
+ /**
59
+ * Parse multi-document YAML into a list of objects.
60
+ * Uses YAML.parseAllDocuments() then .toJSON() on each document, filtering nulls.
61
+ *
62
+ * @param {string} text
63
+ * @returns {Object[]}
64
+ * @throws {Error} If YAML parsing fails
65
+ */
66
+ export function parseYamlDocuments(text) {
67
+ const stripped = stripCodeFences(text);
68
+ let docs;
69
+ try {
70
+ docs = YAML.parseAllDocuments(stripped);
71
+ } catch (e) {
72
+ throw new Error(`YAML parse error: ${e.message}`);
73
+ }
74
+
75
+ return docs
76
+ .map(doc => {
77
+ if (doc.errors && doc.errors.length > 0) {
78
+ throw new Error(`YAML parse error: ${doc.errors[0].message}`);
79
+ }
80
+ return doc.toJSON();
81
+ })
82
+ .filter(doc => doc != null);
83
+ }
84
+
85
+ /**
86
+ * Validate a single Sigma rule dict.
87
+ *
88
+ * @param {Object} rule
89
+ * @returns {{ errors: string[], warnings: string[] }}
90
+ */
91
+ function validateSingleRule(rule) {
92
+ const errors = [];
93
+ const warnings = [];
94
+
95
+ // Required fields
96
+ for (const fieldName of REQUIRED_FIELDS) {
97
+ if (!(fieldName in rule)) {
98
+ errors.push(`Missing required field: ${fieldName}`);
99
+ }
100
+ }
101
+
102
+ // Recommended fields
103
+ for (const fieldName of RECOMMENDED_FIELDS) {
104
+ if (!(fieldName in rule)) {
105
+ warnings.push(`Missing recommended field: ${fieldName}`);
106
+ }
107
+ }
108
+
109
+ // Level validation
110
+ if ('level' in rule && !VALID_LEVELS.includes(rule.level)) {
111
+ errors.push(
112
+ `Invalid level: ${rule.level} (must be one of ${JSON.stringify(VALID_LEVELS)})`
113
+ );
114
+ }
115
+
116
+ // Status validation (warning, not error)
117
+ if ('status' in rule && !VALID_STATUSES.includes(rule.status)) {
118
+ warnings.push(`Non-standard status: ${rule.status}`);
119
+ }
120
+
121
+ // Detection structure
122
+ const detection = rule.detection;
123
+ if (detection && typeof detection === 'object' && !Array.isArray(detection)) {
124
+ if (!('condition' in detection)) {
125
+ errors.push("Detection missing 'condition' field");
126
+ }
127
+ if (Object.keys(detection).length < 2) {
128
+ warnings.push('Detection has condition but no selection criteria');
129
+ }
130
+ }
131
+
132
+ // Logsource structure
133
+ const logsource = rule.logsource;
134
+ if (logsource && typeof logsource === 'object' && !Array.isArray(logsource)) {
135
+ if (!('category' in logsource) && !('product' in logsource) && !('service' in logsource)) {
136
+ warnings.push('Logsource missing category/product/service');
137
+ }
138
+ }
139
+
140
+ // ATT&CK tags
141
+ const tags = rule.tags;
142
+ if (Array.isArray(tags) && tags.length > 0) {
143
+ const hasTechniqueId = tags.some(tag => ATTACK_TECHNIQUE_RE.test(String(tag)));
144
+ if (!hasTechniqueId) {
145
+ warnings.push(
146
+ 'Tags contain no ATT&CK technique IDs ' +
147
+ '(expected pattern: attack.tNNNN or attack.tNNNN.NNN)'
148
+ );
149
+ }
150
+ }
151
+
152
+ return { errors, warnings };
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // SigmaValidator
157
+ // ---------------------------------------------------------------------------
158
+
159
+ /**
160
+ * Deterministic validator for Sigma detection rule YAML.
161
+ *
162
+ * Scoring:
163
+ * - 1.0: no errors and no warnings (clean)
164
+ * - 0.5: warnings only (valid but imperfect)
165
+ * - 0.0: any errors (invalid)
166
+ */
167
+ export class SigmaValidator {
168
+ /**
169
+ * Validate Sigma rule(s) in the agent result's output text.
170
+ *
171
+ * @param {import('../framework.js').ModeAgentResult} result
172
+ * @returns {import('../framework.js').ValidationResult}
173
+ */
174
+ validate(result) {
175
+ const text = result.outputText || '';
176
+
177
+ // Parse YAML
178
+ let documents;
179
+ try {
180
+ documents = parseYamlDocuments(text);
181
+ } catch (e) {
182
+ return new ValidationResult({
183
+ valid: false,
184
+ errors: [e.message],
185
+ warnings: [],
186
+ score: 0.0,
187
+ metadata: { rules_count: 0 },
188
+ });
189
+ }
190
+
191
+ // Empty output
192
+ if (documents.length === 0) {
193
+ return new ValidationResult({
194
+ valid: false,
195
+ errors: ['No Sigma rules found in output'],
196
+ warnings: [],
197
+ score: 0.0,
198
+ metadata: { rules_count: 0 },
199
+ });
200
+ }
201
+
202
+ // Validate each document
203
+ const allErrors = [];
204
+ const allWarnings = [];
205
+ const rules = [];
206
+
207
+ for (let i = 0; i < documents.length; i++) {
208
+ const doc = documents[i];
209
+ if (typeof doc !== 'object' || doc === null || Array.isArray(doc)) {
210
+ allErrors.push(
211
+ `Document ${i + 1} is not a YAML mapping ` +
212
+ `(got ${Array.isArray(doc) ? 'Array' : typeof doc})`
213
+ );
214
+ continue;
215
+ }
216
+
217
+ rules.push(doc);
218
+ const { errors: errs, warnings: warns } = validateSingleRule(doc);
219
+
220
+ // Prefix multi-rule errors with document index
221
+ if (documents.length > 1) {
222
+ allErrors.push(...errs.map(e => `Rule ${i + 1}: ${e}`));
223
+ allWarnings.push(...warns.map(w => `Rule ${i + 1}: ${w}`));
224
+ } else {
225
+ allErrors.push(...errs);
226
+ allWarnings.push(...warns);
227
+ }
228
+ }
229
+
230
+ // Scoring
231
+ let score;
232
+ if (allErrors.length > 0) {
233
+ score = 0.0;
234
+ } else if (allWarnings.length > 0) {
235
+ score = 0.5;
236
+ } else {
237
+ score = 1.0;
238
+ }
239
+
240
+ return new ValidationResult({
241
+ valid: allErrors.length === 0,
242
+ errors: allErrors,
243
+ warnings: allWarnings,
244
+ score,
245
+ metadata: { rules_count: rules.length },
246
+ });
247
+ }
248
+ }
@@ -0,0 +1,363 @@
1
+ // Copyright (c) 2026 defconxt. All rights reserved.
2
+ // Licensed under AGPL-3.0 — see LICENSE file for details.
3
+ // CIPHER is a trademark of defconxt.
4
+
5
+ /**
6
+ * STRIDE/DREAD threat model validator for ARCHITECT mode agent output.
7
+ *
8
+ * Ported from autonomous/validators/threat_model.py.
9
+ * Validates LLM-generated JSON threat models for structural correctness:
10
+ * required sections, STRIDE threat type validation, DREAD dimension score
11
+ * ranges (1-10 integers across 5 dimensions), Mermaid DFD syntax, and
12
+ * recommended fields.
13
+ *
14
+ * @module autonomous/validators/threat-model
15
+ */
16
+
17
+ import { ValidationResult } from '../framework.js';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Constants
21
+ // ---------------------------------------------------------------------------
22
+
23
+ const REQUIRED_SECTIONS = [
24
+ 'system_description', 'components', 'trust_boundaries',
25
+ 'stride_analysis', 'dread_scores', 'mitigations', 'dfd_mermaid',
26
+ ];
27
+
28
+ const RECOMMENDED_FIELDS = [
29
+ 'risk_matrix', 'attack_trees', 'residual_risks',
30
+ ];
31
+
32
+ // Accept both single-letter abbreviations and full STRIDE names
33
+ const VALID_STRIDE_ABBREVIATIONS = new Set(['S', 'T', 'R', 'I', 'D', 'E']);
34
+
35
+ const VALID_STRIDE_FULL_NAMES = new Set([
36
+ 'Spoofing', 'Tampering', 'Repudiation',
37
+ 'Information Disclosure', 'Denial of Service', 'Elevation of Privilege',
38
+ ]);
39
+
40
+ const STRIDE_FULL_NAMES_LOWER = new Set(
41
+ [...VALID_STRIDE_FULL_NAMES].map(n => n.toLowerCase())
42
+ );
43
+
44
+ const DREAD_DIMENSIONS = [
45
+ 'Damage', 'Reproducibility', 'Exploitability', 'Affected Users', 'Discoverability',
46
+ ];
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Helpers
50
+ // ---------------------------------------------------------------------------
51
+
52
+ /**
53
+ * Extract JSON content from markdown code fences, or return text as-is.
54
+ *
55
+ * @param {string} text
56
+ * @returns {string}
57
+ */
58
+ function stripCodeFences(text) {
59
+ let matches = [...text.matchAll(/```json\s*\n(.*?)```/gs)].map(m => m[1]);
60
+ if (matches.length > 0) return matches.join('\n');
61
+
62
+ matches = [...text.matchAll(/```\s*\n(.*?)```/gs)].map(m => m[1]);
63
+ if (matches.length > 0) return matches.join('\n');
64
+
65
+ return text;
66
+ }
67
+
68
+ /**
69
+ * Parse a JSON threat model report from text.
70
+ *
71
+ * @param {string} text
72
+ * @returns {Object}
73
+ * @throws {Error}
74
+ */
75
+ function parseJsonReport(text) {
76
+ const stripped = stripCodeFences(text);
77
+ try {
78
+ return JSON.parse(stripped);
79
+ } catch (e) {
80
+ throw new Error(`JSON parse error: ${e.message}`);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Check if a threat type is a valid STRIDE category.
86
+ *
87
+ * @param {string} threatType
88
+ * @returns {boolean}
89
+ */
90
+ function isValidStrideType(threatType) {
91
+ if (VALID_STRIDE_ABBREVIATIONS.has(threatType)) return true;
92
+ if (STRIDE_FULL_NAMES_LOWER.has(threatType.toLowerCase())) return true;
93
+ return false;
94
+ }
95
+
96
+ /**
97
+ * Validate a threat model report dict for structural correctness.
98
+ *
99
+ * @param {Object} report
100
+ * @returns {{ errors: string[], warnings: string[] }}
101
+ */
102
+ function validateReport(report) {
103
+ const errors = [];
104
+ const warnings = [];
105
+
106
+ // Required top-level sections
107
+ for (const section of REQUIRED_SECTIONS) {
108
+ if (!(section in report)) {
109
+ errors.push(`Missing required section: ${section}`);
110
+ }
111
+ }
112
+
113
+ // components must be a non-empty list
114
+ const components = report.components;
115
+ if (components !== undefined) {
116
+ if (!Array.isArray(components)) {
117
+ errors.push(`components must be a list (got ${typeof components})`);
118
+ } else if (components.length === 0) {
119
+ errors.push('components list must not be empty');
120
+ }
121
+ }
122
+
123
+ // trust_boundaries must be a non-empty list
124
+ const trustBoundaries = report.trust_boundaries;
125
+ if (trustBoundaries !== undefined) {
126
+ if (!Array.isArray(trustBoundaries)) {
127
+ errors.push(`trust_boundaries must be a list (got ${typeof trustBoundaries})`);
128
+ } else if (trustBoundaries.length === 0) {
129
+ errors.push('trust_boundaries list must not be empty');
130
+ }
131
+ }
132
+
133
+ // stride_analysis must be a non-empty list with required keys
134
+ const strideAnalysis = report.stride_analysis;
135
+ if (strideAnalysis !== undefined) {
136
+ if (!Array.isArray(strideAnalysis)) {
137
+ errors.push(`stride_analysis must be a list (got ${typeof strideAnalysis})`);
138
+ } else if (strideAnalysis.length === 0) {
139
+ errors.push('stride_analysis list must not be empty');
140
+ } else {
141
+ const requiredKeys = ['component', 'threat_type', 'description', 'dread_score'];
142
+ for (let i = 0; i < strideAnalysis.length; i++) {
143
+ const item = strideAnalysis[i];
144
+ if (typeof item !== 'object' || item === null || Array.isArray(item)) {
145
+ errors.push(`stride_analysis[${i}] must be a dict (got ${Array.isArray(item) ? 'Array' : typeof item})`);
146
+ continue;
147
+ }
148
+ for (const key of requiredKeys) {
149
+ if (!(key in item)) {
150
+ errors.push(`stride_analysis[${i}] missing required key: ${key}`);
151
+ }
152
+ }
153
+ // Validate threat_type
154
+ const threatType = item.threat_type || '';
155
+ if (threatType && !isValidStrideType(String(threatType))) {
156
+ errors.push(
157
+ `stride_analysis[${i}] invalid threat_type: ` +
158
+ `'${threatType}' (must be one of ${JSON.stringify([...VALID_STRIDE_ABBREVIATIONS].sort())} or a full STRIDE name)`
159
+ );
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ // dread_scores must be a non-empty list with all 5 dimensions
166
+ const dreadScores = report.dread_scores;
167
+ if (dreadScores !== undefined) {
168
+ if (!Array.isArray(dreadScores)) {
169
+ errors.push(`dread_scores must be a list (got ${typeof dreadScores})`);
170
+ } else if (dreadScores.length === 0) {
171
+ errors.push('dread_scores list must not be empty');
172
+ } else {
173
+ for (let i = 0; i < dreadScores.length; i++) {
174
+ const item = dreadScores[i];
175
+ if (typeof item !== 'object' || item === null || Array.isArray(item)) {
176
+ errors.push(`dread_scores[${i}] must be a dict (got ${Array.isArray(item) ? 'Array' : typeof item})`);
177
+ continue;
178
+ }
179
+ for (const dim of DREAD_DIMENSIONS) {
180
+ if (!(dim in item)) {
181
+ errors.push(`dread_scores[${i}] missing DREAD dimension: ${dim}`);
182
+ } else {
183
+ const val = item[dim];
184
+ if (!Number.isInteger(val)) {
185
+ errors.push(
186
+ `dread_scores[${i}].${dim} must be an integer (got ${typeof val})`
187
+ );
188
+ } else if (val < 1 || val > 10) {
189
+ errors.push(`dread_scores[${i}].${dim} must be 1-10 (got ${val})`);
190
+ }
191
+ }
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ // mitigations must be a non-empty list
198
+ const mitigations = report.mitigations;
199
+ if (mitigations !== undefined) {
200
+ if (!Array.isArray(mitigations)) {
201
+ errors.push(`mitigations must be a list (got ${typeof mitigations})`);
202
+ } else if (mitigations.length === 0) {
203
+ errors.push('mitigations list must not be empty');
204
+ }
205
+ }
206
+
207
+ // dfd_mermaid must be a non-empty string containing graph or flowchart
208
+ const dfdMermaid = report.dfd_mermaid;
209
+ if (dfdMermaid !== undefined) {
210
+ if (typeof dfdMermaid !== 'string') {
211
+ errors.push(`dfd_mermaid must be a string (got ${typeof dfdMermaid})`);
212
+ } else if (!dfdMermaid.trim()) {
213
+ errors.push('dfd_mermaid must not be empty');
214
+ } else {
215
+ const lower = dfdMermaid.toLowerCase();
216
+ if (!lower.includes('graph') && !lower.includes('flowchart')) {
217
+ errors.push(
218
+ "dfd_mermaid must contain 'graph' or 'flowchart' keyword (invalid Mermaid DFD syntax)"
219
+ );
220
+ }
221
+ }
222
+ }
223
+
224
+ // Recommended fields
225
+ for (const fieldName of RECOMMENDED_FIELDS) {
226
+ if (!(fieldName in report)) {
227
+ warnings.push(`Missing recommended field: ${fieldName}`);
228
+ }
229
+ }
230
+
231
+ return { errors, warnings };
232
+ }
233
+
234
+ /**
235
+ * Compute the highest risk level from DREAD score averages.
236
+ *
237
+ * @param {Array<Object>} dreadScores
238
+ * @returns {string}
239
+ */
240
+ function computeHighestRiskLevel(dreadScores) {
241
+ if (!Array.isArray(dreadScores) || dreadScores.length === 0) return 'none';
242
+
243
+ let maxAvg = 0;
244
+ for (const item of dreadScores) {
245
+ if (typeof item !== 'object' || item === null || Array.isArray(item)) continue;
246
+ const dimValues = [];
247
+ for (const dim of DREAD_DIMENSIONS) {
248
+ const val = item[dim];
249
+ if (Number.isInteger(val)) dimValues.push(val);
250
+ }
251
+ if (dimValues.length > 0) {
252
+ const avg = dimValues.reduce((a, b) => a + b, 0) / dimValues.length;
253
+ if (avg > maxAvg) maxAvg = avg;
254
+ }
255
+ }
256
+
257
+ if (maxAvg <= 0) return 'none';
258
+ if (maxAvg >= 8) return 'Critical';
259
+ if (maxAvg >= 6) return 'High';
260
+ if (maxAvg >= 4) return 'Medium';
261
+ return 'Low';
262
+ }
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // ThreatModelValidator
266
+ // ---------------------------------------------------------------------------
267
+
268
+ /**
269
+ * Deterministic validator for JSON STRIDE/DREAD threat model reports.
270
+ *
271
+ * Scoring:
272
+ * - 1.0: no errors and no warnings (clean)
273
+ * - 0.5: warnings only (valid but imperfect)
274
+ * - 0.0: any errors (invalid)
275
+ */
276
+ export class ThreatModelValidator {
277
+ /**
278
+ * @param {import('../framework.js').ModeAgentResult} result
279
+ * @returns {import('../framework.js').ValidationResult}
280
+ */
281
+ validate(result) {
282
+ const text = result.outputText || '';
283
+ const emptyMeta = {
284
+ component_count: 0,
285
+ threat_count: 0,
286
+ mitigation_count: 0,
287
+ highest_risk_level: 'none',
288
+ };
289
+
290
+ // Empty output
291
+ if (!text.trim()) {
292
+ return new ValidationResult({
293
+ valid: false,
294
+ errors: ['No threat model found in output (empty)'],
295
+ warnings: [],
296
+ score: 0.0,
297
+ metadata: { ...emptyMeta },
298
+ });
299
+ }
300
+
301
+ // Parse JSON
302
+ let parsed;
303
+ try {
304
+ parsed = parseJsonReport(text);
305
+ } catch (e) {
306
+ return new ValidationResult({
307
+ valid: false,
308
+ errors: [e.message],
309
+ warnings: [],
310
+ score: 0.0,
311
+ metadata: { ...emptyMeta },
312
+ });
313
+ }
314
+
315
+ // Must be an object
316
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
317
+ return new ValidationResult({
318
+ valid: false,
319
+ errors: [`Threat model must be a JSON object (got ${Array.isArray(parsed) ? 'Array' : typeof parsed})`],
320
+ warnings: [],
321
+ score: 0.0,
322
+ metadata: { ...emptyMeta },
323
+ });
324
+ }
325
+
326
+ // Structural validation
327
+ const { errors, warnings } = validateReport(parsed);
328
+
329
+ // Compute metadata
330
+ const components = parsed.components || [];
331
+ const strideAnalysis = parsed.stride_analysis || [];
332
+ const mitigations = parsed.mitigations || [];
333
+ const dreadScores = parsed.dread_scores || [];
334
+
335
+ const componentCount = Array.isArray(components) ? components.length : 0;
336
+ const threatCount = Array.isArray(strideAnalysis) ? strideAnalysis.length : 0;
337
+ const mitigationCount = Array.isArray(mitigations) ? mitigations.length : 0;
338
+ const highestRiskLevel = computeHighestRiskLevel(dreadScores);
339
+
340
+ // Scoring
341
+ let score;
342
+ if (errors.length > 0) {
343
+ score = 0.0;
344
+ } else if (warnings.length > 0) {
345
+ score = 0.5;
346
+ } else {
347
+ score = 1.0;
348
+ }
349
+
350
+ return new ValidationResult({
351
+ valid: errors.length === 0,
352
+ errors,
353
+ warnings,
354
+ score,
355
+ metadata: {
356
+ component_count: componentCount,
357
+ threat_count: threatCount,
358
+ mitigation_count: mitigationCount,
359
+ highest_risk_level: highestRiskLevel,
360
+ },
361
+ });
362
+ }
363
+ }