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.
- package/bin/cipher.js +566 -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 +238 -0
- package/lib/brand.js +105 -0
- package/lib/commands.js +100 -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 +991 -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 +288 -0
- 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
|
+
}
|