cipher-security 2.0.8 → 2.1.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 +1 -1
- package/lib/agent-runtime/handlers/architect.js +199 -0
- package/lib/agent-runtime/handlers/base.js +240 -0
- package/lib/agent-runtime/handlers/blue.js +220 -0
- package/lib/agent-runtime/handlers/incident.js +161 -0
- package/lib/agent-runtime/handlers/privacy.js +190 -0
- package/lib/agent-runtime/handlers/purple.js +209 -0
- package/lib/agent-runtime/handlers/recon.js +174 -0
- package/lib/agent-runtime/handlers/red.js +246 -0
- package/lib/agent-runtime/handlers/researcher.js +170 -0
- package/lib/agent-runtime/handlers.js +35 -0
- package/lib/agent-runtime/index.js +196 -0
- package/lib/agent-runtime/parser.js +316 -0
- package/lib/autonomous/feedback-loop.js +13 -6
- package/lib/autonomous/modes/red.js +557 -0
- package/lib/autonomous/modes/researcher.js +322 -0
- package/lib/autonomous/researcher.js +12 -45
- package/lib/autonomous/runner.js +9 -537
- 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
|
+
}
|