cipher-security 2.0.8 → 2.2.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 (70) hide show
  1. package/bin/cipher.js +11 -1
  2. package/lib/agent-runtime/handlers/architect.js +199 -0
  3. package/lib/agent-runtime/handlers/base.js +240 -0
  4. package/lib/agent-runtime/handlers/blue.js +220 -0
  5. package/lib/agent-runtime/handlers/incident.js +161 -0
  6. package/lib/agent-runtime/handlers/privacy.js +190 -0
  7. package/lib/agent-runtime/handlers/purple.js +209 -0
  8. package/lib/agent-runtime/handlers/recon.js +174 -0
  9. package/lib/agent-runtime/handlers/red.js +246 -0
  10. package/lib/agent-runtime/handlers/researcher.js +170 -0
  11. package/lib/agent-runtime/handlers.js +35 -0
  12. package/lib/agent-runtime/index.js +196 -0
  13. package/lib/agent-runtime/parser.js +316 -0
  14. package/lib/analyze/consistency.js +566 -0
  15. package/lib/analyze/constitution.js +110 -0
  16. package/lib/analyze/sharding.js +251 -0
  17. package/lib/autonomous/agent-tool.js +165 -0
  18. package/lib/autonomous/feedback-loop.js +13 -6
  19. package/lib/autonomous/framework.js +17 -0
  20. package/lib/autonomous/handoff.js +506 -0
  21. package/lib/autonomous/modes/blue.js +26 -0
  22. package/lib/autonomous/modes/red.js +585 -0
  23. package/lib/autonomous/modes/researcher.js +322 -0
  24. package/lib/autonomous/researcher.js +12 -45
  25. package/lib/autonomous/runner.js +9 -537
  26. package/lib/benchmark/agent.js +88 -26
  27. package/lib/benchmark/baselines.js +3 -0
  28. package/lib/benchmark/claude-code-solver.js +254 -0
  29. package/lib/benchmark/cognitive.js +283 -0
  30. package/lib/benchmark/index.js +12 -2
  31. package/lib/benchmark/knowledge.js +281 -0
  32. package/lib/benchmark/llm.js +156 -15
  33. package/lib/benchmark/models.js +5 -2
  34. package/lib/benchmark/nyu-ctf.js +192 -0
  35. package/lib/benchmark/overthewire.js +347 -0
  36. package/lib/benchmark/picoctf.js +281 -0
  37. package/lib/benchmark/prompts.js +280 -0
  38. package/lib/benchmark/registry.js +219 -0
  39. package/lib/benchmark/remote-solver.js +356 -0
  40. package/lib/benchmark/remote-target.js +263 -0
  41. package/lib/benchmark/reporter.js +35 -0
  42. package/lib/benchmark/runner.js +174 -10
  43. package/lib/benchmark/sandbox.js +35 -0
  44. package/lib/benchmark/scorer.js +22 -4
  45. package/lib/benchmark/solver.js +34 -1
  46. package/lib/benchmark/tools.js +262 -16
  47. package/lib/commands.js +9 -0
  48. package/lib/execution/council.js +434 -0
  49. package/lib/execution/parallel.js +292 -0
  50. package/lib/gates/circuit-breaker.js +135 -0
  51. package/lib/gates/confidence.js +302 -0
  52. package/lib/gates/corrections.js +219 -0
  53. package/lib/gates/self-check.js +245 -0
  54. package/lib/gateway/commands.js +727 -0
  55. package/lib/guardrails/engine.js +364 -0
  56. package/lib/mcp/server.js +349 -3
  57. package/lib/memory/compressor.js +94 -7
  58. package/lib/pipeline/hooks.js +288 -0
  59. package/lib/pipeline/index.js +11 -0
  60. package/lib/review/budget.js +210 -0
  61. package/lib/review/engine.js +526 -0
  62. package/lib/review/layers/acceptance-auditor.js +279 -0
  63. package/lib/review/layers/blind-hunter.js +500 -0
  64. package/lib/review/layers/defense-in-depth.js +209 -0
  65. package/lib/review/layers/edge-case-hunter.js +266 -0
  66. package/lib/review/panel.js +519 -0
  67. package/lib/review/two-stage.js +244 -0
  68. package/lib/session/cost-tracker.js +203 -0
  69. package/lib/session/logger.js +349 -0
  70. package/package.json +1 -1
