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,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
+ }