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.
@@ -0,0 +1,174 @@
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
+ * RECON mode handler — OSINT & threat intelligence.
8
+ * Adds: sources, indicators, profile
9
+ */
10
+ export class ReconHandler extends BaseHandler {
11
+ constructor() { super('recon'); }
12
+
13
+ domainActions() {
14
+ return ['sources', 'indicators', 'profile'];
15
+ }
16
+
17
+ describeDomain(action) {
18
+ return {
19
+ 'sources': 'List OSINT sources and collection methods from technique',
20
+ 'indicators': 'Extract indicators of compromise and intelligence artifacts',
21
+ 'profile': 'Build target profile structure from technique methodology',
22
+ }[action];
23
+ }
24
+
25
+ async executeDomain(command, ctx) {
26
+ switch (command) {
27
+ case 'sources': return this.sources(ctx);
28
+ case 'indicators': return this.indicators(ctx);
29
+ case 'profile': return this.profile(ctx);
30
+ default: throw new Error(`Unknown RECON command: ${command}`);
31
+ }
32
+ }
33
+
34
+ /** List OSINT sources and methods. */
35
+ sources(ctx) {
36
+ const { skill, technique } = ctx;
37
+ if (!skill) throw new Error('No SKILL.md found');
38
+
39
+ const sources = [];
40
+
41
+ // Extract from tables
42
+ for (const table of skill.tables) {
43
+ if (table.headers.some(h => /source|tool|platform|service|api|url/i.test(h))) {
44
+ for (const row of table.rows) {
45
+ sources.push(row);
46
+ }
47
+ }
48
+ }
49
+
50
+ // Extract tool/API references from code blocks
51
+ const apis = skill.codeBlocks
52
+ .filter(b => /api|curl|request|fetch|http/i.test(b.code))
53
+ .map(b => ({
54
+ context: b.context,
55
+ lang: b.lang,
56
+ snippet: b.code.split('\n').slice(0, 5).join('\n'),
57
+ }));
58
+
59
+ // Classify sources as passive/active
60
+ const passiveKeywords = /passive|public|osint|open.source|search|query|lookup/i;
61
+ const activeKeywords = /active|scan|probe|enumerate|brute|fuzz/i;
62
+
63
+ const collectionType = passiveKeywords.test(skill.raw) ? 'passive' :
64
+ activeKeywords.test(skill.raw) ? 'active' : 'mixed';
65
+
66
+ return {
67
+ action: 'sources',
68
+ technique,
69
+ mode: 'RECON',
70
+ tools: skill.tools,
71
+ sources: sources.slice(0, 20),
72
+ apis: apis.slice(0, 10),
73
+ collectionType,
74
+ collectionClassification: this.classifyCollectionType(collectionType, skill.raw),
75
+ status: 'sources_complete',
76
+ };
77
+ }
78
+
79
+ /** Classify collection type with confidence score and OPSEC risk. */
80
+ classifyCollectionType(type, raw) {
81
+ const passiveKw = /passive|public|osint|open.source|search|query|lookup|whois|dns|certificate/gi;
82
+ const activeKw = /active|scan|probe|enumerate|brute|fuzz|exploit|inject|crawl|spider/gi;
83
+ const passiveMatches = (String(raw).match(passiveKw) || []).length;
84
+ const activeMatches = (String(raw).match(activeKw) || []).length;
85
+ const totalMatches = passiveMatches + activeMatches;
86
+ const confidence = totalMatches >= 3 ? 'high' : totalMatches >= 1 ? 'medium' : 'low';
87
+ const opsecRiskMap = { active: 'high', mixed: 'medium', passive: 'low' };
88
+ return {
89
+ type: type || 'mixed',
90
+ confidence,
91
+ opsecRisk: opsecRiskMap[type] || 'medium',
92
+ passiveIndicators: passiveMatches,
93
+ activeIndicators: activeMatches,
94
+ };
95
+ }
96
+
97
+ /** Extract indicators and intelligence artifacts. */
98
+ indicators(ctx) {
99
+ const { skill, technique } = ctx;
100
+ if (!skill) throw new Error('No SKILL.md found');
101
+
102
+ const indicators = [];
103
+
104
+ // Extract from tables with indicator columns
105
+ for (const table of skill.tables) {
106
+ if (table.headers.some(h => /indicator|ioc|artifact|hash|ip|domain|url|signature/i.test(h))) {
107
+ for (const row of table.rows) {
108
+ indicators.push(row);
109
+ }
110
+ }
111
+ }
112
+
113
+ // Extract patterns that look like IOCs from content
114
+ const iocPatterns = {
115
+ ipv4: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g,
116
+ domain: /\b[\w-]+\.(?:com|net|org|io|xyz|tk|top|cc|ru)\b/gi,
117
+ hash_md5: /\b[a-f0-9]{32}\b/gi,
118
+ hash_sha256: /\b[a-f0-9]{64}\b/gi,
119
+ };
120
+
121
+ const extractedIocs = {};
122
+ for (const [type, re] of Object.entries(iocPatterns)) {
123
+ const matches = [...new Set(skill.raw.match(re) || [])];
124
+ if (matches.length) extractedIocs[type] = matches.slice(0, 10);
125
+ }
126
+
127
+ return {
128
+ action: 'indicators',
129
+ technique,
130
+ mode: 'RECON',
131
+ attackIds: skill.attackIds,
132
+ indicators: indicators.slice(0, 20),
133
+ extractedIocs,
134
+ status: 'indicators_complete',
135
+ };
136
+ }
137
+
138
+ /** Build target profile structure. */
139
+ profile(ctx) {
140
+ const { skill, technique } = ctx;
141
+ if (!skill) throw new Error('No SKILL.md found');
142
+
143
+ const target = ctx.flags.target || 'unspecified';
144
+
145
+ // Extract methodology steps
146
+ const methodology = skill.sections
147
+ .filter(s => /step|workflow|method|approach|procedure|phase/i.test(s.title))
148
+ .map(s => ({
149
+ phase: s.title,
150
+ content: s.content.slice(0, 300),
151
+ }));
152
+
153
+ // Extract data points to collect
154
+ const dataPoints = [];
155
+ for (const table of skill.tables) {
156
+ if (table.headers.some(h => /field|data|attribute|property|element/i.test(h))) {
157
+ for (const row of table.rows) {
158
+ dataPoints.push(row);
159
+ }
160
+ }
161
+ }
162
+
163
+ return {
164
+ action: 'profile',
165
+ technique,
166
+ mode: 'RECON',
167
+ target,
168
+ tools: skill.tools,
169
+ methodology: methodology.slice(0, 10),
170
+ dataPoints: dataPoints.slice(0, 20),
171
+ status: 'profile_complete',
172
+ };
173
+ }
174
+ }
@@ -0,0 +1,246 @@
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
+ * RED mode handler — offensive security.
8
+ * Adds: generate (attack commands, payloads), attack-path, ttps
9
+ */
10
+ // Static ATT&CK technique → tactic mapping (70+ common techniques)
11
+ const TECHNIQUE_TACTIC_MAP = {
12
+ T1001: 'command-and-control', T1003: 'credential-access', T1005: 'collection',
13
+ T1007: 'discovery', T1010: 'discovery', T1011: 'exfiltration',
14
+ T1012: 'discovery', T1016: 'discovery', T1018: 'discovery',
15
+ T1020: 'exfiltration', T1021: 'lateral-movement', T1027: 'defense-evasion',
16
+ T1033: 'discovery', T1036: 'defense-evasion', T1037: 'persistence',
17
+ T1039: 'collection', T1040: 'credential-access', T1041: 'exfiltration',
18
+ T1046: 'discovery', T1047: 'execution', T1048: 'exfiltration',
19
+ T1049: 'discovery', T1053: 'execution', T1055: 'defense-evasion',
20
+ T1056: 'collection', T1057: 'discovery', T1059: 'execution',
21
+ T1068: 'privilege-escalation', T1069: 'discovery', T1070: 'defense-evasion',
22
+ T1071: 'command-and-control', T1072: 'lateral-movement', T1074: 'collection',
23
+ T1078: 'defense-evasion', T1080: 'lateral-movement', T1082: 'discovery',
24
+ T1083: 'discovery', T1087: 'discovery', T1090: 'command-and-control',
25
+ T1091: 'lateral-movement', T1095: 'command-and-control', T1098: 'persistence',
26
+ T1102: 'command-and-control', T1104: 'command-and-control', T1105: 'command-and-control',
27
+ T1106: 'execution', T1110: 'credential-access', T1111: 'credential-access',
28
+ T1112: 'defense-evasion', T1113: 'collection', T1114: 'collection',
29
+ T1115: 'collection', T1119: 'collection', T1120: 'discovery',
30
+ T1123: 'collection', T1124: 'discovery', T1125: 'collection',
31
+ T1127: 'defense-evasion', T1129: 'execution', T1132: 'command-and-control',
32
+ T1133: 'persistence', T1134: 'defense-evasion', T1135: 'discovery',
33
+ T1136: 'persistence', T1137: 'persistence', T1140: 'defense-evasion',
34
+ T1176: 'persistence', T1185: 'collection', T1187: 'credential-access',
35
+ T1189: 'initial-access', T1190: 'initial-access', T1195: 'initial-access',
36
+ T1197: 'defense-evasion', T1199: 'initial-access', T1200: 'initial-access',
37
+ T1201: 'discovery', T1202: 'defense-evasion', T1203: 'execution',
38
+ T1204: 'execution', T1205: 'defense-evasion', T1210: 'lateral-movement',
39
+ T1211: 'defense-evasion', T1213: 'collection', T1218: 'defense-evasion',
40
+ T1219: 'command-and-control', T1220: 'defense-evasion',
41
+ T1221: 'defense-evasion', T1222: 'defense-evasion',
42
+ T1486: 'impact', T1489: 'impact', T1490: 'impact',
43
+ T1491: 'impact', T1496: 'impact', T1498: 'impact',
44
+ T1499: 'impact', T1529: 'impact',
45
+ T1543: 'persistence', T1546: 'persistence', T1547: 'persistence',
46
+ T1548: 'privilege-escalation', T1550: 'defense-evasion',
47
+ T1552: 'credential-access', T1553: 'defense-evasion',
48
+ T1555: 'credential-access', T1556: 'credential-access',
49
+ T1557: 'credential-access', T1558: 'credential-access',
50
+ T1559: 'execution', T1560: 'collection',
51
+ T1562: 'defense-evasion', T1563: 'lateral-movement',
52
+ T1564: 'defense-evasion', T1565: 'impact',
53
+ T1566: 'initial-access', T1567: 'exfiltration',
54
+ T1568: 'command-and-control', T1569: 'execution',
55
+ T1570: 'lateral-movement', T1571: 'command-and-control',
56
+ T1572: 'command-and-control', T1573: 'command-and-control',
57
+ T1574: 'persistence', T1578: 'defense-evasion',
58
+ };
59
+
60
+ // Tactic → kill chain phase mapping
61
+ const TACTIC_PHASE_MAP = {
62
+ 'reconnaissance': 'reconnaissance',
63
+ 'resource-development': 'weaponization',
64
+ 'initial-access': 'delivery',
65
+ 'execution': 'exploitation',
66
+ 'persistence': 'installation',
67
+ 'privilege-escalation': 'exploitation',
68
+ 'defense-evasion': 'exploitation',
69
+ 'credential-access': 'exploitation',
70
+ 'discovery': 'exploitation',
71
+ 'lateral-movement': 'exploitation',
72
+ 'collection': 'actions-on-objectives',
73
+ 'command-and-control': 'command-and-control',
74
+ 'exfiltration': 'actions-on-objectives',
75
+ 'impact': 'actions-on-objectives',
76
+ };
77
+
78
+ export class RedHandler extends BaseHandler {
79
+ constructor() { super('red'); }
80
+
81
+ domainActions() {
82
+ return ['generate', 'attack-path', 'ttps'];
83
+ }
84
+
85
+ describeDomain(action) {
86
+ return {
87
+ 'generate': 'Generate attack commands and payloads from SKILL.md patterns',
88
+ 'attack-path': 'Map attack paths with ATT&CK technique chain',
89
+ 'ttps': 'Extract TTPs with MITRE ATT&CK mappings',
90
+ }[action];
91
+ }
92
+
93
+ async executeDomain(command, ctx) {
94
+ switch (command) {
95
+ case 'generate': return this.generate(ctx);
96
+ case 'attack-path': return this.attackPath(ctx);
97
+ case 'ttps': return this.ttps(ctx);
98
+ default: throw new Error(`Unknown RED command: ${command}`);
99
+ }
100
+ }
101
+
102
+ /** Generate attack commands from code blocks. */
103
+ generate(ctx) {
104
+ const source = ctx.apiRef || ctx.skill;
105
+ if (!source) throw new Error('No source material found');
106
+
107
+ const attackLangs = ['bash', 'powershell', 'python', 'sh', 'cmd', 'text'];
108
+ const commands = source.codeBlocks
109
+ .filter(b => attackLangs.includes(b.lang))
110
+ .map(b => ({
111
+ lang: b.lang,
112
+ context: b.context,
113
+ commands: b.code.split('\n')
114
+ .filter(l => l.trim() && !l.trim().startsWith('#') && !l.trim().startsWith('//'))
115
+ .map(l => l.trim()),
116
+ }))
117
+ .filter(b => b.commands.length > 0);
118
+
119
+ const target = ctx.flags.target || 'unspecified';
120
+
121
+ return {
122
+ action: 'generate',
123
+ technique: ctx.technique,
124
+ mode: 'RED',
125
+ target,
126
+ attackIds: source.attackIds,
127
+ tools: source.tools,
128
+ commandGroups: commands.slice(0, 20),
129
+ totalCommands: commands.reduce((n, g) => n + g.commands.length, 0),
130
+ warning: 'Authorized testing only. Verify scope before execution.',
131
+ tactics: this.classifyAttackTactic(source.attackIds),
132
+ status: 'generate_complete',
133
+ };
134
+ }
135
+
136
+ /** Map attack paths from technique content. */
137
+ attackPath(ctx) {
138
+ const { skill, technique } = ctx;
139
+ if (!skill) throw new Error('No SKILL.md found');
140
+
141
+ // Extract attack path patterns from content
142
+ const paths = [];
143
+ const pathRe = /(?:→|->|==>|leads to|then|next)/gi;
144
+ for (const section of skill.sections) {
145
+ if (/path|chain|flow|step|workflow|kill.chain/i.test(section.title)) {
146
+ paths.push({
147
+ phase: section.title,
148
+ content: section.content.slice(0, 300),
149
+ });
150
+ }
151
+ }
152
+
153
+ // Extract from code blocks with arrow patterns
154
+ for (const block of skill.codeBlocks) {
155
+ if (pathRe.test(block.code)) {
156
+ paths.push({
157
+ phase: block.context || 'attack-flow',
158
+ content: block.code.slice(0, 300),
159
+ });
160
+ }
161
+ }
162
+
163
+ return {
164
+ action: 'attack-path',
165
+ technique,
166
+ mode: 'RED',
167
+ attackIds: skill.attackIds,
168
+ paths: paths.slice(0, 10),
169
+ detectionOpportunities: this.extractDetectionNotes(skill),
170
+ status: 'attack_path_complete',
171
+ };
172
+ }
173
+
174
+ /** Extract TTPs. */
175
+ ttps(ctx) {
176
+ const { skill, technique } = ctx;
177
+ if (!skill) throw new Error('No SKILL.md found');
178
+
179
+ const ttps = skill.attackIds.map(id => ({
180
+ id,
181
+ technique: ctx.technique,
182
+ tools: skill.tools,
183
+ }));
184
+
185
+ // Cross-reference with tables that have ATT&CK column
186
+ for (const table of skill.tables) {
187
+ const attackCol = table.headers.find(h => /att&ck|mitre|technique/i.test(h));
188
+ if (attackCol) {
189
+ for (const row of table.rows) {
190
+ const desc = row[table.headers.find(h => /description|name|action/i.test(h))] || '';
191
+ if (row[attackCol]) {
192
+ ttps.push({
193
+ id: row[attackCol],
194
+ description: desc,
195
+ technique: ctx.technique,
196
+ });
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ return {
203
+ action: 'ttps',
204
+ technique,
205
+ mode: 'RED',
206
+ ttps: this.dedup(ttps, 'id'),
207
+ tactics: this.classifyAttackTactic(skill.attackIds),
208
+ detectionOpportunities: this.extractDetectionNotes(skill),
209
+ status: 'ttps_complete',
210
+ };
211
+ }
212
+
213
+ /** PURPLE layer — every RED output includes detection opportunities. */
214
+ extractDetectionNotes(skill) {
215
+ const notes = [];
216
+ for (const section of skill.sections) {
217
+ if (/detect|monitor|alert|log|hunt|blue|defend/i.test(section.title)) {
218
+ notes.push({
219
+ area: section.title,
220
+ summary: section.content.slice(0, 200),
221
+ });
222
+ }
223
+ }
224
+ return notes.slice(0, 5);
225
+ }
226
+
227
+ /** Classify ATT&CK technique IDs to tactic names and kill chain phases. */
228
+ classifyAttackTactic(attackIds) {
229
+ if (!Array.isArray(attackIds)) return [];
230
+ return attackIds.map(id => {
231
+ const baseId = String(id).replace(/\.\d+$/, '');
232
+ const tactic = TECHNIQUE_TACTIC_MAP[baseId] || 'unknown';
233
+ const phase = TACTIC_PHASE_MAP[tactic] || 'unknown';
234
+ return { id, tactic, phase };
235
+ });
236
+ }
237
+
238
+ dedup(arr, key) {
239
+ const seen = new Set();
240
+ return arr.filter(item => {
241
+ if (seen.has(item[key])) return false;
242
+ seen.add(item[key]);
243
+ return true;
244
+ });
245
+ }
246
+ }
@@ -0,0 +1,170 @@
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
+ * RESEARCHER mode handler — CVE analysis, reverse engineering, AI security.
8
+ * Adds: vulnerabilities, techniques, research
9
+ */
10
+ export class ResearcherHandler extends BaseHandler {
11
+ constructor() { super('researcher'); }
12
+
13
+ domainActions() {
14
+ return ['vulnerabilities', 'techniques', 'research'];
15
+ }
16
+
17
+ describeDomain(action) {
18
+ return {
19
+ 'vulnerabilities': 'Extract CVE references, vulnerability patterns, and exploitation vectors',
20
+ 'techniques': 'Map reverse engineering and analysis techniques with tool commands',
21
+ 'research': 'Compile research notes, references, and threat landscape context',
22
+ }[action];
23
+ }
24
+
25
+ async executeDomain(command, ctx) {
26
+ switch (command) {
27
+ case 'vulnerabilities': return this.vulnerabilities(ctx);
28
+ case 'techniques': return this.techniques(ctx);
29
+ case 'research': return this.research(ctx);
30
+ default: throw new Error(`Unknown RESEARCHER command: ${command}`);
31
+ }
32
+ }
33
+
34
+ /** Extract vulnerability information. */
35
+ vulnerabilities(ctx) {
36
+ const { skill, technique } = ctx;
37
+ if (!skill) throw new Error('No SKILL.md found');
38
+
39
+ // Extract CVE references
40
+ const cves = new Set();
41
+ const cveRe = /CVE-\d{4}-\d{4,}/g;
42
+ let m;
43
+ while ((m = cveRe.exec(skill.raw))) {
44
+ cves.add(m[0]);
45
+ }
46
+
47
+ // Extract CWE references
48
+ const cwes = new Set();
49
+ const cweRe = /CWE-\d+/g;
50
+ while ((m = cweRe.exec(skill.raw))) {
51
+ cwes.add(m[0]);
52
+ }
53
+
54
+ // Extract vulnerability tables
55
+ const vulnTables = [];
56
+ for (const table of skill.tables) {
57
+ if (table.headers.some(h => /vuln|cve|cwe|weakness|flaw|bug|exploit/i.test(h))) {
58
+ vulnTables.push({
59
+ headers: table.headers,
60
+ rows: table.rows.slice(0, 15),
61
+ });
62
+ }
63
+ }
64
+
65
+ const cvesList = [...cves];
66
+
67
+ return {
68
+ action: 'vulnerabilities',
69
+ technique,
70
+ mode: 'RESEARCHER',
71
+ attackIds: skill.attackIds,
72
+ cves: cvesList,
73
+ cveValidation: this.validateCveIds(cvesList),
74
+ cwes: [...cwes],
75
+ vulnTables: vulnTables.slice(0, 5),
76
+ status: 'vulnerabilities_complete',
77
+ };
78
+ }
79
+
80
+ /** Validate CVE IDs for correct format (CVE-YYYY-NNNNN, year≥1999, 4-7 digit suffix). */
81
+ validateCveIds(cves) {
82
+ if (!Array.isArray(cves)) return { valid: [], invalid: [], total: 0 };
83
+ const valid = [];
84
+ const invalid = [];
85
+ const cveRe = /^CVE-(\d{4})-(\d{4,7})$/;
86
+ for (const id of cves) {
87
+ const m = String(id).match(cveRe);
88
+ if (m && parseInt(m[1], 10) >= 1999) { valid.push(id); } else { invalid.push(id); }
89
+ }
90
+ return { valid, invalid, total: cves.length };
91
+ }
92
+
93
+ /** Map analysis techniques. */
94
+ techniques(ctx) {
95
+ const { skill, technique } = ctx;
96
+ if (!skill) throw new Error('No SKILL.md found');
97
+
98
+ // Extract analysis methods from sections
99
+ const methods = skill.sections
100
+ .filter(s => /method|technique|approach|analys|revers|debug|disassembl/i.test(s.title))
101
+ .map(s => ({
102
+ method: s.title,
103
+ content: s.content.slice(0, 400),
104
+ }));
105
+
106
+ // Extract tool-specific commands
107
+ const toolCommands = skill.codeBlocks
108
+ .filter(b => ['bash', 'python', 'sh', 'powershell', 'gdb', 'r2'].includes(b.lang))
109
+ .map(b => ({
110
+ lang: b.lang,
111
+ context: b.context,
112
+ commands: b.code.split('\n')
113
+ .filter(l => l.trim() && !l.trim().startsWith('#'))
114
+ .slice(0, 10),
115
+ }));
116
+
117
+ return {
118
+ action: 'techniques',
119
+ technique,
120
+ mode: 'RESEARCHER',
121
+ attackIds: skill.attackIds,
122
+ tools: skill.tools,
123
+ methods: methods.slice(0, 10),
124
+ toolCommands: toolCommands.slice(0, 15),
125
+ status: 'techniques_complete',
126
+ };
127
+ }
128
+
129
+ /** Compile research notes and context. */
130
+ research(ctx) {
131
+ const { skill, technique } = ctx;
132
+ if (!skill) throw new Error('No SKILL.md found');
133
+
134
+ // Extract references
135
+ const refs = [];
136
+ const urlRe = /https?:\/\/[^\s)]+/g;
137
+ let m;
138
+ while ((m = urlRe.exec(skill.raw))) {
139
+ refs.push(m[0]);
140
+ }
141
+
142
+ // Extract key concepts
143
+ const concepts = skill.sections
144
+ .filter(s => /concept|background|overview|theory|fundamentals/i.test(s.title))
145
+ .map(s => ({
146
+ topic: s.title,
147
+ content: s.content.slice(0, 400),
148
+ }));
149
+
150
+ // Extract related techniques
151
+ const related = skill.sections
152
+ .filter(s => /related|see.also|further|reference/i.test(s.title))
153
+ .map(s => ({
154
+ topic: s.title,
155
+ content: s.content.slice(0, 300),
156
+ }));
157
+
158
+ return {
159
+ action: 'research',
160
+ technique,
161
+ mode: 'RESEARCHER',
162
+ attackIds: skill.attackIds,
163
+ tools: skill.tools,
164
+ concepts: concepts.slice(0, 5),
165
+ references: [...new Set(refs)].slice(0, 20),
166
+ related: related.slice(0, 5),
167
+ status: 'research_complete',
168
+ };
169
+ }
170
+ }
@@ -0,0 +1,35 @@
1
+ // Copyright (c) 2026 defconxt. All rights reserved.
2
+ // Licensed under AGPL-3.0 — see LICENSE file for details.
3
+
4
+ /**
5
+ * Domain handler registry.
6
+ * Maps mode names to handler modules.
7
+ */
8
+
9
+ import { RedHandler } from './handlers/red.js';
10
+ import { BlueHandler } from './handlers/blue.js';
11
+ import { PurpleHandler } from './handlers/purple.js';
12
+ import { IncidentHandler } from './handlers/incident.js';
13
+ import { PrivacyHandler } from './handlers/privacy.js';
14
+ import { ReconHandler } from './handlers/recon.js';
15
+ import { ArchitectHandler } from './handlers/architect.js';
16
+ import { ResearcherHandler } from './handlers/researcher.js';
17
+
18
+ const HANDLERS = {
19
+ red: new RedHandler(),
20
+ blue: new BlueHandler(),
21
+ purple: new PurpleHandler(),
22
+ incident: new IncidentHandler(),
23
+ privacy: new PrivacyHandler(),
24
+ recon: new ReconHandler(),
25
+ architect: new ArchitectHandler(),
26
+ researcher: new ResearcherHandler(),
27
+ };
28
+
29
+ /**
30
+ * Get the handler for a given mode.
31
+ * Falls back to architect if mode is unknown.
32
+ */
33
+ export function getHandler(mode) {
34
+ return HANDLERS[mode] || HANDLERS.architect;
35
+ }