@@ -0,0 +1,161 @@
1
+ // Copyright (c) 2026 defconxt. All rights reserved.
2
+ // Licensed under AGPL-3.0 — see LICENSE file for details.
3
+
4
+ import { BaseHandler } from './base.js';
5
+
6
+ /**
7
+ * INCIDENT mode handler — forensics & IR.
8
+ * Adds: forensic, timeline, evidence
9
+ */
10
+ export class IncidentHandler extends BaseHandler {
11
+ constructor() { super('incident'); }
12
+
13
+ domainActions() {
14
+ return ['forensic', 'timeline', 'evidence'];
15
+ }
16
+
17
+ describeDomain(action) {
18
+ return {
19
+ 'forensic': 'Extract forensic procedures and artifact collection steps',
20
+ 'timeline': 'Build investigation timeline from technique indicators',
21
+ 'evidence': 'List evidence sources, preservation steps, and chain-of-custody',
22
+ }[action];
23
+ }
24
+
25
+ async executeDomain(command, ctx) {
26
+ switch (command) {
27
+ case 'forensic': return this.forensic(ctx);
28
+ case 'timeline': return this.timeline(ctx);
29
+ case 'evidence': return this.evidence(ctx);
30
+ default: throw new Error(`Unknown INCIDENT command: ${command}`);
31
+ }
32
+ }
33
+
34
+ /** Extract forensic procedures. */
35
+ forensic(ctx) {
36
+ const { skill, technique } = ctx;
37
+ if (!skill) throw new Error('No SKILL.md found');
38
+
39
+ const procedures = [];
40
+ for (const section of skill.sections) {
41
+ if (/forensic|collect|acqui|preserv|analyz|examin|procedur/i.test(section.title)) {
42
+ procedures.push({
43
+ phase: section.title,
44
+ content: section.content.slice(0, 500),
45
+ });
46
+ }
47
+ }
48
+
49
+ // Extract forensic commands
50
+ const commands = skill.codeBlocks
51
+ .filter(b => ['bash', 'powershell', 'sh', 'python'].includes(b.lang))
52
+ .map(b => ({
53
+ lang: b.lang,
54
+ context: b.context,
55
+ commands: b.code.split('\n')
56
+ .filter(l => l.trim() && !l.trim().startsWith('#'))
57
+ .slice(0, 10),
58
+ }));
59
+
60
+ return {
61
+ action: 'forensic',
62
+ technique,
63
+ mode: 'INCIDENT',
64
+ attackIds: skill.attackIds,
65
+ tools: skill.tools,
66
+ procedures: procedures.slice(0, 10),
67
+ commands: commands.slice(0, 15),
68
+ status: 'forensic_complete',
69
+ };
70
+ }
71
+
72
+ /** Build investigation timeline framework. */
73
+ timeline(ctx) {
74
+ const { skill, technique } = ctx;
75
+ if (!skill) throw new Error('No SKILL.md found');
76
+
77
+ // Extract timeline-relevant data from tables
78
+ const artifacts = [];
79
+ for (const table of skill.tables) {
80
+ if (table.headers.some(h => /event|log|artifact|timestamp|evidence|indicator/i.test(h))) {
81
+ artifacts.push({
82
+ headers: table.headers,
83
+ entries: table.rows.slice(0, 15),
84
+ });
85
+ }
86
+ }
87
+
88
+ // Extract phases from workflow sections
89
+ const phases = skill.sections
90
+ .filter(s => /step|phase|stage|workflow|timeline|triage|contain|eradicat|recover/i.test(s.title))
91
+ .map(s => ({
92
+ phase: s.title,
93
+ summary: s.content.slice(0, 300),
94
+ }));
95
+
96
+ return {
97
+ action: 'timeline',
98
+ technique,
99
+ mode: 'INCIDENT',
100
+ attackIds: skill.attackIds,
101
+ artifacts: artifacts.slice(0, 5),
102
+ phases: phases.slice(0, 10),
103
+ status: 'timeline_complete',
104
+ };
105
+ }
106
+
107
+ /** Classify evidence sources as volatile or non-volatile. */
108
+ classifyEvidenceType(sources) {
109
+ if (!Array.isArray(sources)) return { volatile: [], nonVolatile: [], unclassified: [] };
110
+ const volatileRe = /memory|ram|process|network|connection|cache|register/i;
111
+ const nonVolatileRe = /disk|file|log|registry|image|backup|database/i;
112
+ const volatile = [];
113
+ const nonVolatile = [];
114
+ const unclassified = [];
115
+ sources.forEach((src, idx) => {
116
+ const text = typeof src === 'string' ? src : Object.values(src || {}).join(' ');
117
+ const isVolatile = volatileRe.test(text);
118
+ const isNonVolatile = nonVolatileRe.test(text);
119
+ if (isVolatile) { volatile.push(idx); }
120
+ else if (isNonVolatile) { nonVolatile.push(idx); }
121
+ else { unclassified.push(idx); }
122
+ });
123
+ return { volatile, nonVolatile, unclassified };
124
+ }
125
+
126
+ /** List evidence sources and preservation steps. */
127
+ evidence(ctx) {
128
+ const { skill, technique } = ctx;
129
+ if (!skill) throw new Error('No SKILL.md found');
130
+
131
+ const sources = [];
132
+ // Extract from tables
133
+ for (const table of skill.tables) {
134
+ if (table.headers.some(h => /source|evidence|artifact|log|location|path/i.test(h))) {
135
+ for (const row of table.rows) {
136
+ sources.push(row);
137
+ }
138
+ }
139
+ }
140
+
141
+ // Extract preservation notes
142
+ const preservation = skill.sections
143
+ .filter(s => /preserv|chain|custody|integrity|hash|imag/i.test(s.title))
144
+ .map(s => ({
145
+ topic: s.title,
146
+ content: s.content.slice(0, 300),
147
+ }));
148
+
149
+ return {
150
+ action: 'evidence',
151
+ technique,
152
+ mode: 'INCIDENT',
153
+ attackIds: skill.attackIds,
154
+ tools: skill.tools,
155
+ sources: sources.slice(0, 20),
156
+ evidenceClassification: this.classifyEvidenceType(sources.slice(0, 20)),
157
+ preservation: preservation.slice(0, 5),
158
+ status: 'evidence_complete',
159
+ };
160
+ }
161
+ }
@@ -0,0 +1,190 @@
1
+ // Copyright (c) 2026 defconxt. All rights reserved.
2
+ // Licensed under AGPL-3.0 — see LICENSE file for details.
3
+
4
+ import { BaseHandler } from './base.js';
5
+
6
+ /**
7
+ * PRIVACY mode handler — privacy engineering & compliance.
8
+ * Adds: dpia, data-flow, regulations
9
+ */
10
+ export class PrivacyHandler extends BaseHandler {
11
+ constructor() { super('privacy'); }
12
+
13
+ domainActions() {
14
+ return ['dpia', 'data-flow', 'regulations'];
15
+ }
16
+
17
+ describeDomain(action) {
18
+ return {
19
+ 'dpia': 'Generate Data Protection Impact Assessment structure',
20
+ 'data-flow': 'Extract data flow mappings and processing activities',
21
+ 'regulations': 'Map applicable regulations and compliance requirements',
22
+ }[action];
23
+ }
24
+
25
+ async executeDomain(command, ctx) {
26
+ switch (command) {
27
+ case 'dpia': return this.dpia(ctx);
28
+ case 'data-flow': return this.dataFlow(ctx);
29
+ case 'regulations': return this.regulations(ctx);
30
+ default: throw new Error(`Unknown PRIVACY command: ${command}`);
31
+ }
32
+ }
33
+
34
+ /** Generate DPIA structure. */
35
+ dpia(ctx) {
36
+ const { skill, technique } = ctx;
37
+ if (!skill) throw new Error('No SKILL.md found');
38
+
39
+ const scope = ctx.flags.scope || 'unspecified';
40
+
41
+ // Extract risk items from tables
42
+ const risks = [];
43
+ for (const table of skill.tables) {
44
+ if (table.headers.some(h => /risk|threat|impact|severity|likelihood/i.test(h))) {
45
+ for (const row of table.rows) {
46
+ risks.push(row);
47
+ }
48
+ }
49
+ }
50
+
51
+ // Extract checklist items
52
+ const checks = [];
53
+ const checkRe = /^\s*[-*] \[([x ])\] (.+)$/gm;
54
+ let m;
55
+ while ((m = checkRe.exec(skill.raw))) {
56
+ checks.push({ item: m[2].trim(), checked: m[1] === 'x' });
57
+ }
58
+
59
+ // Extract mitigation sections
60
+ const mitigations = skill.sections
61
+ .filter(s => /mitigat|control|safeguard|measure|protect/i.test(s.title))
62
+ .map(s => ({
63
+ area: s.title,
64
+ content: s.content.slice(0, 400),
65
+ }));
66
+
67
+ const regs = this.extractRegulations(skill);
68
+
69
+ return {
70
+ action: 'dpia',
71
+ technique,
72
+ mode: 'PRIVACY',
73
+ scope,
74
+ risks: risks.slice(0, 20),
75
+ checks: checks.slice(0, 30),
76
+ mitigations: mitigations.slice(0, 10),
77
+ regulations: regs,
78
+ regulationValidation: this.validateRegulations(regs),
79
+ status: 'dpia_complete',
80
+ };
81
+ }
82
+
83
+ /** Extract data flow mappings. */
84
+ dataFlow(ctx) {
85
+ const { skill, technique } = ctx;
86
+ if (!skill) throw new Error('No SKILL.md found');
87
+
88
+ // Extract data flow sections
89
+ const flows = skill.sections
90
+ .filter(s => /data.flow|process|transfer|collect|stor|shar/i.test(s.title))
91
+ .map(s => ({
92
+ phase: s.title,
93
+ content: s.content.slice(0, 400),
94
+ }));
95
+
96
+ // Extract from tables with data categories
97
+ const dataCategories = [];
98
+ for (const table of skill.tables) {
99
+ if (table.headers.some(h => /data|category|type|field|pii|personal/i.test(h))) {
100
+ for (const row of table.rows) {
101
+ dataCategories.push(row);
102
+ }
103
+ }
104
+ }
105
+
106
+ // Extract diagrams (mermaid blocks)
107
+ const diagrams = skill.codeBlocks
108
+ .filter(b => b.lang === 'mermaid' || b.context.toLowerCase().includes('flow'))
109
+ .map(b => ({ context: b.context, code: b.code }));
110
+
111
+ return {
112
+ action: 'data-flow',
113
+ technique,
114
+ mode: 'PRIVACY',
115
+ flows: flows.slice(0, 10),
116
+ dataCategories: dataCategories.slice(0, 20),
117
+ diagrams: diagrams.slice(0, 5),
118
+ status: 'data_flow_complete',
119
+ };
120
+ }
121
+
122
+ /** Map applicable regulations. */
123
+ regulations(ctx) {
124
+ const { skill, technique } = ctx;
125
+ if (!skill) throw new Error('No SKILL.md found');
126
+
127
+ const regs = this.extractRegulations(skill);
128
+ return {
129
+ action: 'regulations',
130
+ technique,
131
+ mode: 'PRIVACY',
132
+ regulations: regs,
133
+ regulationValidation: this.validateRegulations(regs),
134
+ status: 'regulations_complete',
135
+ };
136
+ }
137
+
138
+ /** Validate regulatory references for correct format and article/section ranges. */
139
+ validateRegulations(refs) {
140
+ if (!Array.isArray(refs)) return { valid: [], invalid: [], unknown: [] };
141
+ const valid = [];
142
+ const invalid = [];
143
+ const unknown = [];
144
+ const knownFrameworks = /^(HIPAA|CCPA|PCI[\s-]DSS|SOX|SOC\s*[12]|FERPA|COPPA|LGPD|PIPEDA|NIST|ISO|OWASP|MITRE)/i;
145
+
146
+ for (const ref of refs) {
147
+ const s = String(ref).trim();
148
+ // GDPR: validate article number 1-99
149
+ const gdprMatch = s.match(/GDPR\s*Art(?:icle)?\.?\s*(\d+)/i);
150
+ if (gdprMatch) {
151
+ const num = parseInt(gdprMatch[1], 10);
152
+ if (num >= 1 && num <= 99) { valid.push(s); } else { invalid.push(s); }
153
+ continue;
154
+ }
155
+ if (/^GDPR$/i.test(s)) { valid.push(s); continue; }
156
+ // Known frameworks accepted as valid
157
+ if (knownFrameworks.test(s)) { valid.push(s); continue; }
158
+ unknown.push(s);
159
+ }
160
+ return { valid, invalid, unknown };
161
+ }
162
+
163
+ /** Extract regulatory references from content. */
164
+ extractRegulations(skill) {
165
+ const regs = new Set();
166
+ const patterns = [
167
+ /GDPR\s*(?:Art(?:icle)?\.?\s*\d+(?:\(\d+\))?)/gi,
168
+ /CCPA\s*(?:§?\s*\d+(?:\.\d+)?)?/gi,
169
+ /HIPAA\s*(?:§?\s*\d+(?:\.\d+)?)?/gi,
170
+ /PCI[\s-]DSS\s*(?:\d+(?:\.\d+)*)?/gi,
171
+ /SOX\s*(?:§?\s*\d+)?/gi,
172
+ /NIST\s*(?:SP\s*)?(?:800-\d+(?:[A-Z])?)?/gi,
173
+ /ISO\s*(?:27\d{3}(?:[-:]\d+)?)/gi,
174
+ /SOC\s*[12]/gi,
175
+ /FERPA/gi,
176
+ /COPPA/gi,
177
+ /LGPD/gi,
178
+ /PIPEDA/gi,
179
+ ];
180
+
181
+ for (const re of patterns) {
182
+ let m;
183
+ while ((m = re.exec(skill.raw))) {
184
+ regs.add(m[0].trim());
185
+ }
186
+ }
187
+
188
+ return [...regs];
189
+ }
190
+ }
@@ -0,0 +1,209 @@
1
+ // Copyright (c) 2026 defconxt. All rights reserved.
2
+ // Licensed under AGPL-3.0 — see LICENSE file for details.
3
+
4
+ import { BaseHandler } from './base.js';
5
+
6
+ /**
7
+ * PURPLE mode handler — detection engineering.
8
+ * Adds: emulate, coverage, gaps
9
+ */
10
+ export class PurpleHandler extends BaseHandler {
11
+ constructor() { super('purple'); }
12
+
13
+ domainActions() {
14
+ return ['emulate', 'coverage', 'gaps'];
15
+ }
16
+
17
+ describeDomain(action) {
18
+ return {
19
+ 'emulate': 'Build emulation plan pairing attack steps with detection validation',
20
+ 'coverage': 'Map ATT&CK technique coverage from detection rules',
21
+ 'gaps': 'Identify detection gaps with remediation recommendations',
22
+ }[action];
23
+ }
24
+
25
+ async executeDomain(command, ctx) {
26
+ switch (command) {
27
+ case 'emulate': return this.emulate(ctx);
28
+ case 'coverage': return this.coverage(ctx);
29
+ case 'gaps': return this.gaps(ctx);
30
+ default: throw new Error(`Unknown PURPLE command: ${command}`);
31
+ }
32
+ }
33
+
34
+ /** Build emulation plan — attack + detection pairs. */
35
+ emulate(ctx) {
36
+ const { skill, technique } = ctx;
37
+ if (!skill) throw new Error('No SKILL.md found');
38
+
39
+ // Extract attack steps from workflow/procedure sections
40
+ const attackSteps = [];
41
+ const detectionSteps = [];
42
+
43
+ for (const section of skill.sections) {
44
+ if (/step|workflow|procedure|emulat|attack|execution/i.test(section.title)) {
45
+ attackSteps.push({
46
+ phase: section.title,
47
+ content: section.content.slice(0, 400),
48
+ });
49
+ }
50
+ if (/detect|valid|verify|confirm|monitor|alert/i.test(section.title)) {
51
+ detectionSteps.push({
52
+ phase: section.title,
53
+ content: section.content.slice(0, 400),
54
+ });
55
+ }
56
+ }
57
+
58
+ // Pair attack commands with detection queries
59
+ const pairs = [];
60
+ const attackBlocks = skill.codeBlocks.filter(b =>
61
+ ['bash', 'powershell', 'sh', 'cmd', 'python'].includes(b.lang)
62
+ );
63
+ const detectBlocks = skill.codeBlocks.filter(b =>
64
+ ['yaml', 'sigma', 'kql', 'spl', 'kusto', 'splunk'].includes(b.lang)
65
+ );
66
+
67
+ const maxPairs = Math.min(attackBlocks.length, 10);
68
+ for (let i = 0; i < maxPairs; i++) {
69
+ pairs.push({
70
+ attack: {
71
+ lang: attackBlocks[i].lang,
72
+ context: attackBlocks[i].context,
73
+ command: attackBlocks[i].code.split('\n').slice(0, 5).join('\n'),
74
+ },
75
+ detection: detectBlocks[i] ? {
76
+ lang: detectBlocks[i].lang,
77
+ context: detectBlocks[i].context,
78
+ rule: detectBlocks[i].code.split('\n').slice(0, 10).join('\n'),
79
+ } : { note: 'No matching detection rule found — GAP' },
80
+ });
81
+ }
82
+
83
+ return {
84
+ action: 'emulate',
85
+ technique,
86
+ mode: 'PURPLE',
87
+ attackIds: skill.attackIds,
88
+ tools: skill.tools,
89
+ attackSteps: attackSteps.slice(0, 10),
90
+ detectionSteps: detectionSteps.slice(0, 10),
91
+ emulationPairs: pairs,
92
+ status: 'emulate_complete',
93
+ };
94
+ }
95
+
96
+ /** Map detection coverage. */
97
+ coverage(ctx) {
98
+ const { skill, technique } = ctx;
99
+ if (!skill) throw new Error('No SKILL.md found');
100
+
101
+ const covered = [];
102
+ const uncovered = [];
103
+
104
+ for (const id of skill.attackIds) {
105
+ // Check if any detection content references this ID
106
+ const hasDetection = skill.codeBlocks.some(b =>
107
+ (b.lang === 'yaml' || b.lang === 'sigma' || b.lang === 'kql' || b.lang === 'spl') &&
108
+ b.code.includes(id)
109
+ );
110
+ if (hasDetection) {
111
+ covered.push(id);
112
+ } else {
113
+ uncovered.push(id);
114
+ }
115
+ }
116
+
117
+ // Count detection artifacts
118
+ const sigmaCount = skill.codeBlocks.filter(b =>
119
+ (b.lang === 'yaml' || b.lang === 'sigma') &&
120
+ (b.code.includes('logsource') || b.code.includes('detection'))
121
+ ).length;
122
+
123
+ return {
124
+ action: 'coverage',
125
+ technique,
126
+ mode: 'PURPLE',
127
+ attackIds: skill.attackIds,
128
+ covered,
129
+ uncovered,
130
+ coveragePercent: skill.attackIds.length > 0
131
+ ? Math.round((covered.length / skill.attackIds.length) * 100)
132
+ : 0,
133
+ coverageGrade: this.validateCoverage(covered, uncovered),
134
+ sigmaRuleCount: sigmaCount,
135
+ status: 'coverage_complete',
136
+ };
137
+ }
138
+
139
+ /** Assign a coverage grade based on covered vs uncovered technique counts. */
140
+ validateCoverage(covered, uncovered) {
141
+ if (!Array.isArray(covered) || !Array.isArray(uncovered)) return 'N/A';
142
+ const total = covered.length + uncovered.length;
143
+ if (total === 0) return 'N/A';
144
+ const pct = (covered.length / total) * 100;
145
+ if (pct >= 90) return 'A';
146
+ if (pct >= 75) return 'B';
147
+ if (pct >= 50) return 'C';
148
+ if (pct >= 25) return 'D';
149
+ return 'F';
150
+ }
151
+
152
+ /** Identify detection gaps. */
153
+ gaps(ctx) {
154
+ const { skill, technique } = ctx;
155
+ if (!skill) throw new Error('No SKILL.md found');
156
+
157
+ const gaps = [];
158
+
159
+ // Attack techniques without detection rules
160
+ for (const id of skill.attackIds) {
161
+ const hasRule = skill.codeBlocks.some(b =>
162
+ ['yaml', 'sigma', 'kql', 'spl'].includes(b.lang)
163
+ );
164
+ if (!hasRule) {
165
+ gaps.push({
166
+ type: 'missing_detection',
167
+ attackId: id,
168
+ recommendation: `Create detection rule for ${id}`,
169
+ });
170
+ }
171
+ }
172
+
173
+ // Attack code blocks without corresponding detection
174
+ const attackBlockCount = skill.codeBlocks.filter(b =>
175
+ ['bash', 'powershell', 'sh', 'cmd'].includes(b.lang)
176
+ ).length;
177
+ const detectBlockCount = skill.codeBlocks.filter(b =>
178
+ ['yaml', 'sigma', 'kql', 'spl'].includes(b.lang)
179
+ ).length;
180
+
181
+ if (attackBlockCount > detectBlockCount) {
182
+ gaps.push({
183
+ type: 'coverage_imbalance',
184
+ attackCommands: attackBlockCount,
185
+ detectionRules: detectBlockCount,
186
+ recommendation: `${attackBlockCount - detectBlockCount} attack patterns lack detection rules`,
187
+ });
188
+ }
189
+
190
+ // Check for log source gaps
191
+ const logSources = new Set();
192
+ for (const block of skill.codeBlocks) {
193
+ if (block.code.includes('logsource')) {
194
+ const catMatch = block.code.match(/category:\s*(\S+)/);
195
+ if (catMatch) logSources.add(catMatch[1]);
196
+ }
197
+ }
198
+
199
+ return {
200
+ action: 'gaps',
201
+ technique,
202
+ mode: 'PURPLE',
203
+ attackIds: skill.attackIds,
204
+ gaps,
205
+ logSourcesCovered: [...logSources],
206
+ status: 'gaps_complete',
207
+ };
208
+ }
209
+ }