cipher-security 5.0.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 +465 -0
- package/lib/api/billing.js +321 -0
- package/lib/api/compliance.js +693 -0
- package/lib/api/controls.js +1401 -0
- package/lib/api/index.js +49 -0
- package/lib/api/marketplace.js +467 -0
- package/lib/api/openai-proxy.js +383 -0
- package/lib/api/server.js +685 -0
- package/lib/autonomous/feedback-loop.js +554 -0
- package/lib/autonomous/framework.js +512 -0
- package/lib/autonomous/index.js +97 -0
- package/lib/autonomous/leaderboard.js +594 -0
- package/lib/autonomous/modes/architect.js +412 -0
- package/lib/autonomous/modes/blue.js +386 -0
- package/lib/autonomous/modes/incident.js +684 -0
- package/lib/autonomous/modes/privacy.js +369 -0
- package/lib/autonomous/modes/purple.js +294 -0
- package/lib/autonomous/modes/recon.js +250 -0
- package/lib/autonomous/parallel.js +587 -0
- package/lib/autonomous/researcher.js +583 -0
- package/lib/autonomous/runner.js +955 -0
- package/lib/autonomous/scheduler.js +615 -0
- package/lib/autonomous/task-parser.js +127 -0
- package/lib/autonomous/validators/forensic.js +266 -0
- package/lib/autonomous/validators/osint.js +216 -0
- package/lib/autonomous/validators/privacy.js +296 -0
- package/lib/autonomous/validators/purple.js +298 -0
- package/lib/autonomous/validators/sigma.js +248 -0
- package/lib/autonomous/validators/threat-model.js +363 -0
- package/lib/benchmark/agent.js +119 -0
- package/lib/benchmark/baselines.js +43 -0
- package/lib/benchmark/builder.js +143 -0
- package/lib/benchmark/config.js +35 -0
- package/lib/benchmark/coordinator.js +91 -0
- package/lib/benchmark/index.js +20 -0
- package/lib/benchmark/llm.js +58 -0
- package/lib/benchmark/models.js +137 -0
- package/lib/benchmark/reporter.js +103 -0
- package/lib/benchmark/runner.js +103 -0
- package/lib/benchmark/sandbox.js +96 -0
- package/lib/benchmark/scorer.js +32 -0
- package/lib/benchmark/solver.js +166 -0
- package/lib/benchmark/tools.js +62 -0
- package/lib/bot/bot.js +130 -0
- package/lib/commands.js +99 -0
- package/lib/complexity.js +377 -0
- package/lib/config.js +213 -0
- package/lib/gateway/client.js +309 -0
- package/lib/gateway/commands.js +830 -0
- package/lib/gateway/config-validate.js +109 -0
- package/lib/gateway/gateway.js +367 -0
- package/lib/gateway/index.js +62 -0
- package/lib/gateway/mode.js +309 -0
- package/lib/gateway/plugins.js +222 -0
- package/lib/gateway/prompt.js +214 -0
- package/lib/mcp/server.js +262 -0
- package/lib/memory/compressor.js +425 -0
- package/lib/memory/engine.js +763 -0
- package/lib/memory/evolution.js +668 -0
- package/lib/memory/index.js +58 -0
- package/lib/memory/orchestrator.js +506 -0
- package/lib/memory/retriever.js +515 -0
- package/lib/memory/synthesizer.js +333 -0
- package/lib/pipeline/async-scanner.js +510 -0
- package/lib/pipeline/binary-analysis.js +1043 -0
- package/lib/pipeline/dom-xss-scanner.js +435 -0
- package/lib/pipeline/github-actions.js +792 -0
- package/lib/pipeline/index.js +124 -0
- package/lib/pipeline/osint.js +498 -0
- package/lib/pipeline/sarif.js +373 -0
- package/lib/pipeline/scanner.js +880 -0
- package/lib/pipeline/template-manager.js +525 -0
- package/lib/pipeline/xss-scanner.js +353 -0
- package/lib/setup-wizard.js +229 -0
- package/package.json +30 -0
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
|
+
// CIPHER is a trademark of defconxt.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* commands.js — Node.js handler functions for all 22 bridge-mode commands.
|
|
7
|
+
*
|
|
8
|
+
* Each handler accepts a plain args object and returns a plain JS object.
|
|
9
|
+
* No Rich formatting, no Typer framework — just data. The rendering
|
|
10
|
+
* layer stays in cipher.js's formatBridgeResult().
|
|
11
|
+
*
|
|
12
|
+
* All module imports are lazy (dynamic import()) to preserve cold start.
|
|
13
|
+
*
|
|
14
|
+
* @module gateway/commands
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFileSync, existsSync, readdirSync, writeFileSync, statSync } from 'node:fs';
|
|
18
|
+
import { join, dirname, resolve } from 'node:path';
|
|
19
|
+
import { execSync } from 'node:child_process';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
|
|
23
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
+
const __dirname = dirname(__filename);
|
|
25
|
+
|
|
26
|
+
const debug = process.env.CIPHER_DEBUG === '1'
|
|
27
|
+
? (/** @type {string} */ msg) => process.stderr.write(`[bridge:node] commands: ${msg}\n`)
|
|
28
|
+
: () => {};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Default memory directory — mirrors Python's ~/.cipher/memory/default.
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
function defaultMemoryDir() {
|
|
35
|
+
return join(homedir(), '.cipher', 'memory', 'default');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the repository root by walking up from this file.
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
function findRepoRoot() {
|
|
43
|
+
let dir = resolve(__dirname, '..', '..');
|
|
44
|
+
for (let i = 0; i < 10; i++) {
|
|
45
|
+
if (existsSync(join(dir, 'skills')) && (existsSync(join(dir, 'cli')) || existsSync(join(dir, 'package.json')))) {
|
|
46
|
+
return dir;
|
|
47
|
+
}
|
|
48
|
+
const parent = dirname(dir);
|
|
49
|
+
if (parent === dir) break;
|
|
50
|
+
dir = parent;
|
|
51
|
+
}
|
|
52
|
+
return resolve(__dirname, '..', '..');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Read package.json version from the CLI package.
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
function readVersion() {
|
|
60
|
+
try {
|
|
61
|
+
const pkgPath = join(__dirname, '..', '..', 'package.json');
|
|
62
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
63
|
+
return pkg.version || 'unknown';
|
|
64
|
+
} catch {
|
|
65
|
+
return 'unknown';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the skills directory path.
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
function skillsDir() {
|
|
74
|
+
const root = findRepoRoot();
|
|
75
|
+
return join(root, 'skills');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Memory-backed handlers (8)
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Search cross-session memory.
|
|
84
|
+
* @param {object} args
|
|
85
|
+
* @param {string} args.query
|
|
86
|
+
* @param {string} [args.engagement]
|
|
87
|
+
* @param {number} [args.limit]
|
|
88
|
+
* @returns {Promise<object>}
|
|
89
|
+
*/
|
|
90
|
+
export async function handleSearch(args = {}) {
|
|
91
|
+
const { AdaptiveRetriever, CipherMemory } = await import('../memory/index.js');
|
|
92
|
+
const memory = new CipherMemory(defaultMemoryDir());
|
|
93
|
+
try {
|
|
94
|
+
const retriever = new AdaptiveRetriever(memory);
|
|
95
|
+
const results = retriever.retrieve(
|
|
96
|
+
args.query || '',
|
|
97
|
+
args.engagement || '',
|
|
98
|
+
args.limit || 10,
|
|
99
|
+
);
|
|
100
|
+
debug(`search: ${results.length} results for "${args.query}"`);
|
|
101
|
+
return {
|
|
102
|
+
query: args.query || '',
|
|
103
|
+
count: results.length,
|
|
104
|
+
results: results.map(r => ({
|
|
105
|
+
content: r.content,
|
|
106
|
+
type: r.memoryType || '',
|
|
107
|
+
severity: r.severity || '',
|
|
108
|
+
targets: r.targets || [],
|
|
109
|
+
mitre: r.mitreAttack || [],
|
|
110
|
+
created: r.createdAt || '',
|
|
111
|
+
})),
|
|
112
|
+
};
|
|
113
|
+
} finally {
|
|
114
|
+
memory.close();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Store finding/IOC/TTP in memory.
|
|
120
|
+
* @param {object} args
|
|
121
|
+
* @param {string} args.content
|
|
122
|
+
* @param {string} [args.type]
|
|
123
|
+
* @param {string} [args.severity]
|
|
124
|
+
* @param {string} [args.engagement]
|
|
125
|
+
* @param {string[]} [args.tags]
|
|
126
|
+
* @returns {Promise<object>}
|
|
127
|
+
*/
|
|
128
|
+
export async function handleStore(args = {}) {
|
|
129
|
+
const { CipherMemory, MemoryEntry, MemoryType } = await import('../memory/index.js');
|
|
130
|
+
const memory = new CipherMemory(defaultMemoryDir());
|
|
131
|
+
try {
|
|
132
|
+
const typeStr = args.type || 'note';
|
|
133
|
+
const entry = new MemoryEntry({
|
|
134
|
+
content: args.content || '',
|
|
135
|
+
memoryType: Object.values(MemoryType).includes(typeStr) ? typeStr : MemoryType.NOTE,
|
|
136
|
+
severity: args.severity || '',
|
|
137
|
+
engagementId: args.engagement || '',
|
|
138
|
+
tags: Array.isArray(args.tags)
|
|
139
|
+
? args.tags
|
|
140
|
+
: (typeof args.tags === 'string' && args.tags ? args.tags.split(',') : []),
|
|
141
|
+
});
|
|
142
|
+
const entryId = memory.store(entry);
|
|
143
|
+
debug(`store: id=${entryId} type=${typeStr}`);
|
|
144
|
+
return { id: entryId, type: typeStr, stored: true };
|
|
145
|
+
} finally {
|
|
146
|
+
memory.close();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Show memory and platform statistics.
|
|
152
|
+
* @returns {Promise<object>}
|
|
153
|
+
*/
|
|
154
|
+
export async function handleStats(args = {}) {
|
|
155
|
+
const { CipherMemory } = await import('../memory/index.js');
|
|
156
|
+
const memory = new CipherMemory(defaultMemoryDir());
|
|
157
|
+
try {
|
|
158
|
+
const memStats = memory.stats();
|
|
159
|
+
const version = readVersion();
|
|
160
|
+
const sDir = skillsDir();
|
|
161
|
+
let skillCount = 0;
|
|
162
|
+
let domainCount = 0;
|
|
163
|
+
let scriptCount = 0;
|
|
164
|
+
|
|
165
|
+
if (existsSync(sDir)) {
|
|
166
|
+
try {
|
|
167
|
+
// Count SKILL.md files recursively
|
|
168
|
+
skillCount = countFiles(sDir, 'SKILL.md');
|
|
169
|
+
// Count top-level domain directories
|
|
170
|
+
domainCount = readdirSync(sDir).filter(d => {
|
|
171
|
+
try {
|
|
172
|
+
return !d.startsWith('.') && statSync(join(sDir, d)).isDirectory();
|
|
173
|
+
} catch { return false; }
|
|
174
|
+
}).length;
|
|
175
|
+
// Count script files recursively
|
|
176
|
+
scriptCount = countFiles(sDir, '*.py', 'scripts');
|
|
177
|
+
} catch {
|
|
178
|
+
// Skills dir unreadable
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
debug(`stats: version=${version} memory=${JSON.stringify(memStats)}`);
|
|
183
|
+
return {
|
|
184
|
+
version,
|
|
185
|
+
memory: memStats,
|
|
186
|
+
skills: { total: skillCount, domains: domainCount, scripts: scriptCount },
|
|
187
|
+
};
|
|
188
|
+
} finally {
|
|
189
|
+
memory.close();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Score response quality (PRM evaluation).
|
|
195
|
+
* @param {object} args
|
|
196
|
+
* @param {string} args.query
|
|
197
|
+
* @param {string} args.response
|
|
198
|
+
* @param {string} [args.mode]
|
|
199
|
+
* @returns {Promise<object>}
|
|
200
|
+
*/
|
|
201
|
+
export async function handleScore(args = {}) {
|
|
202
|
+
const { ResponseScorer } = await import('../memory/index.js');
|
|
203
|
+
const scorer = new ResponseScorer();
|
|
204
|
+
const scored = scorer.score(
|
|
205
|
+
args.query || '',
|
|
206
|
+
args.response || '',
|
|
207
|
+
args.mode || '',
|
|
208
|
+
);
|
|
209
|
+
debug(`score: ${scored.score}`);
|
|
210
|
+
return {
|
|
211
|
+
score: scored.score,
|
|
212
|
+
votes: scored.votes,
|
|
213
|
+
feedback: scored.feedback,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Export memory database to JSON.
|
|
219
|
+
* @param {object} args
|
|
220
|
+
* @param {string} [args.output]
|
|
221
|
+
* @param {string} [args.engagement]
|
|
222
|
+
* @returns {Promise<object>}
|
|
223
|
+
*/
|
|
224
|
+
export async function handleMemoryExport(args = {}) {
|
|
225
|
+
const { CipherMemory } = await import('../memory/index.js');
|
|
226
|
+
const memory = new CipherMemory(defaultMemoryDir());
|
|
227
|
+
try {
|
|
228
|
+
const results = memory.search('', {}, 10000);
|
|
229
|
+
let entries = results.map(r => (typeof r.toDict === 'function' ? r.toDict() : r));
|
|
230
|
+
if (args.engagement) {
|
|
231
|
+
entries = entries.filter(e =>
|
|
232
|
+
(e.engagement_id || e.engagementId) === args.engagement
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
const output = args.output || 'cipher-memory-export.json';
|
|
236
|
+
const data = { entries, count: entries.length };
|
|
237
|
+
writeFileSync(output, JSON.stringify(data, null, 2), 'utf-8');
|
|
238
|
+
debug(`memory-export: ${entries.length} entries → ${output}`);
|
|
239
|
+
return { exported: entries.length, file: output };
|
|
240
|
+
} finally {
|
|
241
|
+
memory.close();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Import memory from JSON backup.
|
|
247
|
+
* @param {object} args
|
|
248
|
+
* @param {string} args.file
|
|
249
|
+
* @returns {Promise<object>}
|
|
250
|
+
*/
|
|
251
|
+
export async function handleMemoryImport(args = {}) {
|
|
252
|
+
const file = args.file;
|
|
253
|
+
if (!file || !existsSync(file)) {
|
|
254
|
+
return { error: true, message: `File not found: ${file || '(none)'}` };
|
|
255
|
+
}
|
|
256
|
+
const { CipherMemory, MemoryEntry } = await import('../memory/index.js');
|
|
257
|
+
const memory = new CipherMemory(defaultMemoryDir());
|
|
258
|
+
try {
|
|
259
|
+
const raw = JSON.parse(readFileSync(file, 'utf-8'));
|
|
260
|
+
const entries = raw.entries || (Array.isArray(raw) ? raw : []);
|
|
261
|
+
let imported = 0;
|
|
262
|
+
for (const entryData of entries) {
|
|
263
|
+
try {
|
|
264
|
+
const entry = MemoryEntry.fromDict(entryData);
|
|
265
|
+
memory.store(entry);
|
|
266
|
+
imported++;
|
|
267
|
+
} catch {
|
|
268
|
+
// Skip invalid entries
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
debug(`memory-import: ${imported}/${entries.length} from ${file}`);
|
|
272
|
+
return { imported, total: entries.length, file };
|
|
273
|
+
} finally {
|
|
274
|
+
memory.close();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Trigger memory ingestion pipeline.
|
|
280
|
+
* @returns {Promise<object>}
|
|
281
|
+
*/
|
|
282
|
+
export async function handleIngest(args = {}) {
|
|
283
|
+
const { CipherMemory } = await import('../memory/index.js');
|
|
284
|
+
const memory = new CipherMemory(defaultMemoryDir());
|
|
285
|
+
try {
|
|
286
|
+
const stats = memory.consolidate();
|
|
287
|
+
debug('ingest: consolidation complete');
|
|
288
|
+
return { ingested: true, ...stats };
|
|
289
|
+
} finally {
|
|
290
|
+
memory.close();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Config check + memory stats + backend reachability.
|
|
296
|
+
* @returns {Promise<object>}
|
|
297
|
+
*/
|
|
298
|
+
export async function handleStatus(args = {}) {
|
|
299
|
+
const { loadConfig, configExists } = await import('../config.js');
|
|
300
|
+
const { validateConfig } = await import('./config-validate.js');
|
|
301
|
+
|
|
302
|
+
const hasConfig = configExists();
|
|
303
|
+
let configValid = false;
|
|
304
|
+
let backend = 'unknown';
|
|
305
|
+
|
|
306
|
+
if (hasConfig) {
|
|
307
|
+
try {
|
|
308
|
+
const raw = loadConfig();
|
|
309
|
+
const config = validateConfig(raw);
|
|
310
|
+
configValid = true;
|
|
311
|
+
backend = config.backend;
|
|
312
|
+
} catch {
|
|
313
|
+
// Config present but invalid
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Memory stats
|
|
318
|
+
let memStats = null;
|
|
319
|
+
try {
|
|
320
|
+
const { CipherMemory } = await import('../memory/index.js');
|
|
321
|
+
const memory = new CipherMemory(defaultMemoryDir());
|
|
322
|
+
memStats = memory.stats();
|
|
323
|
+
memory.close();
|
|
324
|
+
} catch {
|
|
325
|
+
// Memory unavailable
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const status = configValid ? 'ready' : (hasConfig ? 'misconfigured' : 'unconfigured');
|
|
329
|
+
debug(`status: ${status} backend=${backend}`);
|
|
330
|
+
return {
|
|
331
|
+
status,
|
|
332
|
+
backend,
|
|
333
|
+
config: { exists: hasConfig, valid: configValid },
|
|
334
|
+
memory: memStats,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
// Pipeline-backed handlers (6)
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Analyze git diff for security issues.
|
|
344
|
+
* @param {object} args
|
|
345
|
+
* @param {string} args.file - Diff file path or diff text content
|
|
346
|
+
* @returns {Promise<object>}
|
|
347
|
+
*/
|
|
348
|
+
export async function handleDiff(args = {}) {
|
|
349
|
+
const { SecurityDiffAnalyzer } = await import('../pipeline/index.js');
|
|
350
|
+
const analyzer = new SecurityDiffAnalyzer();
|
|
351
|
+
|
|
352
|
+
let diffText = args.file || '';
|
|
353
|
+
// If it looks like a file path (no newlines, exists on disk), read it
|
|
354
|
+
if (diffText && !diffText.includes('\n') && existsSync(diffText)) {
|
|
355
|
+
diffText = readFileSync(diffText, 'utf-8');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const analysis = analyzer.analyzeDiff(diffText);
|
|
359
|
+
const riskVal = analysis.riskLevel?.value ?? analysis.riskLevel ?? 'info';
|
|
360
|
+
|
|
361
|
+
debug(`diff: risk=${riskVal} files=${analysis.filesChanged?.length}`);
|
|
362
|
+
return {
|
|
363
|
+
risk_level: riskVal,
|
|
364
|
+
files_changed: analysis.filesChanged?.length ?? 0,
|
|
365
|
+
auth_changes: analysis.authChanges?.length ?? 0,
|
|
366
|
+
sql_changes: analysis.sqlChanges?.length ?? 0,
|
|
367
|
+
crypto_changes: analysis.cryptoChanges?.length ?? 0,
|
|
368
|
+
secrets_found: analysis.secrets?.length ?? 0,
|
|
369
|
+
has_security_findings: analysis.hasSecurityFindings ?? false,
|
|
370
|
+
summary: analysis.summary || '',
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Generate GitHub Actions security workflow.
|
|
376
|
+
* @param {object} args
|
|
377
|
+
* @param {string} [args.profile]
|
|
378
|
+
* @returns {Promise<object>}
|
|
379
|
+
*/
|
|
380
|
+
export async function handleWorkflow(args = {}) {
|
|
381
|
+
const { WorkflowGenerator } = await import('../pipeline/index.js');
|
|
382
|
+
const gen = new WorkflowGenerator();
|
|
383
|
+
const workflow = gen.generateWorkflow(args.profile || 'pentest');
|
|
384
|
+
debug('workflow: generated');
|
|
385
|
+
return { workflow };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Convert scan results to SARIF v2.1.0.
|
|
390
|
+
* @param {object} args
|
|
391
|
+
* @param {string} args.input - Scan results JSON file path
|
|
392
|
+
* @param {string} [args.output]
|
|
393
|
+
* @returns {Promise<object>}
|
|
394
|
+
*/
|
|
395
|
+
export async function handleSarif(args = {}) {
|
|
396
|
+
const { SarifReport } = await import('../pipeline/index.js');
|
|
397
|
+
const input = args.input;
|
|
398
|
+
if (!input || !existsSync(input)) {
|
|
399
|
+
return { error: true, message: `Input file not found: ${input || '(none)'}` };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const data = JSON.parse(readFileSync(input, 'utf-8'));
|
|
403
|
+
const report = new SarifReport();
|
|
404
|
+
const findings = data.findings || (Array.isArray(data) ? data : []);
|
|
405
|
+
|
|
406
|
+
for (const finding of findings) {
|
|
407
|
+
report.addFinding(finding);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const output = args.output || 'cipher-results.sarif';
|
|
411
|
+
report.write(output);
|
|
412
|
+
const summary = report.summary();
|
|
413
|
+
|
|
414
|
+
debug(`sarif: ${findings.length} findings → ${output}`);
|
|
415
|
+
return { file: output, findings: findings.length, summary };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Run OSINT investigation (domain/IP).
|
|
420
|
+
* @param {object} args
|
|
421
|
+
* @param {string} args.target
|
|
422
|
+
* @param {string} [args.type]
|
|
423
|
+
* @returns {Promise<object>}
|
|
424
|
+
*/
|
|
425
|
+
export async function handleOsint(args = {}) {
|
|
426
|
+
const { OSINTPipeline } = await import('../pipeline/index.js');
|
|
427
|
+
const pipeline = new OSINTPipeline();
|
|
428
|
+
const target = args.target || '';
|
|
429
|
+
const type = args.type || 'domain';
|
|
430
|
+
|
|
431
|
+
const results = type === 'ip'
|
|
432
|
+
? pipeline.investigateIp(target)
|
|
433
|
+
: pipeline.investigateDomain(target);
|
|
434
|
+
|
|
435
|
+
const data = {
|
|
436
|
+
target,
|
|
437
|
+
type,
|
|
438
|
+
results: results.map(r => (typeof r.toDict === 'function' ? r.toDict() : r)),
|
|
439
|
+
summary: pipeline.summary(),
|
|
440
|
+
};
|
|
441
|
+
debug(`osint: ${target} (${type}) → ${results.length} results`);
|
|
442
|
+
return data;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* List all skill domains.
|
|
447
|
+
* @returns {Promise<object>}
|
|
448
|
+
*/
|
|
449
|
+
export async function handleDomains(args = {}) {
|
|
450
|
+
const sDir = skillsDir();
|
|
451
|
+
if (!existsSync(sDir)) {
|
|
452
|
+
return { domains: {}, total_domains: 0, total_techniques: 0 };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const domainMap = {};
|
|
456
|
+
const entries = readdirSync(sDir).sort();
|
|
457
|
+
for (const d of entries) {
|
|
458
|
+
const dirPath = join(sDir, d);
|
|
459
|
+
try {
|
|
460
|
+
if (d.startsWith('.') || !statSync(dirPath).isDirectory()) continue;
|
|
461
|
+
} catch { continue; }
|
|
462
|
+
|
|
463
|
+
const techniquesDir = join(dirPath, 'techniques');
|
|
464
|
+
if (existsSync(techniquesDir)) {
|
|
465
|
+
try {
|
|
466
|
+
const techniques = readdirSync(techniquesDir).filter(t => {
|
|
467
|
+
try {
|
|
468
|
+
return statSync(join(techniquesDir, t)).isDirectory();
|
|
469
|
+
} catch { return false; }
|
|
470
|
+
});
|
|
471
|
+
domainMap[d] = techniques.length;
|
|
472
|
+
} catch {
|
|
473
|
+
domainMap[d] = 0;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const totalTechniques = Object.values(domainMap).reduce((a, b) => a + b, 0);
|
|
479
|
+
debug(`domains: ${Object.keys(domainMap).length} domains, ${totalTechniques} techniques`);
|
|
480
|
+
return {
|
|
481
|
+
domains: domainMap,
|
|
482
|
+
total_domains: Object.keys(domainMap).length,
|
|
483
|
+
total_techniques: totalTechniques,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Search skills by keyword.
|
|
489
|
+
* @param {object} args
|
|
490
|
+
* @param {string} args.query
|
|
491
|
+
* @param {number} [args.limit]
|
|
492
|
+
* @returns {Promise<object>}
|
|
493
|
+
*/
|
|
494
|
+
export async function handleSkills(args = {}) {
|
|
495
|
+
const sDir = skillsDir();
|
|
496
|
+
const query = (args.query || '').toLowerCase();
|
|
497
|
+
const limit = args.limit || 10;
|
|
498
|
+
const results = [];
|
|
499
|
+
|
|
500
|
+
if (!existsSync(sDir)) {
|
|
501
|
+
return { query: args.query || '', count: 0, results: [] };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Walk skills dir looking for SKILL.md files
|
|
505
|
+
const walk = (dir, relBase) => {
|
|
506
|
+
if (results.length >= limit) return;
|
|
507
|
+
try {
|
|
508
|
+
const entries = readdirSync(dir);
|
|
509
|
+
for (const entry of entries) {
|
|
510
|
+
if (results.length >= limit) return;
|
|
511
|
+
const fullPath = join(dir, entry);
|
|
512
|
+
try {
|
|
513
|
+
if (statSync(fullPath).isDirectory()) {
|
|
514
|
+
walk(fullPath, join(relBase, entry));
|
|
515
|
+
} else if (entry === 'SKILL.md') {
|
|
516
|
+
const name = dirname(join(relBase, entry)).split('/').pop() || '';
|
|
517
|
+
const rel = dirname(join(relBase, entry));
|
|
518
|
+
if (query === '' || name.toLowerCase().includes(query) || rel.toLowerCase().includes(query)) {
|
|
519
|
+
results.push({ name, path: rel });
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
} catch { /* skip unreadable entries */ }
|
|
523
|
+
}
|
|
524
|
+
} catch { /* skip unreadable dirs */ }
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
walk(sDir, '');
|
|
528
|
+
debug(`skills: "${query}" → ${results.length} results`);
|
|
529
|
+
return { query: args.query || '', count: results.length, results };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ---------------------------------------------------------------------------
|
|
533
|
+
// Self-contained handlers (4)
|
|
534
|
+
// ---------------------------------------------------------------------------
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Read version from package.json.
|
|
538
|
+
* @returns {Promise<object>}
|
|
539
|
+
*/
|
|
540
|
+
export async function handleVersion(args = {}) {
|
|
541
|
+
return { version: readVersion() };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Comprehensive health check.
|
|
546
|
+
* @returns {Promise<object>}
|
|
547
|
+
*/
|
|
548
|
+
export async function handleDoctor(args = {}) {
|
|
549
|
+
const checks = [];
|
|
550
|
+
|
|
551
|
+
// Node.js version
|
|
552
|
+
checks.push({
|
|
553
|
+
name: 'Node.js',
|
|
554
|
+
status: 'ok',
|
|
555
|
+
detail: process.version,
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// npm version
|
|
559
|
+
try {
|
|
560
|
+
const npmVer = execSync('npm --version', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
561
|
+
checks.push({ name: 'npm', status: 'ok', detail: `v${npmVer}` });
|
|
562
|
+
} catch {
|
|
563
|
+
checks.push({ name: 'npm', status: 'missing', detail: 'npm not found' });
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// External tools (optional)
|
|
567
|
+
for (const tool of ['nuclei', 'katana', 'ollama']) {
|
|
568
|
+
try {
|
|
569
|
+
execSync(`which ${tool}`, { encoding: 'utf-8', timeout: 3000 });
|
|
570
|
+
checks.push({ name: tool, status: 'ok', detail: 'installed' });
|
|
571
|
+
} catch {
|
|
572
|
+
checks.push({ name: tool, status: 'optional', detail: `${tool} not found — optional` });
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Config validity
|
|
577
|
+
try {
|
|
578
|
+
const { loadConfig, configExists } = await import('../config.js');
|
|
579
|
+
const { validateConfig } = await import('./config-validate.js');
|
|
580
|
+
|
|
581
|
+
if (configExists()) {
|
|
582
|
+
const raw = loadConfig();
|
|
583
|
+
const config = validateConfig(raw);
|
|
584
|
+
checks.push({
|
|
585
|
+
name: 'Config',
|
|
586
|
+
status: 'ok',
|
|
587
|
+
detail: `backend=${config.backend} model=${config[`${config.backend}_model`] || 'default'}`,
|
|
588
|
+
});
|
|
589
|
+
} else {
|
|
590
|
+
checks.push({
|
|
591
|
+
name: 'Config',
|
|
592
|
+
status: 'missing',
|
|
593
|
+
detail: 'No config found — run: cipher setup',
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
} catch (err) {
|
|
597
|
+
checks.push({
|
|
598
|
+
name: 'Config',
|
|
599
|
+
status: 'error',
|
|
600
|
+
detail: err.message?.split('\n')[0] || 'Config validation failed',
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Memory database health
|
|
605
|
+
try {
|
|
606
|
+
const { CipherMemory } = await import('../memory/index.js');
|
|
607
|
+
const memory = new CipherMemory(defaultMemoryDir());
|
|
608
|
+
const stats = memory.stats();
|
|
609
|
+
memory.close();
|
|
610
|
+
checks.push({
|
|
611
|
+
name: 'Memory',
|
|
612
|
+
status: 'ok',
|
|
613
|
+
detail: `total=${stats.total ?? 0} active=${stats.active ?? 0} archived=${stats.archived ?? 0}`,
|
|
614
|
+
});
|
|
615
|
+
} catch (err) {
|
|
616
|
+
checks.push({
|
|
617
|
+
name: 'Memory',
|
|
618
|
+
status: 'error',
|
|
619
|
+
detail: err.message || 'Memory database unavailable',
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const healthy = checks.every(c => c.status === 'ok' || c.status === 'optional');
|
|
624
|
+
const summary = `${checks.filter(c => c.status === 'ok').length}/${checks.length} checks passed`;
|
|
625
|
+
|
|
626
|
+
debug(`doctor: ${summary}`);
|
|
627
|
+
return { checks, healthy, summary };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Plugin management.
|
|
632
|
+
* @param {object} args
|
|
633
|
+
* @param {string} [args.action] - 'list', 'install', 'remove'
|
|
634
|
+
* @returns {Promise<object>}
|
|
635
|
+
*/
|
|
636
|
+
export async function handlePlugin(args = {}) {
|
|
637
|
+
const { PluginManager } = await import('./plugins.js');
|
|
638
|
+
const pm = new PluginManager();
|
|
639
|
+
const action = args.action || 'list';
|
|
640
|
+
|
|
641
|
+
if (action === 'list') {
|
|
642
|
+
const plugins = pm.plugins;
|
|
643
|
+
return {
|
|
644
|
+
count: plugins.length,
|
|
645
|
+
plugins: plugins.map(p => ({
|
|
646
|
+
name: p.name,
|
|
647
|
+
version: p.version,
|
|
648
|
+
description: p.description,
|
|
649
|
+
author: p.author,
|
|
650
|
+
modes: p.modes,
|
|
651
|
+
priority: p.priority,
|
|
652
|
+
source: p.source,
|
|
653
|
+
})),
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// install/remove are placeholder — real implementation depends on registry
|
|
658
|
+
return {
|
|
659
|
+
error: true,
|
|
660
|
+
message: `Plugin action '${action}' not yet implemented. Only 'list' is available.`,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Non-streaming query path — delegates to Gateway.send().
|
|
666
|
+
* @param {object} args
|
|
667
|
+
* @param {string} args.message
|
|
668
|
+
* @param {Array<{role: string, content: string}>} [args.history]
|
|
669
|
+
* @returns {Promise<object>}
|
|
670
|
+
*/
|
|
671
|
+
export async function handleQuery(args = {}) {
|
|
672
|
+
const { Gateway } = await import('./gateway.js');
|
|
673
|
+
const gateway = new Gateway();
|
|
674
|
+
const response = await gateway.send(args.message || '', args.history);
|
|
675
|
+
debug('query: response received');
|
|
676
|
+
return { response, mode: 'auto' };
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// ---------------------------------------------------------------------------
|
|
680
|
+
// Stub handlers (4)
|
|
681
|
+
// ---------------------------------------------------------------------------
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* @returns {Promise<object>}
|
|
685
|
+
*/
|
|
686
|
+
export async function handleLeaderboard(args = {}) {
|
|
687
|
+
return {
|
|
688
|
+
error: true,
|
|
689
|
+
message: 'Leaderboard requires autonomous framework (available after S04). Use Python bridge: cipher leaderboard --bridge',
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* @returns {Promise<object>}
|
|
695
|
+
*/
|
|
696
|
+
export async function handleFeedback(args = {}) {
|
|
697
|
+
return {
|
|
698
|
+
error: true,
|
|
699
|
+
message: 'Feedback loop requires autonomous framework (available after S04). Use Python bridge: cipher feedback --bridge',
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* @returns {Promise<object>}
|
|
705
|
+
*/
|
|
706
|
+
export async function handleMarketplace(args = {}) {
|
|
707
|
+
return {
|
|
708
|
+
error: true,
|
|
709
|
+
message: 'Marketplace requires api/marketplace module (available after S05). Use Python bridge: cipher marketplace --bridge',
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* @returns {Promise<object>}
|
|
715
|
+
*/
|
|
716
|
+
export async function handleCompliance(args = {}) {
|
|
717
|
+
return {
|
|
718
|
+
error: true,
|
|
719
|
+
message: 'Compliance requires api/compliance module (available after S05). Use Python bridge: cipher compliance --bridge',
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// ---------------------------------------------------------------------------
|
|
724
|
+
// Helpers
|
|
725
|
+
// ---------------------------------------------------------------------------
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Recursively count files matching a filename pattern.
|
|
729
|
+
* @param {string} dir
|
|
730
|
+
* @param {string} filename - exact filename (e.g. 'SKILL.md') or glob-like '*.py'
|
|
731
|
+
* @param {string} [subdir] - optional subdirectory filter (only count in dirs named this)
|
|
732
|
+
* @returns {number}
|
|
733
|
+
*/
|
|
734
|
+
function countFiles(dir, filename, subdir) {
|
|
735
|
+
let count = 0;
|
|
736
|
+
const isGlob = filename.startsWith('*.');
|
|
737
|
+
const ext = isGlob ? filename.slice(1) : null;
|
|
738
|
+
|
|
739
|
+
const walk = (d) => {
|
|
740
|
+
try {
|
|
741
|
+
const entries = readdirSync(d);
|
|
742
|
+
for (const entry of entries) {
|
|
743
|
+
const fullPath = join(d, entry);
|
|
744
|
+
try {
|
|
745
|
+
const st = statSync(fullPath);
|
|
746
|
+
if (st.isDirectory()) {
|
|
747
|
+
if (!subdir || entry === subdir || d !== dir) {
|
|
748
|
+
walk(fullPath);
|
|
749
|
+
}
|
|
750
|
+
} else if (st.isFile()) {
|
|
751
|
+
if (isGlob ? entry.endsWith(ext) : entry === filename) {
|
|
752
|
+
if (!subdir || dirname(fullPath).split('/').includes(subdir)) {
|
|
753
|
+
count++;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
} catch { /* skip unreadable entries */ }
|
|
758
|
+
}
|
|
759
|
+
} catch { /* skip unreadable dirs */ }
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
walk(dir);
|
|
763
|
+
return count;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// ---------------------------------------------------------------------------
|
|
767
|
+
// Scan command — delegates to Node.js NucleiRunner
|
|
768
|
+
// ---------------------------------------------------------------------------
|
|
769
|
+
|
|
770
|
+
export async function handleScan(args = {}) {
|
|
771
|
+
const target = Array.isArray(args) ? args.find(a => !a.startsWith('-')) : args.target;
|
|
772
|
+
if (!target) {
|
|
773
|
+
return { error: true, message: 'Usage: cipher scan <target> [--profile <profile>]' };
|
|
774
|
+
}
|
|
775
|
+
try {
|
|
776
|
+
const { NucleiRunner, ScanProfile } = await import('../pipeline/scanner.js');
|
|
777
|
+
const runner = new NucleiRunner();
|
|
778
|
+
const profileArg = Array.isArray(args) ? (args.find((a, i) => args[i - 1] === '--profile') || 'standard') : (args.profile || 'standard');
|
|
779
|
+
const result = await runner.scan(target, { profile: ScanProfile.fromDomain(profileArg) });
|
|
780
|
+
return {
|
|
781
|
+
output: JSON.stringify({
|
|
782
|
+
target,
|
|
783
|
+
profile: profileArg,
|
|
784
|
+
findings: (result.findings || []).length,
|
|
785
|
+
status: 'completed',
|
|
786
|
+
}, null, 2),
|
|
787
|
+
};
|
|
788
|
+
} catch (err) {
|
|
789
|
+
return { error: true, message: `Scan failed: ${err.message}` };
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// ---------------------------------------------------------------------------
|
|
794
|
+
// Dashboard command — stub for Node.js TUI (Textual TUI not ported)
|
|
795
|
+
// ---------------------------------------------------------------------------
|
|
796
|
+
|
|
797
|
+
export async function handleDashboard() {
|
|
798
|
+
return {
|
|
799
|
+
output: JSON.stringify({
|
|
800
|
+
status: 'info',
|
|
801
|
+
message: 'Dashboard TUI is not yet available in Node.js mode. Use `cipher status` for system status or `cipher api` for the REST API.',
|
|
802
|
+
}, null, 2),
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// ---------------------------------------------------------------------------
|
|
807
|
+
// Web command — delegates to API server
|
|
808
|
+
// ---------------------------------------------------------------------------
|
|
809
|
+
|
|
810
|
+
export async function handleWeb(args = {}) {
|
|
811
|
+
return {
|
|
812
|
+
output: JSON.stringify({
|
|
813
|
+
status: 'info',
|
|
814
|
+
message: 'The web interface has been consolidated into `cipher api`. Use `cipher api --no-auth --port 8443` to start the REST API server.',
|
|
815
|
+
}, null, 2),
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// ---------------------------------------------------------------------------
|
|
820
|
+
// Setup Signal command — Signal bot configuration
|
|
821
|
+
// ---------------------------------------------------------------------------
|
|
822
|
+
|
|
823
|
+
export async function handleSetupSignal() {
|
|
824
|
+
return {
|
|
825
|
+
output: JSON.stringify({
|
|
826
|
+
status: 'info',
|
|
827
|
+
message: 'Signal setup: Set SIGNAL_SERVICE, SIGNAL_PHONE_NUMBER, and SIGNAL_WHITELIST environment variables, then run `cipher bot`.',
|
|
828
|
+
}, null, 2),
|
|
829
|
+
};
|
|
830
|
+
}
|