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.
- package/bin/cipher.js +11 -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/analyze/consistency.js +566 -0
- package/lib/analyze/constitution.js +110 -0
- package/lib/analyze/sharding.js +251 -0
- package/lib/autonomous/agent-tool.js +165 -0
- package/lib/autonomous/feedback-loop.js +13 -6
- package/lib/autonomous/framework.js +17 -0
- package/lib/autonomous/handoff.js +506 -0
- package/lib/autonomous/modes/blue.js +26 -0
- package/lib/autonomous/modes/red.js +585 -0
- package/lib/autonomous/modes/researcher.js +322 -0
- package/lib/autonomous/researcher.js +12 -45
- package/lib/autonomous/runner.js +9 -537
- package/lib/benchmark/agent.js +88 -26
- package/lib/benchmark/baselines.js +3 -0
- package/lib/benchmark/claude-code-solver.js +254 -0
- package/lib/benchmark/cognitive.js +283 -0
- package/lib/benchmark/index.js +12 -2
- package/lib/benchmark/knowledge.js +281 -0
- package/lib/benchmark/llm.js +156 -15
- package/lib/benchmark/models.js +5 -2
- package/lib/benchmark/nyu-ctf.js +192 -0
- package/lib/benchmark/overthewire.js +347 -0
- package/lib/benchmark/picoctf.js +281 -0
- package/lib/benchmark/prompts.js +280 -0
- package/lib/benchmark/registry.js +219 -0
- package/lib/benchmark/remote-solver.js +356 -0
- package/lib/benchmark/remote-target.js +263 -0
- package/lib/benchmark/reporter.js +35 -0
- package/lib/benchmark/runner.js +174 -10
- package/lib/benchmark/sandbox.js +35 -0
- package/lib/benchmark/scorer.js +22 -4
- package/lib/benchmark/solver.js +34 -1
- package/lib/benchmark/tools.js +262 -16
- package/lib/commands.js +9 -0
- package/lib/execution/council.js +434 -0
- package/lib/execution/parallel.js +292 -0
- package/lib/gates/circuit-breaker.js +135 -0
- package/lib/gates/confidence.js +302 -0
- package/lib/gates/corrections.js +219 -0
- package/lib/gates/self-check.js +245 -0
- package/lib/gateway/commands.js +727 -0
- package/lib/guardrails/engine.js +364 -0
- package/lib/mcp/server.js +349 -3
- package/lib/memory/compressor.js +94 -7
- package/lib/pipeline/hooks.js +288 -0
- package/lib/pipeline/index.js +11 -0
- package/lib/review/budget.js +210 -0
- package/lib/review/engine.js +526 -0
- package/lib/review/layers/acceptance-auditor.js +279 -0
- package/lib/review/layers/blind-hunter.js +500 -0
- package/lib/review/layers/defense-in-depth.js +209 -0
- package/lib/review/layers/edge-case-hunter.js +266 -0
- package/lib/review/panel.js +519 -0
- package/lib/review/two-stage.js +244 -0
- package/lib/session/cost-tracker.js +203 -0
- package/lib/session/logger.js +349 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|