cipher-security 2.0.7 → 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,196 @@
1
+ #!/usr/bin/env node
2
+ // Copyright (c) 2026 defconxt. All rights reserved.
3
+ // Licensed under AGPL-3.0 — see LICENSE file for details.
4
+
5
+ /**
6
+ * CIPHER Agent Runtime
7
+ *
8
+ * Shared runtime for all 1,436 technique agent scripts.
9
+ * Reads SKILL.md + api-reference.md at execution time, extracts structured data,
10
+ * and dispatches to domain-specific handlers.
11
+ *
12
+ * Usage from agent.js:
13
+ * import { run } from '../../../cli/lib/agent-runtime/index.js';
14
+ * run(import.meta.url);
15
+ */
16
+
17
+ import { readFileSync, existsSync } from 'node:fs';
18
+ import { dirname, join, resolve, basename, sep } from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { parseSkillMd } from './parser.js';
21
+ import { getHandler } from './handlers.js';
22
+
23
+ // ── Domain → mode mapping ──────────────────────────────────────────────
24
+ const DOMAIN_MODE = {
25
+ 'red-team': 'red',
26
+ 'adversary-simulation': 'red',
27
+ 'exploit-development': 'red',
28
+ 'binary-exploitation': 'red',
29
+ 'c2-frameworks': 'red',
30
+ 'password-cracking': 'red',
31
+ 'bug-bounty': 'red',
32
+ 'social-engineering': 'red',
33
+
34
+ 'blue-team': 'blue',
35
+ 'soc-operations': 'blue',
36
+ 'detection-engineering': 'blue',
37
+ 'incident-response': 'blue',
38
+ 'incident-management': 'blue',
39
+ 'endpoint-security': 'blue',
40
+ 'ransomware-defense': 'blue',
41
+ 'phishing-defense': 'blue',
42
+
43
+ 'purple-team': 'purple',
44
+
45
+ 'digital-forensics': 'incident',
46
+ 'investigation-attribution': 'incident',
47
+ 'log-analysis': 'incident',
48
+ 'malware-analysis': 'incident',
49
+
50
+ 'privacy-engineering': 'privacy',
51
+ 'data-security': 'privacy',
52
+ 'compliance-audit': 'privacy',
53
+ 'governance-risk-compliance': 'privacy',
54
+
55
+ 'osint-recon': 'recon',
56
+ 'threat-intelligence': 'recon',
57
+
58
+ 'ai-llm-security': 'researcher',
59
+ 'reverse-engineering': 'researcher',
60
+ };
61
+
62
+ // Everything else → architect
63
+ const DEFAULT_MODE = 'architect';
64
+
65
+ /**
66
+ * Resolve technique paths from the agent.js location.
67
+ * Expected layout:
68
+ * skills/<domain>/techniques/<technique>/scripts/agent.js
69
+ */
70
+ function resolvePaths(agentUrl) {
71
+ const agentPath = fileURLToPath(agentUrl);
72
+ const scriptsDir = dirname(agentPath);
73
+ const techniqueDir = dirname(scriptsDir);
74
+ const techniquesDir = dirname(techniqueDir);
75
+ const domainDir = dirname(techniquesDir);
76
+
77
+ const technique = basename(techniqueDir);
78
+ const domain = basename(domainDir);
79
+
80
+ const skillPath = join(techniqueDir, 'SKILL.md');
81
+ const apiRefPath = join(techniqueDir, 'references', 'api-reference.md');
82
+
83
+ return { agentPath, techniqueDir, technique, domain, skillPath, apiRefPath };
84
+ }
85
+
86
+ /**
87
+ * Main entry point for all agent scripts.
88
+ */
89
+ export async function run(importMetaUrl) {
90
+ const { technique, domain, skillPath, apiRefPath } = resolvePaths(importMetaUrl);
91
+ const args = process.argv.slice(2);
92
+ const command = args[0];
93
+
94
+ // Resolve mode
95
+ const mode = DOMAIN_MODE[domain] || DEFAULT_MODE;
96
+
97
+ // Parse SKILL.md
98
+ let skill = null;
99
+ if (existsSync(skillPath)) {
100
+ const raw = readFileSync(skillPath, 'utf8');
101
+ skill = parseSkillMd(raw);
102
+ } else {
103
+ console.error(`[WARN] SKILL.md not found: ${skillPath}`);
104
+ }
105
+
106
+ // Parse api-reference.md
107
+ let apiRef = null;
108
+ if (existsSync(apiRefPath)) {
109
+ const raw = readFileSync(apiRefPath, 'utf8');
110
+ apiRef = parseSkillMd(raw);
111
+ }
112
+
113
+ // Build context
114
+ const ctx = {
115
+ technique,
116
+ domain,
117
+ mode,
118
+ skill,
119
+ apiRef,
120
+ args: args.slice(1),
121
+ flags: parseFlags(args.slice(1)),
122
+ };
123
+
124
+ // No command → show help
125
+ if (!command) {
126
+ showHelp(ctx);
127
+ process.exit(0);
128
+ }
129
+
130
+ // Get domain handler and dispatch
131
+ const handler = getHandler(mode);
132
+ const actions = handler.actions();
133
+
134
+ if (command === 'help') {
135
+ showHelp(ctx);
136
+ process.exit(0);
137
+ }
138
+
139
+ if (!actions.includes(command)) {
140
+ console.error(`Unknown command: ${command}`);
141
+ console.error(`Available: ${actions.join(', ')}`);
142
+ process.exit(1);
143
+ }
144
+
145
+ try {
146
+ const result = await handler.execute(command, ctx);
147
+ console.log(JSON.stringify(result, null, 2));
148
+ } catch (err) {
149
+ console.error(JSON.stringify({
150
+ error: err.message,
151
+ action: command,
152
+ technique,
153
+ domain,
154
+ mode,
155
+ }, null, 2));
156
+ process.exit(1);
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Parse --flag value pairs from argv.
162
+ */
163
+ function parseFlags(args) {
164
+ const flags = {};
165
+ for (let i = 0; i < args.length; i++) {
166
+ if (args[i].startsWith('--')) {
167
+ const key = args[i].slice(2);
168
+ const val = (i + 1 < args.length && !args[i + 1].startsWith('--'))
169
+ ? args[++i]
170
+ : true;
171
+ flags[key] = val;
172
+ }
173
+ }
174
+ return flags;
175
+ }
176
+
177
+ /**
178
+ * Show help for this technique's available actions.
179
+ */
180
+ function showHelp(ctx) {
181
+ const handler = getHandler(ctx.mode);
182
+ const actions = handler.actions();
183
+ const title = ctx.skill?.frontmatter?.name || ctx.technique;
184
+ const desc = ctx.skill?.frontmatter?.description || '';
185
+
186
+ console.log(`\n ${title}`);
187
+ if (desc) console.log(` ${desc.slice(0, 120)}`);
188
+ console.log(`\n Mode: ${ctx.mode.toUpperCase()} | Domain: ${ctx.domain}`);
189
+ console.log(`\n Commands:`);
190
+ for (const action of actions) {
191
+ const info = handler.describe(action);
192
+ console.log(` ${action.padEnd(16)} ${info}`);
193
+ }
194
+ console.log(`\n Usage: node agent.js <command> [--flag value ...]`);
195
+ console.log();
196
+ }
@@ -0,0 +1,316 @@
1
+ // Copyright (c) 2026 defconxt. All rights reserved.
2
+ // Licensed under AGPL-3.0 — see LICENSE file for details.
3
+
4
+ /**
5
+ * SKILL.md / api-reference.md parser
6
+ *
7
+ * Extracts structured data from markdown technique files:
8
+ * - YAML frontmatter (name, description, tags, mitre-attack, tools)
9
+ * - Tables (as arrays of row objects keyed by header)
10
+ * - Code blocks (with language tag)
11
+ * - Sections (h2/h3 hierarchy)
12
+ * - ATT&CK technique IDs (TXxxx.xxx pattern)
13
+ */
14
+
15
+ /**
16
+ * Parse a SKILL.md or api-reference.md file into structured data.
17
+ * @param {string} raw - raw markdown content
18
+ * @returns {ParsedSkill}
19
+ */
20
+ export function parseSkillMd(raw) {
21
+ const { frontmatter, body } = extractFrontmatter(raw);
22
+ const sections = extractSections(body);
23
+ const tables = extractTables(body);
24
+ const codeBlocks = extractCodeBlocks(body);
25
+ const attackIds = extractAttackIds(raw);
26
+ const tools = frontmatter?.metadata?.tools || extractToolNames(raw);
27
+
28
+ return {
29
+ frontmatter,
30
+ sections,
31
+ tables,
32
+ codeBlocks,
33
+ attackIds,
34
+ tools,
35
+ raw: body,
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Extract YAML frontmatter between --- delimiters.
41
+ * Lightweight parser — handles scalar, list, and nested map values.
42
+ */
43
+ function extractFrontmatter(raw) {
44
+ // Strip leading HTML comments (copyright headers) before looking for frontmatter
45
+ const stripped = raw.replace(/^(?:\s*<!--[\s\S]*?-->\s*)+/, '');
46
+ const match = stripped.match(/^---\r?\n([\s\S]*?)\r?\n---/);
47
+ if (!match) return { frontmatter: {}, body: raw };
48
+
49
+ const yaml = match[1];
50
+ // Calculate body from stripped string (comments already removed)
51
+ const body = stripped.slice(match[0].length).trim();
52
+ const frontmatter = parseSimpleYaml(yaml);
53
+ return { frontmatter, body };
54
+ }
55
+
56
+ /**
57
+ * Minimal YAML parser — supports scalars, arrays (- item), and one level of nesting.
58
+ */
59
+ function parseSimpleYaml(yaml) {
60
+ const result = {};
61
+ let currentKey = null;
62
+ let currentList = null;
63
+ let nestedObj = null;
64
+ let nestedKey = null;
65
+
66
+ for (const line of yaml.split('\n')) {
67
+ // Blank line
68
+ if (!line.trim()) continue;
69
+
70
+ // List item (2-space indent)
71
+ const listMatch = line.match(/^ {2}- (.+)$/);
72
+ if (listMatch && currentKey) {
73
+ if (!currentList) currentList = [];
74
+ let val = listMatch[1].trim();
75
+ // Strip quotes
76
+ if ((val.startsWith('"') && val.endsWith('"')) ||
77
+ (val.startsWith("'") && val.endsWith("'"))) {
78
+ val = val.slice(1, -1);
79
+ }
80
+ currentList.push(val);
81
+ continue;
82
+ }
83
+
84
+ // Nested key (2-space indent, key: value)
85
+ const nestedMatch = line.match(/^ {2}(\w[\w-]*)\s*:\s*(.+)?$/);
86
+ if (nestedMatch && nestedKey) {
87
+ if (!nestedObj) nestedObj = {};
88
+ const nk = nestedMatch[1];
89
+ let nv = nestedMatch[2]?.trim() || '';
90
+ // Handle inline list: ["T1222.001", "T1484.001"]
91
+ if (nv.startsWith('[') && nv.endsWith(']')) {
92
+ nv = nv.slice(1, -1).split(',').map(s => {
93
+ s = s.trim();
94
+ return (s.startsWith('"') || s.startsWith("'")) ? s.slice(1, -1) : s;
95
+ });
96
+ }
97
+ nestedObj[nk] = nv;
98
+ continue;
99
+ }
100
+
101
+ // Flush pending list/nested
102
+ if (currentList && currentKey) {
103
+ result[currentKey] = currentList;
104
+ currentList = null;
105
+ }
106
+ if (nestedObj && nestedKey) {
107
+ result[nestedKey] = nestedObj;
108
+ nestedObj = null;
109
+ nestedKey = null;
110
+ }
111
+
112
+ // Top-level key: value
113
+ const kvMatch = line.match(/^(\w[\w-]*)\s*:\s*(.*)?$/);
114
+ if (kvMatch) {
115
+ currentKey = kvMatch[1];
116
+ let val = kvMatch[2]?.trim() || '';
117
+
118
+ if (val === '' || val === '>-') {
119
+ // Possibly a list, nested object, or multi-line scalar follows
120
+ if (val === '>-') {
121
+ // Folded scalar — collect following indented lines
122
+ currentKey = kvMatch[1];
123
+ // We'll handle this by checking next lines
124
+ result[currentKey] = '';
125
+ continue;
126
+ }
127
+ // Could be list or nested — peek at next lines
128
+ nestedKey = currentKey;
129
+ continue;
130
+ }
131
+
132
+ // Strip quotes
133
+ if ((val.startsWith('"') && val.endsWith('"')) ||
134
+ (val.startsWith("'") && val.endsWith("'"))) {
135
+ val = val.slice(1, -1);
136
+ }
137
+ // Inline list
138
+ if (val.startsWith('[') && val.endsWith(']')) {
139
+ val = val.slice(1, -1).split(',').map(s => {
140
+ s = s.trim();
141
+ return (s.startsWith('"') || s.startsWith("'")) ? s.slice(1, -1) : s;
142
+ });
143
+ }
144
+ result[currentKey] = val;
145
+ currentList = null;
146
+ nestedKey = null;
147
+ } else {
148
+ // Continuation of folded scalar
149
+ const trimmed = line.trim();
150
+ if (currentKey && typeof result[currentKey] === 'string') {
151
+ result[currentKey] = result[currentKey]
152
+ ? result[currentKey] + ' ' + trimmed
153
+ : trimmed;
154
+ }
155
+ }
156
+ }
157
+
158
+ // Flush trailing
159
+ if (currentList && currentKey) result[currentKey] = currentList;
160
+ if (nestedObj && nestedKey) result[nestedKey] = nestedObj;
161
+
162
+ return result;
163
+ }
164
+
165
+ /**
166
+ * Extract markdown sections (h2 and h3).
167
+ * Returns array of { level, title, content }.
168
+ */
169
+ function extractSections(body) {
170
+ const sections = [];
171
+ const lines = body.split('\n');
172
+ let current = null;
173
+ const contentLines = [];
174
+
175
+ for (const line of lines) {
176
+ const h2 = line.match(/^## (.+)$/);
177
+ const h3 = line.match(/^### (.+)$/);
178
+
179
+ if (h2 || h3) {
180
+ if (current) {
181
+ current.content = contentLines.splice(0).join('\n').trim();
182
+ sections.push(current);
183
+ }
184
+ current = {
185
+ level: h2 ? 2 : 3,
186
+ title: (h2 || h3)[1].trim(),
187
+ content: '',
188
+ };
189
+ } else {
190
+ contentLines.push(line);
191
+ }
192
+ }
193
+
194
+ if (current) {
195
+ current.content = contentLines.join('\n').trim();
196
+ sections.push(current);
197
+ }
198
+
199
+ return sections;
200
+ }
201
+
202
+ /**
203
+ * Extract markdown tables.
204
+ * Returns array of { headers: string[], rows: object[] }.
205
+ */
206
+ function extractTables(body) {
207
+ const tables = [];
208
+ const lines = body.split('\n');
209
+ let i = 0;
210
+
211
+ while (i < lines.length) {
212
+ const line = lines[i];
213
+ // Detect table header row: | Header | Header |
214
+ if (line.match(/^\|.+\|$/) && i + 1 < lines.length && lines[i + 1].match(/^\|[-:\s|]+\|$/)) {
215
+ const headers = line.split('|').slice(1, -1).map(h => h.trim());
216
+ i += 2; // skip separator row
217
+ const rows = [];
218
+ while (i < lines.length && lines[i].match(/^\|.+\|$/)) {
219
+ const cells = lines[i].split('|').slice(1, -1).map(c => c.trim());
220
+ const row = {};
221
+ headers.forEach((h, idx) => { row[h] = cells[idx] || ''; });
222
+ rows.push(row);
223
+ i++;
224
+ }
225
+ tables.push({ headers, rows });
226
+ } else {
227
+ i++;
228
+ }
229
+ }
230
+
231
+ return tables;
232
+ }
233
+
234
+ /**
235
+ * Extract fenced code blocks.
236
+ * Returns array of { lang, code, context }.
237
+ * context = the heading above the code block (nearest h2/h3).
238
+ */
239
+ function extractCodeBlocks(body) {
240
+ const blocks = [];
241
+ const lines = body.split('\n');
242
+ let currentHeading = '';
243
+
244
+ let i = 0;
245
+ while (i < lines.length) {
246
+ // Track headings
247
+ const hMatch = lines[i].match(/^#{2,3} (.+)$/);
248
+ if (hMatch) {
249
+ currentHeading = hMatch[1].trim();
250
+ i++;
251
+ continue;
252
+ }
253
+
254
+ // Detect code fence
255
+ const fenceMatch = lines[i].match(/^```(\w*)$/);
256
+ if (fenceMatch) {
257
+ const lang = fenceMatch[1] || 'text';
258
+ i++;
259
+ const codeLines = [];
260
+ while (i < lines.length && !lines[i].match(/^```$/)) {
261
+ codeLines.push(lines[i]);
262
+ i++;
263
+ }
264
+ blocks.push({
265
+ lang,
266
+ code: codeLines.join('\n'),
267
+ context: currentHeading,
268
+ });
269
+ i++; // skip closing fence
270
+ } else {
271
+ i++;
272
+ }
273
+ }
274
+
275
+ return blocks;
276
+ }
277
+
278
+ /**
279
+ * Extract MITRE ATT&CK technique IDs from text.
280
+ * Matches T####, T####.###
281
+ */
282
+ function extractAttackIds(text) {
283
+ const ids = new Set();
284
+ const re = /T\d{4}(?:\.\d{3})?/g;
285
+ let m;
286
+ while ((m = re.exec(text))) {
287
+ ids.add(m[0]);
288
+ }
289
+ return [...ids];
290
+ }
291
+
292
+ /**
293
+ * Extract tool names from markdown.
294
+ * Looks for tool mentions in headings, table cells, and code blocks.
295
+ */
296
+ function extractToolNames(raw) {
297
+ // Known tool patterns
298
+ const known = [
299
+ 'nmap', 'nikto', 'burpsuite', 'metasploit', 'wireshark', 'bloodhound',
300
+ 'powerview', 'impacket', 'certipy', 'nuclei', 'katana', 'ffuf', 'gobuster',
301
+ 'hashcat', 'john', 'hydra', 'sqlmap', 'subfinder', 'amass', 'shodan',
302
+ 'ghidra', 'ida', 'gdb', 'radare2', 'frida', 'mimikatz', 'rubeus',
303
+ 'crackmapexec', 'covenant', 'sliver', 'cobalt strike', 'empire',
304
+ 'volatility', 'autopsy', 'velociraptor', 'osquery', 'sigma', 'yara',
305
+ 'snort', 'suricata', 'zeek', 'sysmon', 'wevtutil', 'auditd',
306
+ 'terraform', 'ansible', 'prowler', 'scoutsuite', 'checkov',
307
+ 'trivy', 'grype', 'syft', 'cosign', 'falco', 'caldera', 'atomic red team',
308
+ 'bloodyad', 'certipy', 'petitpotam', 'responder', 'bettercap',
309
+ ];
310
+ const found = [];
311
+ const lower = raw.toLowerCase();
312
+ for (const tool of known) {
313
+ if (lower.includes(tool)) found.push(tool);
314
+ }
315
+ return [...new Set(found)];
316
+ }
@@ -177,13 +177,20 @@ export class SkillQualityAnalyzer {
177
177
  if (existsSync(agentJs)) {
178
178
  const agentContent = readFileSync(agentJs, 'utf-8');
179
179
  const agentLines = agentContent.trim().split('\n');
180
- if (agentLines.length < SkillQualityAnalyzer.MIN_AGENT_PY_LINES) {
181
- issues.push(`agent.js too short (${agentLines.length} lines)`);
180
+ // New runtime pattern: 3-line wrapper importing from agent-runtime
181
+ const usesRuntime = agentContent.includes('agent-runtime');
182
+ if (usesRuntime) {
183
+ // Runtime wrapper delegates to shared runtime — full quality
184
+ scores.agent_quality = 1.0;
185
+ } else {
186
+ // Legacy standalone agent.js — validate inline content
187
+ if (agentLines.length < SkillQualityAnalyzer.MIN_AGENT_PY_LINES) {
188
+ issues.push(`agent.js too short (${agentLines.length} lines)`);
189
+ }
190
+ scores.agent_quality = Math.min(agentLines.length / 50, 1.0);
191
+ if (!agentContent.includes('process.argv')) issues.push('agent.js missing CLI dispatch');
192
+ if (!agentContent.includes('json')) issues.push('agent.js missing JSON output');
182
193
  }
183
- scores.agent_quality = Math.min(agentLines.length / 50, 1.0);
184
- if (!agentContent.includes('process.argv')) issues.push('agent.js missing CLI dispatch');
185
- if (!agentContent.includes('json')) issues.push('agent.js missing JSON output');
186
- if (!agentContent.includes('process.argv')) issues.push('agent.js missing CLI entry point');
187
194
  } else {
188
195
  issues.push('scripts/agent.js missing');
189
196
  scores.agent_quality = 0;