cipher-security 2.0.2 → 2.0.3
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 +16 -26
- package/lib/api/billing.js +3 -0
- package/lib/api/marketplace.js +3 -0
- package/lib/autonomous/feedback-loop.js +9 -9
- package/lib/autonomous/leaderboard.js +2 -0
- package/lib/autonomous/researcher.js +5 -5
- package/lib/commands.js +1 -1
- package/lib/config.js +4 -4
- package/lib/gateway/commands.js +203 -61
- package/lib/gateway/prompt.js +4 -4
- package/lib/memory/compressor.js +1 -1
- package/lib/memory/engine.js +15 -11
- package/lib/pipeline/github-actions.js +6 -6
- package/lib/setup-wizard.js +1 -10
- package/package.json +1 -1
package/bin/cipher.js
CHANGED
|
@@ -9,15 +9,13 @@
|
|
|
9
9
|
* Fast path: --version / -V / --help / -h use only node:fs + node:path
|
|
10
10
|
* (zero dynamic imports, <200ms cold start).
|
|
11
11
|
*
|
|
12
|
-
* Command path: dynamically imports
|
|
13
|
-
*
|
|
12
|
+
* Command path: dynamically imports gateway/commands.js handlers,
|
|
13
|
+
* dispatches to native Node.js functions, prints results.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { readFileSync } from 'node:fs';
|
|
17
17
|
import { resolve, dirname } from 'node:path';
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
|
-
import { spawn } from 'node:child_process';
|
|
20
|
-
|
|
21
19
|
// ---------------------------------------------------------------------------
|
|
22
20
|
// Fast path — no dynamic imports, must complete in <200ms
|
|
23
21
|
// ---------------------------------------------------------------------------
|
|
@@ -123,7 +121,7 @@ if (args.length === 0) {
|
|
|
123
121
|
}
|
|
124
122
|
|
|
125
123
|
// ---------------------------------------------------------------------------
|
|
126
|
-
//
|
|
124
|
+
// Native commands — handled before dynamic imports
|
|
127
125
|
// ---------------------------------------------------------------------------
|
|
128
126
|
|
|
129
127
|
const nativeCommands = new Set(['setup']);
|
|
@@ -214,36 +212,30 @@ const knownCommands = new Set([
|
|
|
214
212
|
* @returns {{ command: string, commandArgs: string[] }}
|
|
215
213
|
*/
|
|
216
214
|
function parseCommand(argv) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
for (const arg of argv) {
|
|
221
|
-
if (arg.startsWith('-')) {
|
|
222
|
-
flags.push(arg);
|
|
223
|
-
} else {
|
|
224
|
-
positional.push(arg);
|
|
225
|
-
}
|
|
215
|
+
if (argv.length === 0) {
|
|
216
|
+
console.error('No command specified. Run `cipher --help` for usage.');
|
|
217
|
+
process.exit(1);
|
|
226
218
|
}
|
|
227
219
|
|
|
228
|
-
|
|
229
|
-
|
|
220
|
+
const first = argv[0];
|
|
221
|
+
|
|
222
|
+
if (first.startsWith('-')) {
|
|
223
|
+
// No command, just flags — treat as error
|
|
230
224
|
console.error('No command specified. Run `cipher --help` for usage.');
|
|
231
225
|
process.exit(1);
|
|
232
226
|
}
|
|
233
227
|
|
|
234
|
-
const first = positional[0];
|
|
235
|
-
|
|
236
228
|
if (knownCommands.has(first)) {
|
|
237
229
|
return {
|
|
238
230
|
command: first,
|
|
239
|
-
commandArgs:
|
|
231
|
+
commandArgs: argv.slice(1), // Pass all args as-is, preserve flag-value order
|
|
240
232
|
};
|
|
241
233
|
}
|
|
242
234
|
|
|
243
|
-
// Bare query — join
|
|
235
|
+
// Bare query — join everything as query text
|
|
244
236
|
return {
|
|
245
237
|
command: 'query',
|
|
246
|
-
commandArgs:
|
|
238
|
+
commandArgs: argv,
|
|
247
239
|
};
|
|
248
240
|
}
|
|
249
241
|
|
|
@@ -254,7 +246,7 @@ function parseCommand(argv) {
|
|
|
254
246
|
/**
|
|
255
247
|
* Format and print a bridge command result, then exit if needed.
|
|
256
248
|
*
|
|
257
|
-
* Result shapes from
|
|
249
|
+
* Result shapes from gateway/commands.js:
|
|
258
250
|
* 1. {output, exit_code, error: true} — command error: print output to stderr, exit with code
|
|
259
251
|
* 2. {output, exit_code} — text-mode result: print output directly
|
|
260
252
|
* 3. Plain object (no output field) — structured JSON: pretty-print as JSON
|
|
@@ -350,8 +342,6 @@ async function formatBridgeResult(result, commandName = '') {
|
|
|
350
342
|
const { command, commandArgs } = parseCommand(cleanedArgs);
|
|
351
343
|
|
|
352
344
|
const debug = process.env.CIPHER_DEBUG === '1';
|
|
353
|
-
|
|
354
|
-
|
|
355
345
|
// ---------------------------------------------------------------------------
|
|
356
346
|
// Autonomous mode dispatch — bypasses normal command routing entirely
|
|
357
347
|
// ---------------------------------------------------------------------------
|
|
@@ -414,7 +404,7 @@ const { getCommandMode } = await import('../lib/commands.js');
|
|
|
414
404
|
const mode = getCommandMode(command);
|
|
415
405
|
|
|
416
406
|
if (mode === 'native') {
|
|
417
|
-
// ── Native dispatch —
|
|
407
|
+
// ── Native dispatch — handler functions ────────────────────────────────
|
|
418
408
|
if (debug) {
|
|
419
409
|
process.stderr.write(`[bridge:node] Dispatching command=${command} mode=native\n`);
|
|
420
410
|
}
|
|
@@ -435,7 +425,7 @@ if (mode === 'native') {
|
|
|
435
425
|
}
|
|
436
426
|
|
|
437
427
|
try {
|
|
438
|
-
// ── API server command: long-running HTTP server
|
|
428
|
+
// ── API server command: long-running HTTP server ──────────────────────
|
|
439
429
|
if (command === 'api') {
|
|
440
430
|
const { createAPIServer, APIConfig } = await import('../lib/api/server.js');
|
|
441
431
|
const apiPort = commandArgs.find((a, i) => commandArgs[i - 1] === '--port') || '8443';
|
package/lib/api/billing.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
2
|
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
3
|
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
|
|
4
7
|
/**
|
|
5
8
|
* CIPHER Billing Engine — usage-based metering middleware.
|
|
6
9
|
*
|
package/lib/api/marketplace.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
2
|
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
3
|
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
|
|
4
7
|
/**
|
|
5
8
|
* CIPHER Skill Marketplace — publish, discover, and install CIPHER skills.
|
|
6
9
|
*
|
|
@@ -172,20 +172,20 @@ export class SkillQualityAnalyzer {
|
|
|
172
172
|
scores.section_coverage = 0;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
// Check agent.
|
|
176
|
-
const
|
|
177
|
-
if (existsSync(
|
|
178
|
-
const agentContent = readFileSync(
|
|
175
|
+
// Check agent.js
|
|
176
|
+
const agentJs = join(fullPath, 'scripts', 'agent.js');
|
|
177
|
+
if (existsSync(agentJs)) {
|
|
178
|
+
const agentContent = readFileSync(agentJs, 'utf-8');
|
|
179
179
|
const agentLines = agentContent.trim().split('\n');
|
|
180
180
|
if (agentLines.length < SkillQualityAnalyzer.MIN_AGENT_PY_LINES) {
|
|
181
|
-
issues.push(`agent.
|
|
181
|
+
issues.push(`agent.js too short (${agentLines.length} lines)`);
|
|
182
182
|
}
|
|
183
183
|
scores.agent_quality = Math.min(agentLines.length / 50, 1.0);
|
|
184
|
-
if (!agentContent.includes('
|
|
185
|
-
if (!agentContent.includes('json')) issues.push('agent.
|
|
186
|
-
if (!agentContent.includes('
|
|
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
187
|
} else {
|
|
188
|
-
issues.push('scripts/agent.
|
|
188
|
+
issues.push('scripts/agent.js missing');
|
|
189
189
|
scores.agent_quality = 0;
|
|
190
190
|
}
|
|
191
191
|
|
|
@@ -306,8 +306,8 @@ ${description}
|
|
|
306
306
|
## Verification
|
|
307
307
|
|
|
308
308
|
\`\`\`bash
|
|
309
|
-
|
|
310
|
-
|
|
309
|
+
node scripts/agent.js analyze --target example
|
|
310
|
+
node scripts/agent.js report --format json
|
|
311
311
|
\`\`\`
|
|
312
312
|
|
|
313
313
|
## References
|
|
@@ -373,7 +373,7 @@ function _generateApiReference(domain, techniqueName) {
|
|
|
373
373
|
|
|
374
374
|
| Command | Description |
|
|
375
375
|
|---|---|
|
|
376
|
-
| \`
|
|
376
|
+
| \`node agent.js analyze --target <t>\` | Analyze a target for ${techniqueName} indicators |
|
|
377
377
|
|
|
378
378
|
## Options
|
|
379
379
|
|
|
@@ -406,7 +406,7 @@ export class AutonomousResearcher {
|
|
|
406
406
|
}
|
|
407
407
|
|
|
408
408
|
/**
|
|
409
|
-
* Create a complete hypothesis with SKILL.md, agent.
|
|
409
|
+
* Create a complete hypothesis with SKILL.md, agent.js, and api-reference.md.
|
|
410
410
|
* @param {string} domain
|
|
411
411
|
* @param {string} techniqueName
|
|
412
412
|
* @returns {ResearchHypothesis}
|
|
@@ -507,7 +507,7 @@ export class AutonomousResearcher {
|
|
|
507
507
|
mkdirSync(refsDir, { recursive: true });
|
|
508
508
|
|
|
509
509
|
writeFileSync(join(base, 'SKILL.md'), hypothesis.skillContent, 'utf-8');
|
|
510
|
-
const agentPath = join(scriptsDir, 'agent.
|
|
510
|
+
const agentPath = join(scriptsDir, 'agent.js');
|
|
511
511
|
writeFileSync(agentPath, hypothesis.agentScript, 'utf-8');
|
|
512
512
|
try { chmodSync(agentPath, 0o755); } catch { /* ok */ }
|
|
513
513
|
writeFileSync(join(refsDir, 'api-reference.md'), hypothesis.referenceContent, 'utf-8');
|
package/lib/commands.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Maps all 29 CLI commands to one of three dispatch modes:
|
|
9
9
|
* - native: Dispatched directly through Node.js handler functions (no Python)
|
|
10
|
-
* - passthrough:
|
|
10
|
+
* - passthrough: Legacy mode — no commands use this as of v2.0
|
|
11
11
|
* (full terminal access for Rich panels, Textual TUIs, long-running services)
|
|
12
12
|
* - bridge: (Legacy) Dispatched via JSON-RPC through python-bridge.js — no commands
|
|
13
13
|
* use this mode as of M007/S03, retained for backward compatibility
|
package/lib/config.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Mirrors the Python gateway/config.py load_config() format and precedence:
|
|
9
9
|
* 1. Environment variables (LLM_BACKEND, ANTHROPIC_API_KEY) — highest
|
|
10
|
-
* 2. Project-root config.yaml (where
|
|
10
|
+
* 2. Project-root config.yaml (where cli/ directory lives)
|
|
11
11
|
* 3. ~/.config/cipher/config.yaml — lowest
|
|
12
12
|
*
|
|
13
13
|
* Produces YAML that Python's yaml.safe_load() can parse identically.
|
|
@@ -25,8 +25,8 @@ import { parse, stringify } from 'yaml';
|
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Walk up from `startDir` looking for
|
|
29
|
-
* _find_project_root(). Returns the directory containing
|
|
28
|
+
* Walk up from `startDir` looking for cli/ directory — same logic as Python's
|
|
29
|
+
* _find_project_root(). Returns the directory containing cli/ directory, or null.
|
|
30
30
|
*
|
|
31
31
|
* @param {string} [startDir=process.cwd()]
|
|
32
32
|
* @returns {string | null}
|
|
@@ -34,7 +34,7 @@ import { parse, stringify } from 'yaml';
|
|
|
34
34
|
function findProjectRoot(startDir = process.cwd()) {
|
|
35
35
|
let current = resolve(startDir);
|
|
36
36
|
for (let i = 0; i < 20; i++) {
|
|
37
|
-
if (existsSync(join(current, '
|
|
37
|
+
if (existsSync(join(current, 'cli'))) {
|
|
38
38
|
return current;
|
|
39
39
|
}
|
|
40
40
|
const parent = dirname(current);
|
package/lib/gateway/commands.js
CHANGED
|
@@ -35,6 +35,39 @@ function defaultMemoryDir() {
|
|
|
35
35
|
return join(homedir(), '.cipher', 'memory', 'default');
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Extract a value from CLI args array or object.
|
|
40
|
+
* Handles both `cipher search lateral movement` (array) and `{query: "..."}` (object).
|
|
41
|
+
* @param {string[]|object} args
|
|
42
|
+
* @param {string} key - object key (e.g. 'query')
|
|
43
|
+
* @param {string} [flagName] - CLI flag (e.g. '--query'). If null, uses positional args.
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
function getArg(args, key, flagName) {
|
|
47
|
+
if (!Array.isArray(args)) return args[key] || '';
|
|
48
|
+
if (flagName) {
|
|
49
|
+
const idx = args.indexOf(flagName);
|
|
50
|
+
if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
|
|
51
|
+
}
|
|
52
|
+
// Positional: join all non-flag args
|
|
53
|
+
return args.filter(a => !a.startsWith('-')).join(' ');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extract a flag value from CLI args.
|
|
58
|
+
* @param {string[]|object} args
|
|
59
|
+
* @param {string} key - object key
|
|
60
|
+
* @param {string} flag - CLI flag (e.g. '--limit')
|
|
61
|
+
* @param {*} defaultVal
|
|
62
|
+
* @returns {*}
|
|
63
|
+
*/
|
|
64
|
+
function getFlagArg(args, key, flag, defaultVal) {
|
|
65
|
+
if (!Array.isArray(args)) return args[key] ?? defaultVal;
|
|
66
|
+
const idx = args.indexOf(flag);
|
|
67
|
+
if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
|
|
68
|
+
return defaultVal;
|
|
69
|
+
}
|
|
70
|
+
|
|
38
71
|
/**
|
|
39
72
|
* Resolve the repository root by walking up from this file.
|
|
40
73
|
* @returns {string}
|
|
@@ -93,13 +126,13 @@ export async function handleSearch(args = {}) {
|
|
|
93
126
|
try {
|
|
94
127
|
const retriever = new AdaptiveRetriever(memory);
|
|
95
128
|
const results = retriever.retrieve(
|
|
96
|
-
args
|
|
97
|
-
args
|
|
98
|
-
args
|
|
129
|
+
getArg(args, "query"),
|
|
130
|
+
getFlagArg(args, "engagement", "--engagement", ""),
|
|
131
|
+
getFlagArg(args, "limit", "--limit", 10),
|
|
99
132
|
);
|
|
100
133
|
debug(`search: ${results.length} results for "${args.query}"`);
|
|
101
134
|
return {
|
|
102
|
-
query: args
|
|
135
|
+
query: getArg(args, "query"),
|
|
103
136
|
count: results.length,
|
|
104
137
|
results: results.map(r => ({
|
|
105
138
|
content: r.content,
|
|
@@ -126,18 +159,20 @@ export async function handleSearch(args = {}) {
|
|
|
126
159
|
* @returns {Promise<object>}
|
|
127
160
|
*/
|
|
128
161
|
export async function handleStore(args = {}) {
|
|
162
|
+
const content = getArg(args, 'content', '--content');
|
|
163
|
+
if (!content) {
|
|
164
|
+
return { error: true, message: 'Usage: cipher store --content "finding text" [--type finding|ioc|ttp|note]' };
|
|
165
|
+
}
|
|
129
166
|
const { CipherMemory, MemoryEntry, MemoryType } = await import('../memory/index.js');
|
|
130
167
|
const memory = new CipherMemory(defaultMemoryDir());
|
|
131
168
|
try {
|
|
132
|
-
const typeStr = args
|
|
169
|
+
const typeStr = getFlagArg(args, 'type', '--type', 'note');
|
|
133
170
|
const entry = new MemoryEntry({
|
|
134
|
-
content
|
|
171
|
+
content,
|
|
135
172
|
memoryType: Object.values(MemoryType).includes(typeStr) ? typeStr : MemoryType.NOTE,
|
|
136
|
-
severity: args
|
|
137
|
-
engagementId: args
|
|
138
|
-
tags:
|
|
139
|
-
? args.tags
|
|
140
|
-
: (typeof args.tags === 'string' && args.tags ? args.tags.split(',') : []),
|
|
173
|
+
severity: getFlagArg(args, 'severity', '--severity', ''),
|
|
174
|
+
engagementId: getFlagArg(args, 'engagement', '--engagement', ''),
|
|
175
|
+
tags: [],
|
|
141
176
|
});
|
|
142
177
|
const entryId = memory.store(entry);
|
|
143
178
|
debug(`store: id=${entryId} type=${typeStr}`);
|
|
@@ -173,7 +208,7 @@ export async function handleStats(args = {}) {
|
|
|
173
208
|
} catch { return false; }
|
|
174
209
|
}).length;
|
|
175
210
|
// Count script files recursively
|
|
176
|
-
scriptCount = countFiles(sDir, '*.
|
|
211
|
+
scriptCount = countFiles(sDir, '*.js', 'scripts');
|
|
177
212
|
} catch {
|
|
178
213
|
// Skills dir unreadable
|
|
179
214
|
}
|
|
@@ -199,13 +234,14 @@ export async function handleStats(args = {}) {
|
|
|
199
234
|
* @returns {Promise<object>}
|
|
200
235
|
*/
|
|
201
236
|
export async function handleScore(args = {}) {
|
|
237
|
+
const query = getFlagArg(args, 'query', '--query', '');
|
|
238
|
+
const response = getFlagArg(args, 'response', '--response', '');
|
|
239
|
+
if (!query || !response) {
|
|
240
|
+
return { error: true, message: 'Usage: cipher score --query "question" --response "answer"' };
|
|
241
|
+
}
|
|
202
242
|
const { ResponseScorer } = await import('../memory/index.js');
|
|
203
243
|
const scorer = new ResponseScorer();
|
|
204
|
-
const scored = scorer.score(
|
|
205
|
-
args.query || '',
|
|
206
|
-
args.response || '',
|
|
207
|
-
args.mode || '',
|
|
208
|
-
);
|
|
244
|
+
const scored = scorer.score(query, response, getFlagArg(args, 'mode', '--mode', ''));
|
|
209
245
|
debug(`score: ${scored.score}`);
|
|
210
246
|
return {
|
|
211
247
|
score: scored.score,
|
|
@@ -249,9 +285,12 @@ export async function handleMemoryExport(args = {}) {
|
|
|
249
285
|
* @returns {Promise<object>}
|
|
250
286
|
*/
|
|
251
287
|
export async function handleMemoryImport(args = {}) {
|
|
252
|
-
const file = args
|
|
253
|
-
if (!file
|
|
254
|
-
return { error: true, message:
|
|
288
|
+
const file = getArg(args, 'file');
|
|
289
|
+
if (!file) {
|
|
290
|
+
return { error: true, message: 'Usage: cipher memory-import <file.json>\n\nImport findings, IOCs, and notes from a previously exported JSON file.' };
|
|
291
|
+
}
|
|
292
|
+
if (!existsSync(file)) {
|
|
293
|
+
return { error: true, message: `File not found: ${file}` };
|
|
255
294
|
}
|
|
256
295
|
const { CipherMemory, MemoryEntry } = await import('../memory/index.js');
|
|
257
296
|
const memory = new CipherMemory(defaultMemoryDir());
|
|
@@ -283,9 +322,11 @@ export async function handleIngest(args = {}) {
|
|
|
283
322
|
const { CipherMemory } = await import('../memory/index.js');
|
|
284
323
|
const memory = new CipherMemory(defaultMemoryDir());
|
|
285
324
|
try {
|
|
325
|
+
// Rebuild FTS5 index to fix stale/corrupt indexes
|
|
326
|
+
memory.symbolic.db.exec("INSERT INTO entries_fts(entries_fts) VALUES('rebuild')");
|
|
286
327
|
const stats = memory.consolidate();
|
|
287
|
-
debug('ingest: consolidation complete');
|
|
288
|
-
return { ingested: true, ...stats };
|
|
328
|
+
debug('ingest: FTS rebuild + consolidation complete');
|
|
329
|
+
return { ingested: true, fts_rebuilt: true, ...stats };
|
|
289
330
|
} finally {
|
|
290
331
|
memory.close();
|
|
291
332
|
}
|
|
@@ -346,10 +387,13 @@ export async function handleStatus(args = {}) {
|
|
|
346
387
|
* @returns {Promise<object>}
|
|
347
388
|
*/
|
|
348
389
|
export async function handleDiff(args = {}) {
|
|
390
|
+
let diffText = getArg(args, 'file');
|
|
391
|
+
if (!diffText) {
|
|
392
|
+
return { error: true, message: 'Usage: cipher diff <file.patch>\n cat changes.diff | cipher diff -' };
|
|
393
|
+
}
|
|
349
394
|
const { SecurityDiffAnalyzer } = await import('../pipeline/index.js');
|
|
350
395
|
const analyzer = new SecurityDiffAnalyzer();
|
|
351
396
|
|
|
352
|
-
let diffText = args.file || '';
|
|
353
397
|
// If it looks like a file path (no newlines, exists on disk), read it
|
|
354
398
|
if (diffText && !diffText.includes('\n') && existsSync(diffText)) {
|
|
355
399
|
diffText = readFileSync(diffText, 'utf-8');
|
|
@@ -393,11 +437,14 @@ export async function handleWorkflow(args = {}) {
|
|
|
393
437
|
* @returns {Promise<object>}
|
|
394
438
|
*/
|
|
395
439
|
export async function handleSarif(args = {}) {
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
440
|
+
const input = getArg(args, 'input');
|
|
441
|
+
if (!input) {
|
|
442
|
+
return { error: true, message: 'Usage: cipher sarif <scan-results.json> [--output results.sarif]\n\nConvert scan findings to SARIF v2.1.0 for GitHub Advanced Security / CI pipelines.' };
|
|
443
|
+
}
|
|
444
|
+
if (!existsSync(input)) {
|
|
445
|
+
return { error: true, message: `File not found: ${input}` };
|
|
400
446
|
}
|
|
447
|
+
const { SarifReport } = await import('../pipeline/index.js');
|
|
401
448
|
|
|
402
449
|
const data = JSON.parse(readFileSync(input, 'utf-8'));
|
|
403
450
|
const report = new SarifReport();
|
|
@@ -423,10 +470,13 @@ export async function handleSarif(args = {}) {
|
|
|
423
470
|
* @returns {Promise<object>}
|
|
424
471
|
*/
|
|
425
472
|
export async function handleOsint(args = {}) {
|
|
473
|
+
const target = getArg(args, 'target');
|
|
474
|
+
if (!target) {
|
|
475
|
+
return { error: true, message: 'Usage: cipher osint <domain|ip> [--type domain|ip]' };
|
|
476
|
+
}
|
|
426
477
|
const { OSINTPipeline } = await import('../pipeline/index.js');
|
|
427
478
|
const pipeline = new OSINTPipeline();
|
|
428
|
-
const
|
|
429
|
-
const type = args.type || 'domain';
|
|
479
|
+
const type = getFlagArg(args, 'type', '--type', 'domain');
|
|
430
480
|
|
|
431
481
|
const results = type === 'ip'
|
|
432
482
|
? pipeline.investigateIp(target)
|
|
@@ -493,12 +543,12 @@ export async function handleDomains(args = {}) {
|
|
|
493
543
|
*/
|
|
494
544
|
export async function handleSkills(args = {}) {
|
|
495
545
|
const sDir = skillsDir();
|
|
496
|
-
const query = (args
|
|
497
|
-
const limit = args
|
|
546
|
+
const query = getArg(args, 'query').toLowerCase();
|
|
547
|
+
const limit = parseInt(getFlagArg(args, 'limit', '--limit', 10), 10);
|
|
498
548
|
const results = [];
|
|
499
549
|
|
|
500
550
|
if (!existsSync(sDir)) {
|
|
501
|
-
return { query
|
|
551
|
+
return { query, count: 0, results: [] };
|
|
502
552
|
}
|
|
503
553
|
|
|
504
554
|
// Walk skills dir looking for SKILL.md files
|
|
@@ -526,7 +576,7 @@ export async function handleSkills(args = {}) {
|
|
|
526
576
|
|
|
527
577
|
walk(sDir, '');
|
|
528
578
|
debug(`skills: "${query}" → ${results.length} results`);
|
|
529
|
-
return { query
|
|
579
|
+
return { query, count: results.length, results };
|
|
530
580
|
}
|
|
531
581
|
|
|
532
582
|
// ---------------------------------------------------------------------------
|
|
@@ -684,40 +734,128 @@ export async function handleQuery(args = {}) {
|
|
|
684
734
|
* @returns {Promise<object>}
|
|
685
735
|
*/
|
|
686
736
|
export async function handleLeaderboard(args = {}) {
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
737
|
+
try {
|
|
738
|
+
const { SkillLeaderboard } = await import('../autonomous/leaderboard.js');
|
|
739
|
+
const lb = new SkillLeaderboard();
|
|
740
|
+
const action = Array.isArray(args) ? args[0] : args.action;
|
|
741
|
+
|
|
742
|
+
if (action === 'dashboard') {
|
|
743
|
+
const dashboard = lb.getDashboard();
|
|
744
|
+
lb.close();
|
|
745
|
+
return dashboard;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const top = lb.getTopSkills(20);
|
|
749
|
+
lb.close();
|
|
750
|
+
return {
|
|
751
|
+
top_skills: top.map((e, i) => ({
|
|
752
|
+
rank: i + 1,
|
|
753
|
+
skill: e.skillPath,
|
|
754
|
+
score: e.avgScore,
|
|
755
|
+
invocations: e.invocationCount,
|
|
756
|
+
})),
|
|
757
|
+
total: top.length,
|
|
758
|
+
};
|
|
759
|
+
} catch (err) {
|
|
760
|
+
return { error: true, message: `Leaderboard error: ${err.message}` };
|
|
761
|
+
}
|
|
691
762
|
}
|
|
692
763
|
|
|
693
764
|
/**
|
|
694
765
|
* @returns {Promise<object>}
|
|
695
766
|
*/
|
|
696
767
|
export async function handleFeedback(args = {}) {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
768
|
+
try {
|
|
769
|
+
const { SkillQualityAnalyzer } = await import('../autonomous/feedback-loop.js');
|
|
770
|
+
const analyzer = new SkillQualityAnalyzer(skillsDir());
|
|
771
|
+
const dryRun = Array.isArray(args) ? args.includes('--dry-run') : args.dryRun;
|
|
772
|
+
const maxSkills = Array.isArray(args)
|
|
773
|
+
? parseInt(args.find((a, i) => args[i - 1] === '--max') || '10', 10)
|
|
774
|
+
: args.max || 10;
|
|
775
|
+
|
|
776
|
+
// Analyze skills quality
|
|
777
|
+
const results = [];
|
|
778
|
+
const { readdirSync } = await import('node:fs');
|
|
779
|
+
const sDir = skillsDir();
|
|
780
|
+
for (const domain of readdirSync(sDir).slice(0, maxSkills)) {
|
|
781
|
+
try {
|
|
782
|
+
const analysis = analyzer.analyzeSkill(domain);
|
|
783
|
+
if (analysis.needsImprovement) {
|
|
784
|
+
results.push({ skill: domain, quality: analysis.overallQuality, issues: analysis.issues.length });
|
|
785
|
+
}
|
|
786
|
+
} catch { /* skip */ }
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
analyzed: maxSkills,
|
|
791
|
+
needs_improvement: results.length,
|
|
792
|
+
dry_run: !!dryRun,
|
|
793
|
+
candidates: results,
|
|
794
|
+
};
|
|
795
|
+
} catch (err) {
|
|
796
|
+
return { error: true, message: `Feedback error: ${err.message}` };
|
|
797
|
+
}
|
|
701
798
|
}
|
|
702
799
|
|
|
703
800
|
/**
|
|
704
801
|
* @returns {Promise<object>}
|
|
705
802
|
*/
|
|
706
803
|
export async function handleMarketplace(args = {}) {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
804
|
+
try {
|
|
805
|
+
const { SkillMarketplace } = await import('../api/marketplace.js');
|
|
806
|
+
const mp = new SkillMarketplace();
|
|
807
|
+
const action = Array.isArray(args) ? args[0] : args.action;
|
|
808
|
+
|
|
809
|
+
if (action === 'search') {
|
|
810
|
+
const query = Array.isArray(args) ? args.find((a, i) => args[i - 1] === '--query') || args[1] || '' : args.query || '';
|
|
811
|
+
const results = mp.search(query);
|
|
812
|
+
mp.close();
|
|
813
|
+
return { query, results: results.map(p => ({ name: p.name, domain: p.domain, rating: p.rating, downloads: p.downloads })), total: results.length };
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const index = mp.getIndex();
|
|
817
|
+
mp.close();
|
|
818
|
+
return index;
|
|
819
|
+
} catch (err) {
|
|
820
|
+
return { error: true, message: `Marketplace error: ${err.message}` };
|
|
821
|
+
}
|
|
711
822
|
}
|
|
712
823
|
|
|
713
824
|
/**
|
|
714
825
|
* @returns {Promise<object>}
|
|
715
826
|
*/
|
|
716
827
|
export async function handleCompliance(args = {}) {
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
828
|
+
try {
|
|
829
|
+
const { ComplianceEngine, ComplianceFramework } = await import('../api/compliance.js');
|
|
830
|
+
const engine = new ComplianceEngine();
|
|
831
|
+
const framework = Array.isArray(args) ? args[0] : args.framework;
|
|
832
|
+
|
|
833
|
+
if (!framework) {
|
|
834
|
+
return {
|
|
835
|
+
available_frameworks: Object.keys(ComplianceFramework),
|
|
836
|
+
total: Object.keys(ComplianceFramework).length,
|
|
837
|
+
usage: 'cipher compliance <FRAMEWORK> [--format json|markdown|csv]',
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const fw = framework.toUpperCase();
|
|
842
|
+
if (!ComplianceFramework[fw]) {
|
|
843
|
+
return { error: true, message: `Unknown framework: ${framework}. Available: ${Object.keys(ComplianceFramework).join(', ')}` };
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const format = Array.isArray(args) ? (args.find((a, i) => args[i - 1] === '--format') || 'json') : (args.format || 'json');
|
|
847
|
+
const report = engine.assessFromFindings([], fw);
|
|
848
|
+
|
|
849
|
+
if (format === 'markdown') {
|
|
850
|
+
return { output: report.toMarkdown() };
|
|
851
|
+
} else if (format === 'csv') {
|
|
852
|
+
return { output: engine.exportCsv(report) };
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
return report.toDict();
|
|
856
|
+
} catch (err) {
|
|
857
|
+
return { error: true, message: `Compliance error: ${err.message}` };
|
|
858
|
+
}
|
|
721
859
|
}
|
|
722
860
|
|
|
723
861
|
// ---------------------------------------------------------------------------
|
|
@@ -795,12 +933,15 @@ export async function handleScan(args = {}) {
|
|
|
795
933
|
// ---------------------------------------------------------------------------
|
|
796
934
|
|
|
797
935
|
export async function handleDashboard() {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
936
|
+
const brand = await import('../brand.js');
|
|
937
|
+
brand.header('Dashboard');
|
|
938
|
+
brand.divider();
|
|
939
|
+
brand.info('Dashboard TUI is not yet available.');
|
|
940
|
+
brand.info('Use: cipher status \u2014 system status');
|
|
941
|
+
brand.info('Use: cipher doctor \u2014 health check');
|
|
942
|
+
brand.info('Use: cipher api \u2014 REST API server');
|
|
943
|
+
brand.divider();
|
|
944
|
+
return {};
|
|
804
945
|
}
|
|
805
946
|
|
|
806
947
|
// ---------------------------------------------------------------------------
|
|
@@ -808,12 +949,13 @@ export async function handleDashboard() {
|
|
|
808
949
|
// ---------------------------------------------------------------------------
|
|
809
950
|
|
|
810
951
|
export async function handleWeb(args = {}) {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
952
|
+
const brand = await import('../brand.js');
|
|
953
|
+
brand.header('Web Interface');
|
|
954
|
+
brand.divider();
|
|
955
|
+
brand.info('The web interface has been consolidated into the API server.');
|
|
956
|
+
brand.info('Use: cipher api --no-auth --port 8443');
|
|
957
|
+
brand.divider();
|
|
958
|
+
return {};
|
|
817
959
|
}
|
|
818
960
|
|
|
819
961
|
// ---------------------------------------------------------------------------
|
package/lib/gateway/prompt.js
CHANGED
|
@@ -25,7 +25,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
25
25
|
const __dirname = dirname(__filename);
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Walk up from startDir looking for
|
|
28
|
+
* Walk up from startDir looking for cli/ directory or package.json.
|
|
29
29
|
* Returns the directory or null.
|
|
30
30
|
*
|
|
31
31
|
* @param {string} startDir
|
|
@@ -35,15 +35,15 @@ function findRepoRoot(startDir) {
|
|
|
35
35
|
let current = resolve(startDir);
|
|
36
36
|
for (let i = 0; i < 20; i++) {
|
|
37
37
|
if (
|
|
38
|
-
existsSync(join(current, '
|
|
38
|
+
existsSync(join(current, 'cli')) ||
|
|
39
39
|
existsSync(join(current, 'package.json'))
|
|
40
40
|
) {
|
|
41
41
|
// Check for knowledge/ dir to distinguish from cli/package.json
|
|
42
42
|
if (existsSync(join(current, 'knowledge')) || existsSync(join(current, 'skills'))) {
|
|
43
43
|
return current;
|
|
44
44
|
}
|
|
45
|
-
// If it has
|
|
46
|
-
if (existsSync(join(current, '
|
|
45
|
+
// If it has cli/, it is the repo root
|
|
46
|
+
if (existsSync(join(current, 'cli'))) {
|
|
47
47
|
return current;
|
|
48
48
|
}
|
|
49
49
|
}
|
package/lib/memory/compressor.js
CHANGED
|
@@ -305,7 +305,7 @@ class SemanticCompressor {
|
|
|
305
305
|
* @private
|
|
306
306
|
*/
|
|
307
307
|
async _llmCompress(window, entities) {
|
|
308
|
-
//
|
|
308
|
+
// LLM-based extraction not implemented — returns heuristic results
|
|
309
309
|
// For now, fall back to heuristic compression
|
|
310
310
|
return this._heuristicCompress(window, entities);
|
|
311
311
|
}
|
package/lib/memory/engine.js
CHANGED
|
@@ -481,22 +481,26 @@ class CipherMemory {
|
|
|
481
481
|
search(query, filters = {}, limit = DEFAULT_TOP_K) {
|
|
482
482
|
const resultLists = [];
|
|
483
483
|
|
|
484
|
-
// Layer 1: Lexical search (BM25)
|
|
484
|
+
// Layer 1: Lexical search (BM25) — always run when query is provided
|
|
485
485
|
const lexicalResults = this.symbolic.searchLexical(query, limit * 2);
|
|
486
486
|
if (lexicalResults.length > 0) {
|
|
487
487
|
resultLists.push(lexicalResults.map((r) => r.entry_id));
|
|
488
488
|
}
|
|
489
489
|
|
|
490
|
-
// Layer 2: Symbolic search (structured)
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
490
|
+
// Layer 2: Symbolic search (structured) — only when filters are provided
|
|
491
|
+
// Without filters, symbolic returns ALL entries which drowns out lexical ranking
|
|
492
|
+
const hasFilters = !!(filters.engagementId || filters.memoryType || filters.severity || filters.mitreTechnique);
|
|
493
|
+
if (hasFilters) {
|
|
494
|
+
const symbolicResults = this.symbolic.searchSymbolic({
|
|
495
|
+
engagementId: filters.engagementId ?? '',
|
|
496
|
+
memoryType: filters.memoryType ?? '',
|
|
497
|
+
severity: filters.severity ?? '',
|
|
498
|
+
mitreTechnique: filters.mitreTechnique ?? '',
|
|
499
|
+
limit: limit * 2,
|
|
500
|
+
});
|
|
501
|
+
if (symbolicResults.length > 0) {
|
|
502
|
+
resultLists.push(symbolicResults.map((r) => r.entry_id));
|
|
503
|
+
}
|
|
500
504
|
}
|
|
501
505
|
|
|
502
506
|
// Fuse with RRF
|
|
@@ -678,9 +678,9 @@ jobs:
|
|
|
678
678
|
steps:
|
|
679
679
|
- uses: actions/checkout@v4
|
|
680
680
|
with: { fetch-depth: 0 }
|
|
681
|
-
- uses: actions/setup-
|
|
682
|
-
with: {
|
|
683
|
-
- run:
|
|
681
|
+
- uses: actions/setup-node@v5
|
|
682
|
+
with: { node-version: "22" }
|
|
683
|
+
- run: npm install -g cipher-security
|
|
684
684
|
- name: Run PR Security Review
|
|
685
685
|
env:
|
|
686
686
|
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
@@ -722,9 +722,9 @@ outputs:
|
|
|
722
722
|
runs:
|
|
723
723
|
using: "composite"
|
|
724
724
|
steps:
|
|
725
|
-
- uses: actions/setup-
|
|
726
|
-
with: {
|
|
727
|
-
- { shell: bash, run: "
|
|
725
|
+
- uses: actions/setup-node@v5
|
|
726
|
+
with: { node-version: "22" }
|
|
727
|
+
- { shell: bash, run: "npm install -g cipher-security" }
|
|
728
728
|
- name: Run Security Review
|
|
729
729
|
shell: bash
|
|
730
730
|
env:
|
package/lib/setup-wizard.js
CHANGED
|
@@ -251,16 +251,7 @@ export async function runSetupWizard() {
|
|
|
251
251
|
// ── Write config ──────────────────────────────────────────────────
|
|
252
252
|
const configPath = writeConfig(config);
|
|
253
253
|
|
|
254
|
-
|
|
255
|
-
const { projectConfig } = getConfigPaths();
|
|
256
|
-
const projectRoot = join(projectConfig, '..');
|
|
257
|
-
if (existsSync(join(projectRoot, 'pyproject.toml')) && projectConfig !== configPath) {
|
|
258
|
-
try {
|
|
259
|
-
writeConfig(config, projectConfig);
|
|
260
|
-
} catch {
|
|
261
|
-
// Non-fatal — user config was already written
|
|
262
|
-
}
|
|
263
|
-
}
|
|
254
|
+
|
|
264
255
|
|
|
265
256
|
// ── Completion summary ────────────────────────────────────────────
|
|
266
257
|
const summaryLines = [
|