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,296 @@
|
|
|
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
|
+
* DPIA (Data Protection Impact Assessment) validator for PRIVACY mode agent output.
|
|
7
|
+
*
|
|
8
|
+
* Ported from autonomous/validators/privacy.py.
|
|
9
|
+
* Validates LLM-generated JSON DPIA reports for structural correctness:
|
|
10
|
+
* required sections, GDPR article citation format, risk assessment structure,
|
|
11
|
+
* and recommended fields.
|
|
12
|
+
*
|
|
13
|
+
* @module autonomous/validators/privacy
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { ValidationResult } from '../framework.js';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Constants
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
const REQUIRED_SECTIONS = [
|
|
23
|
+
'processing_description', 'data_flows', 'legal_basis',
|
|
24
|
+
'risk_assessment', 'mitigations', 'gdpr_articles', 'findings',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const RECOMMENDED_FIELDS = [
|
|
28
|
+
'ccpa_mappings', 'hipaa_mappings', 'data_subjects', 'retention_periods',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Helpers
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extract JSON content from markdown code fences, or return text as-is.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} text
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
function stripCodeFences(text) {
|
|
42
|
+
let matches = [...text.matchAll(/```json\s*\n(.*?)```/gs)].map(m => m[1]);
|
|
43
|
+
if (matches.length > 0) return matches.join('\n');
|
|
44
|
+
|
|
45
|
+
matches = [...text.matchAll(/```\s*\n(.*?)```/gs)].map(m => m[1]);
|
|
46
|
+
if (matches.length > 0) return matches.join('\n');
|
|
47
|
+
|
|
48
|
+
return text;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse a JSON DPIA report from text.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} text
|
|
55
|
+
* @returns {Object}
|
|
56
|
+
* @throws {Error}
|
|
57
|
+
*/
|
|
58
|
+
function parseJsonReport(text) {
|
|
59
|
+
const stripped = stripCodeFences(text);
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(stripped);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
throw new Error(`JSON parse error: ${e.message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Validate a DPIA report dict for structural correctness.
|
|
69
|
+
*
|
|
70
|
+
* @param {Object} report
|
|
71
|
+
* @returns {{ errors: string[], warnings: string[] }}
|
|
72
|
+
*/
|
|
73
|
+
function validateReport(report) {
|
|
74
|
+
const errors = [];
|
|
75
|
+
const warnings = [];
|
|
76
|
+
|
|
77
|
+
// Required top-level sections
|
|
78
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
79
|
+
if (!(section in report)) {
|
|
80
|
+
errors.push(`Missing required section: ${section}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// data_flows must be a non-empty list
|
|
85
|
+
const dataFlows = report.data_flows;
|
|
86
|
+
if (dataFlows !== undefined) {
|
|
87
|
+
if (!Array.isArray(dataFlows)) {
|
|
88
|
+
errors.push(`data_flows must be a list (got ${typeof dataFlows})`);
|
|
89
|
+
} else if (dataFlows.length === 0) {
|
|
90
|
+
errors.push('data_flows list must not be empty');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// legal_basis must be a dict with article and basis keys
|
|
95
|
+
const legalBasis = report.legal_basis;
|
|
96
|
+
if (legalBasis !== undefined) {
|
|
97
|
+
if (typeof legalBasis !== 'object' || legalBasis === null || Array.isArray(legalBasis)) {
|
|
98
|
+
errors.push(`legal_basis must be a dict (got ${Array.isArray(legalBasis) ? 'Array' : typeof legalBasis})`);
|
|
99
|
+
} else {
|
|
100
|
+
if (!('article' in legalBasis)) {
|
|
101
|
+
errors.push('legal_basis missing required key: article');
|
|
102
|
+
}
|
|
103
|
+
if (!('basis' in legalBasis)) {
|
|
104
|
+
errors.push('legal_basis missing required key: basis');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// risk_assessment must be a non-empty list with required sub-keys
|
|
110
|
+
const riskAssessment = report.risk_assessment;
|
|
111
|
+
if (riskAssessment !== undefined) {
|
|
112
|
+
if (!Array.isArray(riskAssessment)) {
|
|
113
|
+
errors.push(`risk_assessment must be a list (got ${typeof riskAssessment})`);
|
|
114
|
+
} else if (riskAssessment.length === 0) {
|
|
115
|
+
errors.push('risk_assessment list must not be empty');
|
|
116
|
+
} else {
|
|
117
|
+
const requiredRiskKeys = ['threat', 'likelihood', 'severity', 'risk_level'];
|
|
118
|
+
for (let i = 0; i < riskAssessment.length; i++) {
|
|
119
|
+
const item = riskAssessment[i];
|
|
120
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item)) {
|
|
121
|
+
errors.push(`risk_assessment[${i}] must be a dict (got ${Array.isArray(item) ? 'Array' : typeof item})`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
for (const key of requiredRiskKeys) {
|
|
125
|
+
if (!(key in item)) {
|
|
126
|
+
errors.push(`risk_assessment[${i}] missing required key: ${key}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// mitigations must be a non-empty list
|
|
134
|
+
const mitigations = report.mitigations;
|
|
135
|
+
if (mitigations !== undefined) {
|
|
136
|
+
if (!Array.isArray(mitigations)) {
|
|
137
|
+
errors.push(`mitigations must be a list (got ${typeof mitigations})`);
|
|
138
|
+
} else if (mitigations.length === 0) {
|
|
139
|
+
errors.push('mitigations list must not be empty');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// gdpr_articles must be a non-empty list, each matching Art. \d+
|
|
144
|
+
const gdprArticles = report.gdpr_articles;
|
|
145
|
+
if (gdprArticles !== undefined) {
|
|
146
|
+
if (!Array.isArray(gdprArticles)) {
|
|
147
|
+
errors.push(`gdpr_articles must be a list (got ${typeof gdprArticles})`);
|
|
148
|
+
} else if (gdprArticles.length === 0) {
|
|
149
|
+
errors.push('gdpr_articles list must not be empty');
|
|
150
|
+
} else {
|
|
151
|
+
for (const item of gdprArticles) {
|
|
152
|
+
if (typeof item !== 'string' || !/^Art\.\s*\d+/.test(item)) {
|
|
153
|
+
errors.push(
|
|
154
|
+
`Invalid GDPR article format: '${item}' (must match 'Art. <number>')`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// findings must be a non-empty list
|
|
162
|
+
const findings = report.findings;
|
|
163
|
+
if (findings !== undefined) {
|
|
164
|
+
if (!Array.isArray(findings)) {
|
|
165
|
+
errors.push(`findings must be a list (got ${typeof findings})`);
|
|
166
|
+
} else if (findings.length === 0) {
|
|
167
|
+
errors.push('findings list must not be empty');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Recommended fields
|
|
172
|
+
for (const fieldName of RECOMMENDED_FIELDS) {
|
|
173
|
+
if (!(fieldName in report)) {
|
|
174
|
+
warnings.push(`Missing recommended field: ${fieldName}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { errors, warnings };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// DPIAValidator
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Deterministic validator for JSON DPIA reports.
|
|
187
|
+
*
|
|
188
|
+
* Scoring:
|
|
189
|
+
* - 1.0: no errors and no warnings (clean)
|
|
190
|
+
* - 0.5: warnings only (valid but imperfect)
|
|
191
|
+
* - 0.0: any errors (invalid)
|
|
192
|
+
*/
|
|
193
|
+
export class DPIAValidator {
|
|
194
|
+
/**
|
|
195
|
+
* @param {import('../framework.js').ModeAgentResult} result
|
|
196
|
+
* @returns {import('../framework.js').ValidationResult}
|
|
197
|
+
*/
|
|
198
|
+
validate(result) {
|
|
199
|
+
const text = result.outputText || '';
|
|
200
|
+
const emptyMeta = {
|
|
201
|
+
findings_count: 0,
|
|
202
|
+
risk_count: 0,
|
|
203
|
+
gdpr_article_count: 0,
|
|
204
|
+
highest_risk_level: 'none',
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Empty output
|
|
208
|
+
if (!text.trim()) {
|
|
209
|
+
return new ValidationResult({
|
|
210
|
+
valid: false,
|
|
211
|
+
errors: ['No DPIA report found in output (empty)'],
|
|
212
|
+
warnings: [],
|
|
213
|
+
score: 0.0,
|
|
214
|
+
metadata: { ...emptyMeta },
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Parse JSON
|
|
219
|
+
let parsed;
|
|
220
|
+
try {
|
|
221
|
+
parsed = parseJsonReport(text);
|
|
222
|
+
} catch (e) {
|
|
223
|
+
return new ValidationResult({
|
|
224
|
+
valid: false,
|
|
225
|
+
errors: [e.message],
|
|
226
|
+
warnings: [],
|
|
227
|
+
score: 0.0,
|
|
228
|
+
metadata: { ...emptyMeta },
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Must be an object
|
|
233
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
234
|
+
return new ValidationResult({
|
|
235
|
+
valid: false,
|
|
236
|
+
errors: [`DPIA report must be a JSON object (got ${Array.isArray(parsed) ? 'Array' : typeof parsed})`],
|
|
237
|
+
warnings: [],
|
|
238
|
+
score: 0.0,
|
|
239
|
+
metadata: { ...emptyMeta },
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Structural validation
|
|
244
|
+
const { errors, warnings } = validateReport(parsed);
|
|
245
|
+
|
|
246
|
+
// Compute metadata
|
|
247
|
+
const findings = parsed.findings || [];
|
|
248
|
+
const riskAssessment = parsed.risk_assessment || [];
|
|
249
|
+
const gdprArticles = parsed.gdpr_articles || [];
|
|
250
|
+
|
|
251
|
+
const findingsCount = Array.isArray(findings) ? findings.length : 0;
|
|
252
|
+
const riskCount = Array.isArray(riskAssessment) ? riskAssessment.length : 0;
|
|
253
|
+
const gdprArticleCount = Array.isArray(gdprArticles) ? gdprArticles.length : 0;
|
|
254
|
+
|
|
255
|
+
// Highest risk level
|
|
256
|
+
let highestRiskLevel = 'none';
|
|
257
|
+
if (Array.isArray(riskAssessment) && riskAssessment.length > 0) {
|
|
258
|
+
const severityOrder = {
|
|
259
|
+
critical: 4, high: 3, medium: 2, low: 1, info: 0,
|
|
260
|
+
};
|
|
261
|
+
const scored = riskAssessment
|
|
262
|
+
.filter(item => item && typeof item === 'object' && item.risk_level)
|
|
263
|
+
.map(item => ({
|
|
264
|
+
score: severityOrder[item.risk_level.toLowerCase()] ?? -1,
|
|
265
|
+
level: item.risk_level,
|
|
266
|
+
}))
|
|
267
|
+
.filter(s => s.score >= 0);
|
|
268
|
+
if (scored.length > 0) {
|
|
269
|
+
highestRiskLevel = scored.reduce((a, b) => a.score > b.score ? a : b).level;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Scoring
|
|
274
|
+
let score;
|
|
275
|
+
if (errors.length > 0) {
|
|
276
|
+
score = 0.0;
|
|
277
|
+
} else if (warnings.length > 0) {
|
|
278
|
+
score = 0.5;
|
|
279
|
+
} else {
|
|
280
|
+
score = 1.0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return new ValidationResult({
|
|
284
|
+
valid: errors.length === 0,
|
|
285
|
+
errors,
|
|
286
|
+
warnings,
|
|
287
|
+
score,
|
|
288
|
+
metadata: {
|
|
289
|
+
findings_count: findingsCount,
|
|
290
|
+
risk_count: riskCount,
|
|
291
|
+
gdpr_article_count: gdprArticleCount,
|
|
292
|
+
highest_risk_level: highestRiskLevel,
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
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
|
+
* PURPLE mode validator — emulation plan + Sigma detection + cross-coherence.
|
|
7
|
+
*
|
|
8
|
+
* Ported from autonomous/validators/purple.py.
|
|
9
|
+
* Validates PURPLE mode agent output containing TWO distinct artifacts:
|
|
10
|
+
* 1. Emulation plan (JSON) — extracted with explicit `json` language tag
|
|
11
|
+
* 2. Detection rule (Sigma YAML) — extracted with explicit `yaml` language tag
|
|
12
|
+
* 3. Cross-coherence — Sigma ATT&CK tags must reference the same technique ID
|
|
13
|
+
*
|
|
14
|
+
* Uses explicit language tags for extraction (per KNOWLEDGE.md — optional groups cause cross-contamination).
|
|
15
|
+
*
|
|
16
|
+
* @module autonomous/validators/purple
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { ValidationResult, ModeAgentResult } from '../framework.js';
|
|
20
|
+
import { SigmaValidator } from './sigma.js';
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Constants
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
// ATT&CK technique ID format: T followed by 4 digits, optional .NNN sub-technique
|
|
27
|
+
const TECHNIQUE_ID_RE = /^T\d{4}(\.\d{3})?$/;
|
|
28
|
+
|
|
29
|
+
const REQUIRED_PLAN_FIELDS = [
|
|
30
|
+
'technique_id', 'technique_name', 'emulation_steps', 'expected_artifacts',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Extraction helpers
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extract emulation plan JSON from text.
|
|
39
|
+
* Tries explicitly tagged ```json fences first, then bare fences, then raw text.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} text
|
|
42
|
+
* @returns {Object|null}
|
|
43
|
+
*/
|
|
44
|
+
function extractEmulationPlan(text) {
|
|
45
|
+
// Try explicitly-tagged ```json fences first
|
|
46
|
+
const jsonMatches = [...text.matchAll(/```json\s*\n(.*?)```/gs)].map(m => m[1]);
|
|
47
|
+
for (const match of jsonMatches) {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(match.trim());
|
|
50
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
53
|
+
} catch { /* continue */ }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Try bare (untagged) fences
|
|
57
|
+
const bareMatches = [...text.matchAll(/```\s*\n(.*?)```/gs)].map(m => m[1]);
|
|
58
|
+
for (const match of bareMatches) {
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(match.trim());
|
|
61
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
64
|
+
} catch { /* continue */ }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Fall back to raw text
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(text.trim());
|
|
70
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
71
|
+
return parsed;
|
|
72
|
+
}
|
|
73
|
+
} catch { /* ignore */ }
|
|
74
|
+
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Extract Sigma YAML from text.
|
|
80
|
+
* Looks for explicitly tagged yaml/yml code-fenced blocks first.
|
|
81
|
+
* Falls back to bare fences with YAML-like content.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} text
|
|
84
|
+
* @returns {string|null}
|
|
85
|
+
*/
|
|
86
|
+
function extractSigmaYaml(text) {
|
|
87
|
+
// Try explicitly-tagged ```yaml / ```yml fences
|
|
88
|
+
const matches = [...text.matchAll(/```ya?ml\s*\n(.*?)```/gs)].map(m => m[1]);
|
|
89
|
+
if (matches.length > 0) {
|
|
90
|
+
return matches.join('\n---\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Fall back to bare fences that look like YAML
|
|
94
|
+
const bareMatches = [...text.matchAll(/```\s*\n(.*?)```/gs)].map(m => m[1]);
|
|
95
|
+
const yamlLike = bareMatches.filter(m => /^\s*(title|logsource|detection):/m.test(m));
|
|
96
|
+
if (yamlLike.length > 0) {
|
|
97
|
+
return yamlLike.join('\n---\n');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Structural validation
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Validate emulation plan structure.
|
|
109
|
+
*
|
|
110
|
+
* @param {Object} plan
|
|
111
|
+
* @returns {{ errors: string[], warnings: string[] }}
|
|
112
|
+
*/
|
|
113
|
+
function validateEmulationPlan(plan) {
|
|
114
|
+
const errors = [];
|
|
115
|
+
const warnings = [];
|
|
116
|
+
|
|
117
|
+
// Required fields
|
|
118
|
+
for (const fieldName of REQUIRED_PLAN_FIELDS) {
|
|
119
|
+
if (!(fieldName in plan)) {
|
|
120
|
+
errors.push(`Missing required field: ${fieldName}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// technique_id format
|
|
125
|
+
const techniqueId = plan.technique_id;
|
|
126
|
+
if (techniqueId !== undefined) {
|
|
127
|
+
if (typeof techniqueId !== 'string' || !TECHNIQUE_ID_RE.test(techniqueId)) {
|
|
128
|
+
errors.push(
|
|
129
|
+
`Invalid technique_id format: '${techniqueId}' ` +
|
|
130
|
+
`(must match T\\d{4}(\\.\\d{3})?)`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// emulation_steps must be a non-empty list
|
|
136
|
+
const emulationSteps = plan.emulation_steps;
|
|
137
|
+
if (emulationSteps !== undefined) {
|
|
138
|
+
if (!Array.isArray(emulationSteps)) {
|
|
139
|
+
errors.push(`emulation_steps must be a list (got ${typeof emulationSteps})`);
|
|
140
|
+
} else if (emulationSteps.length === 0) {
|
|
141
|
+
errors.push('emulation_steps must not be empty');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// expected_artifacts must be a non-empty list
|
|
146
|
+
const expectedArtifacts = plan.expected_artifacts;
|
|
147
|
+
if (expectedArtifacts !== undefined) {
|
|
148
|
+
if (!Array.isArray(expectedArtifacts)) {
|
|
149
|
+
errors.push(`expected_artifacts must be a list (got ${typeof expectedArtifacts})`);
|
|
150
|
+
} else if (expectedArtifacts.length === 0) {
|
|
151
|
+
errors.push('expected_artifacts must not be empty');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { errors, warnings };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Check that Sigma rule ATT&CK tags reference the emulation plan's technique.
|
|
160
|
+
*
|
|
161
|
+
* @param {Object} plan
|
|
162
|
+
* @param {string} sigmaText
|
|
163
|
+
* @returns {{ errors: string[], warnings: string[] }}
|
|
164
|
+
*/
|
|
165
|
+
function checkCoherence(plan, sigmaText) {
|
|
166
|
+
const errors = [];
|
|
167
|
+
const warnings = [];
|
|
168
|
+
|
|
169
|
+
const techniqueId = plan.technique_id || '';
|
|
170
|
+
if (!techniqueId) return { errors, warnings };
|
|
171
|
+
|
|
172
|
+
// Normalize: T1059.001 → attack.t1059.001
|
|
173
|
+
const normalizedTag = `attack.${techniqueId.toLowerCase()}`;
|
|
174
|
+
|
|
175
|
+
if (!sigmaText.toLowerCase().includes(normalizedTag)) {
|
|
176
|
+
errors.push(
|
|
177
|
+
`Technique ID mismatch: emulation ${techniqueId} ` +
|
|
178
|
+
`not found in Sigma tags (expected ${normalizedTag})`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { errors, warnings };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// PurpleValidator
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Deterministic validator for PURPLE mode output.
|
|
191
|
+
*
|
|
192
|
+
* Scoring:
|
|
193
|
+
* - 1.0: both halves valid AND coherent
|
|
194
|
+
* - 0.5: both halves structurally valid BUT incoherent
|
|
195
|
+
* - 0.0: structural errors in either half, or neither artifact found
|
|
196
|
+
*/
|
|
197
|
+
export class PurpleValidator {
|
|
198
|
+
constructor() {
|
|
199
|
+
this._sigmaValidator = new SigmaValidator();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @param {import('../framework.js').ModeAgentResult} result
|
|
204
|
+
* @returns {import('../framework.js').ValidationResult}
|
|
205
|
+
*/
|
|
206
|
+
validate(result) {
|
|
207
|
+
const text = result.outputText || '';
|
|
208
|
+
|
|
209
|
+
const allErrors = [];
|
|
210
|
+
const allWarnings = [];
|
|
211
|
+
let emulationValid = false;
|
|
212
|
+
let detectionValid = false;
|
|
213
|
+
let coherent = false;
|
|
214
|
+
let techniqueId = null;
|
|
215
|
+
|
|
216
|
+
// Extract both artifacts
|
|
217
|
+
const plan = extractEmulationPlan(text);
|
|
218
|
+
const sigmaText = extractSigmaYaml(text);
|
|
219
|
+
|
|
220
|
+
// Neither found
|
|
221
|
+
if (plan === null && sigmaText === null) {
|
|
222
|
+
return new ValidationResult({
|
|
223
|
+
valid: false,
|
|
224
|
+
errors: ['No emulation plan or detection rule found'],
|
|
225
|
+
warnings: [],
|
|
226
|
+
score: 0.0,
|
|
227
|
+
metadata: {
|
|
228
|
+
emulation_valid: false,
|
|
229
|
+
detection_valid: false,
|
|
230
|
+
coherent: false,
|
|
231
|
+
technique_id: null,
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Validate emulation plan
|
|
237
|
+
if (plan !== null) {
|
|
238
|
+
const { errors: planErrors, warnings: planWarnings } = validateEmulationPlan(plan);
|
|
239
|
+
allErrors.push(...planErrors);
|
|
240
|
+
allWarnings.push(...planWarnings);
|
|
241
|
+
emulationValid = planErrors.length === 0;
|
|
242
|
+
techniqueId = emulationValid ? (plan.technique_id || null) : null;
|
|
243
|
+
} else {
|
|
244
|
+
allErrors.push('No emulation plan found in output');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Validate Sigma detection rule (delegate to SigmaValidator)
|
|
248
|
+
if (sigmaText !== null) {
|
|
249
|
+
const sigmaResult = this._sigmaValidator.validate(
|
|
250
|
+
new ModeAgentResult({ mode: 'PURPLE', outputText: sigmaText })
|
|
251
|
+
);
|
|
252
|
+
for (const err of sigmaResult.errors) {
|
|
253
|
+
allErrors.push(`Sigma: ${err}`);
|
|
254
|
+
}
|
|
255
|
+
for (const warn of sigmaResult.warnings) {
|
|
256
|
+
allWarnings.push(`Sigma: ${warn}`);
|
|
257
|
+
}
|
|
258
|
+
detectionValid = sigmaResult.valid;
|
|
259
|
+
} else {
|
|
260
|
+
allErrors.push('No detection rule found in output');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Cross-coherence (only if both halves structurally valid)
|
|
264
|
+
if (emulationValid && detectionValid && plan !== null && sigmaText !== null) {
|
|
265
|
+
const { errors: cohErrors, warnings: cohWarnings } = checkCoherence(plan, sigmaText);
|
|
266
|
+
allErrors.push(...cohErrors);
|
|
267
|
+
allWarnings.push(...cohWarnings);
|
|
268
|
+
coherent = cohErrors.length === 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Scoring
|
|
272
|
+
const hasStructuralErrors = !emulationValid || !detectionValid;
|
|
273
|
+
let score;
|
|
274
|
+
if (hasStructuralErrors) {
|
|
275
|
+
score = 0.0;
|
|
276
|
+
} else if (!coherent) {
|
|
277
|
+
score = 0.5;
|
|
278
|
+
} else {
|
|
279
|
+
score = 1.0;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// valid means both halves pass structural checks
|
|
283
|
+
const valid = emulationValid && detectionValid;
|
|
284
|
+
|
|
285
|
+
return new ValidationResult({
|
|
286
|
+
valid,
|
|
287
|
+
errors: allErrors,
|
|
288
|
+
warnings: allWarnings,
|
|
289
|
+
score,
|
|
290
|
+
metadata: {
|
|
291
|
+
emulation_valid: emulationValid,
|
|
292
|
+
detection_valid: detectionValid,
|
|
293
|
+
coherent,
|
|
294
|
+
technique_id: techniqueId,
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|