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
package/bin/cipher.js
CHANGED
|
@@ -144,7 +144,7 @@ let queryNoStream = false;
|
|
|
144
144
|
let autonomousMode = null;
|
|
145
145
|
|
|
146
146
|
/** Set of valid autonomous mode names (lowercased). */
|
|
147
|
-
const MODE_NAMES = new Set(['red', 'blue', 'incident', 'purple', 'recon', 'privacy', 'architect']);
|
|
147
|
+
const MODE_NAMES = new Set(['red', 'blue', 'incident', 'purple', 'recon', 'privacy', 'architect', 'researcher']);
|
|
148
148
|
|
|
149
149
|
/** Clean args with --backend, --no-stream, and --autonomous extracted */
|
|
150
150
|
const cleanedArgs = [];
|
|
@@ -0,0 +1,199 @@
|
|
|
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
|
+
* ARCHITECT mode handler — security architecture & design.
|
|
8
|
+
* Adds: threat-model, controls, design
|
|
9
|
+
*/
|
|
10
|
+
export class ArchitectHandler extends BaseHandler {
|
|
11
|
+
constructor() { super('architect'); }
|
|
12
|
+
|
|
13
|
+
domainActions() {
|
|
14
|
+
return ['threat-model', 'controls', 'design'];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describeDomain(action) {
|
|
18
|
+
return {
|
|
19
|
+
'threat-model': 'Build STRIDE/DREAD threat model from technique patterns',
|
|
20
|
+
'controls': 'Map security controls (CIS, NIST, ISO) to technique',
|
|
21
|
+
'design': 'Extract architecture design patterns and recommendations',
|
|
22
|
+
}[action];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async executeDomain(command, ctx) {
|
|
26
|
+
switch (command) {
|
|
27
|
+
case 'threat-model': return this.threatModel(ctx);
|
|
28
|
+
case 'controls': return this.controls(ctx);
|
|
29
|
+
case 'design': return this.design(ctx);
|
|
30
|
+
default: throw new Error(`Unknown ARCHITECT command: ${command}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Build threat model structure. */
|
|
35
|
+
threatModel(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 STRIDE elements from content
|
|
42
|
+
const stride = {
|
|
43
|
+
spoofing: [],
|
|
44
|
+
tampering: [],
|
|
45
|
+
repudiation: [],
|
|
46
|
+
informationDisclosure: [],
|
|
47
|
+
dos: [],
|
|
48
|
+
elevationOfPrivilege: [],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const stridePatterns = {
|
|
52
|
+
spoofing: /spoof|impersonat|forge|fake identity/i,
|
|
53
|
+
tampering: /tamper|modif|alter|inject|manipulat/i,
|
|
54
|
+
repudiation: /repudiat|non-repudiat|audit|log/i,
|
|
55
|
+
informationDisclosure: /disclos|leak|expos|exfiltrat|data breach/i,
|
|
56
|
+
dos: /denial.of.service|dos|ddos|availability|exhaust/i,
|
|
57
|
+
elevationOfPrivilege: /privilege.escalat|elevat|root|admin|bypass auth/i,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for (const section of skill.sections) {
|
|
61
|
+
for (const [category, pattern] of Object.entries(stridePatterns)) {
|
|
62
|
+
if (pattern.test(section.title) || pattern.test(section.content.slice(0, 200))) {
|
|
63
|
+
stride[category].push({
|
|
64
|
+
source: section.title,
|
|
65
|
+
summary: section.content.slice(0, 200),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Extract trust boundaries from diagrams
|
|
72
|
+
const diagrams = skill.codeBlocks
|
|
73
|
+
.filter(b => b.lang === 'mermaid' || /diagram|flow|architect/i.test(b.context))
|
|
74
|
+
.map(b => ({ context: b.context, code: b.code }));
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
action: 'threat-model',
|
|
78
|
+
technique,
|
|
79
|
+
mode: 'ARCHITECT',
|
|
80
|
+
scope,
|
|
81
|
+
attackIds: skill.attackIds,
|
|
82
|
+
stride,
|
|
83
|
+
diagrams: diagrams.slice(0, 5),
|
|
84
|
+
mitigations: this.extractRecommendations(skill),
|
|
85
|
+
status: 'threat_model_complete',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Map security controls. */
|
|
90
|
+
controls(ctx) {
|
|
91
|
+
const { skill, technique } = ctx;
|
|
92
|
+
if (!skill) throw new Error('No SKILL.md found');
|
|
93
|
+
|
|
94
|
+
// Extract control references
|
|
95
|
+
const controlRefs = new Set();
|
|
96
|
+
const patterns = [
|
|
97
|
+
/CIS\s*(?:Control\s*)?\d+(?:\.\d+)*/gi,
|
|
98
|
+
/NIST\s*(?:SP\s*)?(?:800-\d+(?:[A-Z])?\s*)?(?:[A-Z]{2}-\d+)?/gi,
|
|
99
|
+
/ISO\s*27\d{3}(?:[-:]\d+(?:\.\d+)*)?/gi,
|
|
100
|
+
/SOC\s*[12](?:\s*Type\s*[12I]+)?/gi,
|
|
101
|
+
/PCI[\s-]DSS\s*(?:v?\d+(?:\.\d+)?\s*)?(?:Req(?:uirement)?\s*\d+(?:\.\d+)*)?/gi,
|
|
102
|
+
/OWASP\s*(?:Top\s*10)?\s*(?:A\d{2}(?::\d{4})?)?/gi,
|
|
103
|
+
/MITRE\s*(?:ATT&CK|D3FEND)/gi,
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
for (const re of patterns) {
|
|
107
|
+
let m;
|
|
108
|
+
while ((m = re.exec(skill.raw))) {
|
|
109
|
+
controlRefs.add(m[0].trim());
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Extract from tables with control columns
|
|
114
|
+
const controlTables = [];
|
|
115
|
+
for (const table of skill.tables) {
|
|
116
|
+
if (table.headers.some(h => /control|requirement|standard|framework|compliance/i.test(h))) {
|
|
117
|
+
controlTables.push({
|
|
118
|
+
headers: table.headers,
|
|
119
|
+
rows: table.rows.slice(0, 15),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const controlsList = [...controlRefs];
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
action: 'controls',
|
|
128
|
+
technique,
|
|
129
|
+
mode: 'ARCHITECT',
|
|
130
|
+
attackIds: skill.attackIds,
|
|
131
|
+
controls: controlsList,
|
|
132
|
+
controlValidation: this.validateControlRefs(controlsList),
|
|
133
|
+
controlTables: controlTables.slice(0, 5),
|
|
134
|
+
status: 'controls_complete',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Validate control reference strings against known framework formats. */
|
|
139
|
+
validateControlRefs(controls) {
|
|
140
|
+
if (!Array.isArray(controls)) return { valid: [], invalid: [], total: 0 };
|
|
141
|
+
const patterns = [
|
|
142
|
+
/^CIS\s*(?:Control\s*)?\d+(?:\.\d+)*/i,
|
|
143
|
+
/^NIST\s*(?:SP\s*)?800-\d+/i,
|
|
144
|
+
/^(?:AC|AT|AU|CA|CM|CP|IA|IR|MA|MP|PE|PL|PM|PS|RA|SA|SC|SI|SR)-\d+/i,
|
|
145
|
+
/^ISO\s*27\d{3}/i,
|
|
146
|
+
/^OWASP/i,
|
|
147
|
+
/^PCI[\s-]DSS/i,
|
|
148
|
+
/^SOC\s*[12]/i,
|
|
149
|
+
/^MITRE\s*(?:ATT&CK|D3FEND)/i,
|
|
150
|
+
];
|
|
151
|
+
const valid = [];
|
|
152
|
+
const invalid = [];
|
|
153
|
+
for (const ref of controls) {
|
|
154
|
+
const s = String(ref).trim();
|
|
155
|
+
if (patterns.some(p => p.test(s))) { valid.push(s); } else { invalid.push(s); }
|
|
156
|
+
}
|
|
157
|
+
return { valid, invalid, total: controls.length };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Extract architecture design patterns. */
|
|
161
|
+
design(ctx) {
|
|
162
|
+
const { skill, technique } = ctx;
|
|
163
|
+
if (!skill) throw new Error('No SKILL.md found');
|
|
164
|
+
|
|
165
|
+
// Extract design/architecture sections
|
|
166
|
+
const patterns = skill.sections
|
|
167
|
+
.filter(s => /design|architect|pattern|principle|best.practice|implement/i.test(s.title))
|
|
168
|
+
.map(s => ({
|
|
169
|
+
area: s.title,
|
|
170
|
+
content: s.content.slice(0, 400),
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
// Extract configuration examples
|
|
174
|
+
const configs = skill.codeBlocks
|
|
175
|
+
.filter(b => ['yaml', 'json', 'toml', 'hcl', 'tf', 'xml'].includes(b.lang))
|
|
176
|
+
.map(b => ({
|
|
177
|
+
lang: b.lang,
|
|
178
|
+
context: b.context,
|
|
179
|
+
config: b.code.slice(0, 500),
|
|
180
|
+
}));
|
|
181
|
+
|
|
182
|
+
// Extract diagrams
|
|
183
|
+
const diagrams = skill.codeBlocks
|
|
184
|
+
.filter(b => b.lang === 'mermaid')
|
|
185
|
+
.map(b => ({ context: b.context, code: b.code }));
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
action: 'design',
|
|
189
|
+
technique,
|
|
190
|
+
mode: 'ARCHITECT',
|
|
191
|
+
tools: skill.tools,
|
|
192
|
+
patterns: patterns.slice(0, 10),
|
|
193
|
+
configs: configs.slice(0, 10),
|
|
194
|
+
diagrams: diagrams.slice(0, 5),
|
|
195
|
+
recommendations: this.extractRecommendations(skill),
|
|
196
|
+
status: 'design_complete',
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Base handler — shared actions every domain handler inherits.
|
|
6
|
+
*
|
|
7
|
+
* Subclasses override domainActions() and executeDomain() to add
|
|
8
|
+
* domain-specific commands on top of the common set.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export class BaseHandler {
|
|
12
|
+
constructor(mode) {
|
|
13
|
+
this.mode = mode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** All available actions (common + domain-specific). */
|
|
17
|
+
actions() {
|
|
18
|
+
return [...this.commonActions(), ...this.domainActions()];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Actions shared across all modes. */
|
|
22
|
+
commonActions() {
|
|
23
|
+
return ['analyze', 'enumerate', 'assess', 'report'];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Override in subclass — domain-specific actions. */
|
|
27
|
+
domainActions() {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Description for help text. Override per-action in subclass. */
|
|
32
|
+
describe(action) {
|
|
33
|
+
const common = {
|
|
34
|
+
analyze: 'Extract knowledge, tables, ATT&CK mappings from SKILL.md',
|
|
35
|
+
enumerate: 'List commands and tools from api-reference.md',
|
|
36
|
+
assess: 'Evaluate against technique checklist',
|
|
37
|
+
report: 'Generate structured findings report',
|
|
38
|
+
};
|
|
39
|
+
return common[action] || this.describeDomain(action) || 'No description';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Override in subclass for domain-specific descriptions. */
|
|
43
|
+
describeDomain(action) { return null; }
|
|
44
|
+
|
|
45
|
+
/** Dispatch to the right action. */
|
|
46
|
+
async execute(command, ctx) {
|
|
47
|
+
switch (command) {
|
|
48
|
+
case 'analyze': return this.analyze(ctx);
|
|
49
|
+
case 'enumerate': return this.enumerate(ctx);
|
|
50
|
+
case 'assess': return this.assess(ctx);
|
|
51
|
+
case 'report': return this.report(ctx);
|
|
52
|
+
default: return this.executeDomain(command, ctx);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Override in subclass for domain-specific execution. */
|
|
57
|
+
async executeDomain(command, ctx) {
|
|
58
|
+
throw new Error(`Unknown command: ${command}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Common actions ────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/** Analyze SKILL.md — extract all structured data. */
|
|
64
|
+
analyze(ctx) {
|
|
65
|
+
const { skill, technique, domain, mode } = ctx;
|
|
66
|
+
if (!skill) throw new Error('No SKILL.md found');
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
action: 'analyze',
|
|
70
|
+
technique,
|
|
71
|
+
domain,
|
|
72
|
+
mode: mode.toUpperCase(),
|
|
73
|
+
name: skill.frontmatter?.name || technique,
|
|
74
|
+
description: skill.frontmatter?.description || '',
|
|
75
|
+
tags: skill.frontmatter?.tags || [],
|
|
76
|
+
attackIds: skill.attackIds,
|
|
77
|
+
tools: skill.tools,
|
|
78
|
+
sections: skill.sections.map(s => ({
|
|
79
|
+
level: s.level,
|
|
80
|
+
title: s.title,
|
|
81
|
+
// Truncate content to avoid dumping entire SKILL.md
|
|
82
|
+
summary: s.content.slice(0, 200) + (s.content.length > 200 ? '...' : ''),
|
|
83
|
+
})),
|
|
84
|
+
tables: skill.tables.map(t => ({
|
|
85
|
+
headers: t.headers,
|
|
86
|
+
rowCount: t.rows.length,
|
|
87
|
+
sample: t.rows.slice(0, 3),
|
|
88
|
+
})),
|
|
89
|
+
codeBlockCount: skill.codeBlocks.length,
|
|
90
|
+
status: 'analyze_complete',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Enumerate commands/tools from api-reference.md. */
|
|
95
|
+
enumerate(ctx) {
|
|
96
|
+
const source = ctx.apiRef || ctx.skill;
|
|
97
|
+
if (!source) throw new Error('No api-reference.md or SKILL.md found');
|
|
98
|
+
|
|
99
|
+
const commands = [];
|
|
100
|
+
|
|
101
|
+
// Extract from code blocks
|
|
102
|
+
for (const block of source.codeBlocks) {
|
|
103
|
+
for (const line of block.code.split('\n')) {
|
|
104
|
+
const trimmed = line.trim();
|
|
105
|
+
// Skip comments and empty lines
|
|
106
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('//') || trimmed.startsWith('*')) continue;
|
|
107
|
+
// Skip pure Python/JS function defs — only collect executable commands
|
|
108
|
+
if (trimmed.startsWith('def ') || trimmed.startsWith('function ') || trimmed.startsWith('import ')) continue;
|
|
109
|
+
if (trimmed.startsWith('class ') || trimmed.startsWith('from ')) continue;
|
|
110
|
+
commands.push({
|
|
111
|
+
command: trimmed,
|
|
112
|
+
lang: block.lang,
|
|
113
|
+
context: block.context,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Extract from tables (command tables)
|
|
119
|
+
for (const table of source.tables) {
|
|
120
|
+
const cmdHeader = table.headers.find(h =>
|
|
121
|
+
/command|syntax|usage/i.test(h)
|
|
122
|
+
);
|
|
123
|
+
if (cmdHeader) {
|
|
124
|
+
for (const row of table.rows) {
|
|
125
|
+
commands.push({
|
|
126
|
+
command: row[cmdHeader],
|
|
127
|
+
description: row[table.headers.find(h => /description|purpose/i.test(h))] || '',
|
|
128
|
+
context: 'table',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
action: 'enumerate',
|
|
136
|
+
technique: ctx.technique,
|
|
137
|
+
domain: ctx.domain,
|
|
138
|
+
mode: ctx.mode.toUpperCase(),
|
|
139
|
+
tools: source.tools,
|
|
140
|
+
commandCount: commands.length,
|
|
141
|
+
commands: commands.slice(0, 50), // Cap output size
|
|
142
|
+
status: 'enumerate_complete',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Assess against technique checklist. */
|
|
147
|
+
assess(ctx) {
|
|
148
|
+
const { skill, technique, domain, mode, flags } = ctx;
|
|
149
|
+
if (!skill) throw new Error('No SKILL.md found');
|
|
150
|
+
|
|
151
|
+
const target = flags.target || flags.scope || 'unspecified';
|
|
152
|
+
|
|
153
|
+
// Build checklist from tables and sections
|
|
154
|
+
const checklist = [];
|
|
155
|
+
|
|
156
|
+
// Look for prerequisite tables
|
|
157
|
+
for (const table of skill.tables) {
|
|
158
|
+
if (table.headers.some(h => /requirement|prerequisite|control|check/i.test(h))) {
|
|
159
|
+
for (const row of table.rows) {
|
|
160
|
+
const item = row[table.headers[0]] || Object.values(row)[0];
|
|
161
|
+
checklist.push({
|
|
162
|
+
item,
|
|
163
|
+
category: table.headers[0],
|
|
164
|
+
status: 'needs_review',
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Look for checklist patterns in content ([ ] and [x])
|
|
171
|
+
const checklistRe = /^\s*[-*] \[([x ])\] (.+)$/gm;
|
|
172
|
+
let m;
|
|
173
|
+
while ((m = checklistRe.exec(skill.raw))) {
|
|
174
|
+
checklist.push({
|
|
175
|
+
item: m[2].trim(),
|
|
176
|
+
category: 'checklist',
|
|
177
|
+
status: m[1] === 'x' ? 'implemented' : 'needs_review',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
action: 'assess',
|
|
183
|
+
technique,
|
|
184
|
+
domain,
|
|
185
|
+
mode: mode.toUpperCase(),
|
|
186
|
+
target,
|
|
187
|
+
attackIds: skill.attackIds,
|
|
188
|
+
checklistItems: checklist.length,
|
|
189
|
+
checklist: checklist.slice(0, 50),
|
|
190
|
+
coverage: checklist.length > 0
|
|
191
|
+
? `${checklist.filter(c => c.status === 'implemented').length}/${checklist.length}`
|
|
192
|
+
: 'no checklist found',
|
|
193
|
+
status: 'assess_complete',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Generate structured report. */
|
|
198
|
+
report(ctx) {
|
|
199
|
+
const { skill, technique, domain, mode, flags } = ctx;
|
|
200
|
+
if (!skill) throw new Error('No SKILL.md found');
|
|
201
|
+
|
|
202
|
+
const format = flags.format || 'json';
|
|
203
|
+
const title = skill.frontmatter?.name || technique;
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
action: 'report',
|
|
207
|
+
technique,
|
|
208
|
+
domain,
|
|
209
|
+
mode: mode.toUpperCase(),
|
|
210
|
+
report: {
|
|
211
|
+
title,
|
|
212
|
+
description: skill.frontmatter?.description || '',
|
|
213
|
+
attackTechniques: skill.attackIds,
|
|
214
|
+
tools: skill.tools,
|
|
215
|
+
sections: skill.sections.map(s => s.title),
|
|
216
|
+
tableCount: skill.tables.length,
|
|
217
|
+
codeBlockCount: skill.codeBlocks.length,
|
|
218
|
+
recommendations: this.extractRecommendations(skill),
|
|
219
|
+
},
|
|
220
|
+
format,
|
|
221
|
+
status: 'report_complete',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Extract recommendations from sections named remediation, mitigation, etc. */
|
|
226
|
+
extractRecommendations(skill) {
|
|
227
|
+
const recs = [];
|
|
228
|
+
const recSections = skill.sections.filter(s =>
|
|
229
|
+
/remediat|mitigat|recommend|harden|defense|protect|prevent/i.test(s.title)
|
|
230
|
+
);
|
|
231
|
+
for (const s of recSections) {
|
|
232
|
+
// Extract list items
|
|
233
|
+
const items = s.content.match(/^[-*] .+$/gm);
|
|
234
|
+
if (items) {
|
|
235
|
+
recs.push(...items.map(i => i.replace(/^[-*] /, '').trim()));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return recs.slice(0, 20);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
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
|
+
* BLUE mode handler — defensive security.
|
|
8
|
+
* Adds: detect (Sigma rules, log queries), hunt, harden
|
|
9
|
+
*/
|
|
10
|
+
export class BlueHandler extends BaseHandler {
|
|
11
|
+
constructor() { super('blue'); }
|
|
12
|
+
|
|
13
|
+
domainActions() {
|
|
14
|
+
return ['detect', 'hunt', 'harden'];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describeDomain(action) {
|
|
18
|
+
return {
|
|
19
|
+
'detect': 'Generate detection rules (Sigma, KQL, SPL) from technique patterns',
|
|
20
|
+
'hunt': 'Build threat hunting queries from technique indicators',
|
|
21
|
+
'harden': 'Extract hardening recommendations and CIS controls',
|
|
22
|
+
}[action];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async executeDomain(command, ctx) {
|
|
26
|
+
switch (command) {
|
|
27
|
+
case 'detect': return this.detect(ctx);
|
|
28
|
+
case 'hunt': return this.hunt(ctx);
|
|
29
|
+
case 'harden': return this.harden(ctx);
|
|
30
|
+
default: throw new Error(`Unknown BLUE command: ${command}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Generate detection rules from SKILL.md patterns. */
|
|
35
|
+
detect(ctx) {
|
|
36
|
+
const { skill, technique } = ctx;
|
|
37
|
+
if (!skill) throw new Error('No SKILL.md found');
|
|
38
|
+
|
|
39
|
+
const format = ctx.flags.format || 'sigma';
|
|
40
|
+
const rules = [];
|
|
41
|
+
|
|
42
|
+
// Extract Sigma rules from code blocks
|
|
43
|
+
for (const block of skill.codeBlocks) {
|
|
44
|
+
if (block.lang === 'yaml' || block.lang === 'sigma') {
|
|
45
|
+
if (block.code.includes('logsource') || block.code.includes('detection')) {
|
|
46
|
+
rules.push({
|
|
47
|
+
type: 'sigma',
|
|
48
|
+
context: block.context,
|
|
49
|
+
rule: block.code,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Extract KQL/SPL queries
|
|
56
|
+
for (const block of skill.codeBlocks) {
|
|
57
|
+
if (block.lang === 'kql' || block.lang === 'kusto') {
|
|
58
|
+
rules.push({ type: 'kql', context: block.context, query: block.code });
|
|
59
|
+
}
|
|
60
|
+
if (block.lang === 'spl' || block.lang === 'splunk') {
|
|
61
|
+
rules.push({ type: 'spl', context: block.context, query: block.code });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Extract detection patterns from tables
|
|
66
|
+
const detectionTables = [];
|
|
67
|
+
for (const table of skill.tables) {
|
|
68
|
+
if (table.headers.some(h => /event.?id|log|detection|indicator|signature/i.test(h))) {
|
|
69
|
+
detectionTables.push({
|
|
70
|
+
headers: table.headers,
|
|
71
|
+
rows: table.rows.slice(0, 10),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Build detection summary from content
|
|
77
|
+
const detectionSections = skill.sections
|
|
78
|
+
.filter(s => /detect|sigma|rule|alert|monitor|query/i.test(s.title))
|
|
79
|
+
.map(s => ({
|
|
80
|
+
title: s.title,
|
|
81
|
+
summary: s.content.slice(0, 300),
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
const sigmaValidation = this.validateSigmaRules(rules);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
action: 'detect',
|
|
88
|
+
technique,
|
|
89
|
+
mode: 'BLUE',
|
|
90
|
+
attackIds: skill.attackIds,
|
|
91
|
+
requestedFormat: format,
|
|
92
|
+
rules,
|
|
93
|
+
detectionTables,
|
|
94
|
+
detectionSections,
|
|
95
|
+
sigmaValid: sigmaValidation.invalid.length === 0,
|
|
96
|
+
sigmaErrors: sigmaValidation.errors,
|
|
97
|
+
evasionConsiderations: this.extractEvasionNotes(skill),
|
|
98
|
+
status: 'detect_complete',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Build threat hunting queries. */
|
|
103
|
+
hunt(ctx) {
|
|
104
|
+
const { skill, technique } = ctx;
|
|
105
|
+
if (!skill) throw new Error('No SKILL.md found');
|
|
106
|
+
|
|
107
|
+
const queries = [];
|
|
108
|
+
|
|
109
|
+
// Extract hunting-relevant code blocks
|
|
110
|
+
const huntLangs = ['kql', 'kusto', 'spl', 'splunk', 'sql', 'powershell', 'bash'];
|
|
111
|
+
for (const block of skill.codeBlocks) {
|
|
112
|
+
if (huntLangs.includes(block.lang)) {
|
|
113
|
+
queries.push({
|
|
114
|
+
lang: block.lang,
|
|
115
|
+
context: block.context,
|
|
116
|
+
query: block.code,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Extract indicators from tables
|
|
122
|
+
const indicators = [];
|
|
123
|
+
for (const table of skill.tables) {
|
|
124
|
+
if (table.headers.some(h => /indicator|ioc|artifact|evidence|event/i.test(h))) {
|
|
125
|
+
indicators.push(...table.rows.slice(0, 10));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Hunt hypothesis from sections
|
|
130
|
+
const hypotheses = skill.sections
|
|
131
|
+
.filter(s => /hunt|hypothesis|scenario|use.case/i.test(s.title))
|
|
132
|
+
.map(s => ({
|
|
133
|
+
title: s.title,
|
|
134
|
+
content: s.content.slice(0, 300),
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
action: 'hunt',
|
|
139
|
+
technique,
|
|
140
|
+
mode: 'BLUE',
|
|
141
|
+
attackIds: skill.attackIds,
|
|
142
|
+
tools: skill.tools,
|
|
143
|
+
queries: queries.slice(0, 20),
|
|
144
|
+
indicators: indicators.slice(0, 20),
|
|
145
|
+
hypotheses: hypotheses.slice(0, 5),
|
|
146
|
+
status: 'hunt_complete',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Extract hardening recommendations. */
|
|
151
|
+
harden(ctx) {
|
|
152
|
+
const { skill, technique } = ctx;
|
|
153
|
+
if (!skill) throw new Error('No SKILL.md found');
|
|
154
|
+
|
|
155
|
+
const recommendations = this.extractRecommendations(skill);
|
|
156
|
+
|
|
157
|
+
// Extract CIS/NIST references
|
|
158
|
+
const controls = [];
|
|
159
|
+
const controlRe = /(?:CIS|NIST|ISO)\s*[\w.-]+/g;
|
|
160
|
+
let m;
|
|
161
|
+
while ((m = controlRe.exec(skill.raw))) {
|
|
162
|
+
controls.push(m[0]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Extract configuration code blocks
|
|
166
|
+
const configs = skill.codeBlocks
|
|
167
|
+
.filter(b => /config|harden|secure|policy|baseline/i.test(b.context))
|
|
168
|
+
.map(b => ({
|
|
169
|
+
lang: b.lang,
|
|
170
|
+
context: b.context,
|
|
171
|
+
config: b.code.slice(0, 500),
|
|
172
|
+
}));
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
action: 'harden',
|
|
176
|
+
technique,
|
|
177
|
+
mode: 'BLUE',
|
|
178
|
+
attackIds: skill.attackIds,
|
|
179
|
+
recommendations,
|
|
180
|
+
controls: [...new Set(controls)],
|
|
181
|
+
configs: configs.slice(0, 10),
|
|
182
|
+
evasionConsiderations: this.extractEvasionNotes(skill),
|
|
183
|
+
status: 'harden_complete',
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Validate Sigma rules for required structural fields. */
|
|
188
|
+
validateSigmaRules(rules) {
|
|
189
|
+
if (!Array.isArray(rules)) return { valid: [], invalid: [], errors: [] };
|
|
190
|
+
const requiredFields = ['title:', 'logsource:', 'detection:', 'condition:'];
|
|
191
|
+
const valid = [];
|
|
192
|
+
const invalid = [];
|
|
193
|
+
const errors = [];
|
|
194
|
+
rules.forEach((rule, idx) => {
|
|
195
|
+
const ruleText = typeof rule === 'string' ? rule : (rule && rule.rule) || '';
|
|
196
|
+
const missing = requiredFields.filter(f => !ruleText.includes(f));
|
|
197
|
+
if (missing.length === 0) {
|
|
198
|
+
valid.push(idx);
|
|
199
|
+
} else {
|
|
200
|
+
invalid.push(idx);
|
|
201
|
+
errors.push({ index: idx, missing: missing.map(f => f.replace(':', '')) });
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
return { valid, invalid, errors };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** PURPLE layer — every BLUE output includes evasion considerations. */
|
|
208
|
+
extractEvasionNotes(skill) {
|
|
209
|
+
const notes = [];
|
|
210
|
+
for (const section of skill.sections) {
|
|
211
|
+
if (/evas|bypass|limitation|blind.spot|false.negative|gap/i.test(section.title)) {
|
|
212
|
+
notes.push({
|
|
213
|
+
area: section.title,
|
|
214
|
+
summary: section.content.slice(0, 200),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return notes.slice(0, 5);
|
|
219
|
+
}
|
|
220
|
+
}
|