cipher-security 2.0.4 → 2.0.6

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 CHANGED
@@ -72,7 +72,7 @@ ${B}Commands:${R}
72
72
 
73
73
  ${GRN}api${R} Start REST API server
74
74
  ${GRN}mcp${R} Start MCP server (stdio)
75
- ${GRN}bot${R} Start Signal bot
75
+ ${GRN}bot${R} Start Signal bot (background)
76
76
 
77
77
  ${GRN}workflow${R} Generate CI/CD security workflow
78
78
  ${GRN}memory-export${R} Export memory to JSON
@@ -82,8 +82,6 @@ ${B}Commands:${R}
82
82
  ${GRN}plugin${R} Manage plugins
83
83
  ${GRN}setup-signal${R} Configure Signal integration
84
84
  ${GRN}score${R} Score response quality
85
- ${GRN}dashboard${R} System dashboard
86
- ${GRN}web${R} Web interface (use cipher api)
87
85
 
88
86
  ${B}Options:${R}
89
87
  --version, -V Print version and exit
@@ -194,7 +192,7 @@ if (autonomousMode === '__pending__') {
194
192
  const knownCommands = new Set([
195
193
  // Gateway
196
194
  'query', 'ingest', 'status', 'doctor', 'setup-signal',
197
- 'dashboard', 'web', 'version', 'plugin',
195
+ 'version', 'plugin',
198
196
  // Pipeline
199
197
  'scan', 'search', 'store', 'diff', 'workflow', 'stats', 'domains',
200
198
  'skills', 'score', 'marketplace', 'compliance', 'leaderboard',
@@ -456,11 +454,74 @@ if (mode === 'native') {
456
454
  mcp.startStdio();
457
455
  // ── Bot command: Signal bot ──────────────────────────────────────────
458
456
  } else if (command === 'bot') {
459
- const { banner, header, info, warn, success, divider } = await import('../lib/brand.js');
457
+ const { banner, header, info, warn, success, divider, error: brandError } = await import('../lib/brand.js');
460
458
  const { loadConfig, configExists } = await import('../lib/config.js');
459
+ const { existsSync, readFileSync, unlinkSync } = await import('node:fs');
460
+ const { join } = await import('node:path');
461
+ const { homedir } = await import('node:os');
462
+
463
+ const pidFile = join(homedir(), '.cipher', 'bot.pid');
464
+ const logFile = join(homedir(), '.cipher', 'bot.log');
465
+ const subAction = commandArgs[0];
466
+
467
+ // ── cipher bot stop ────────────────────────────────────────────
468
+ if (subAction === 'stop') {
469
+ if (!existsSync(pidFile)) {
470
+ header('Signal Bot');
471
+ warn('Bot is not running (no PID file).');
472
+ process.exit(0);
473
+ }
474
+ const pid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);
475
+ try {
476
+ process.kill(pid, 'SIGTERM');
477
+ unlinkSync(pidFile);
478
+ header('Signal Bot');
479
+ success(`Bot stopped (PID ${pid}).`);
480
+ } catch (err) {
481
+ // Process already gone
482
+ unlinkSync(pidFile);
483
+ header('Signal Bot');
484
+ warn(`Bot process ${pid} not found — cleaned up stale PID file.`);
485
+ }
486
+ process.exit(0);
487
+ }
461
488
 
462
- banner(pkg.version);
463
- header('Signal Bot');
489
+ // ── cipher bot status ──────────────────────────────────────────
490
+ if (subAction === 'status') {
491
+ header('Signal Bot');
492
+ if (!existsSync(pidFile)) {
493
+ warn('Bot is not running.');
494
+ } else {
495
+ const pid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);
496
+ try {
497
+ process.kill(pid, 0); // signal 0 = check if alive
498
+ success(`Bot is running (PID ${pid}).`);
499
+ info(`Log: ${logFile}`);
500
+ } catch {
501
+ warn(`Bot PID ${pid} is stale — not running.`);
502
+ unlinkSync(pidFile);
503
+ }
504
+ }
505
+ process.exit(0);
506
+ }
507
+
508
+ // ── cipher bot (start) ─────────────────────────────────────────
509
+ // Fork as foreground if --foreground, otherwise daemonize
510
+ const foreground = commandArgs.includes('--foreground') || commandArgs.includes('-f');
511
+
512
+ // Check if already running (skip in foreground mode — we ARE the child)
513
+ if (!foreground && existsSync(pidFile)) {
514
+ const existingPid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);
515
+ try {
516
+ process.kill(existingPid, 0);
517
+ header('Signal Bot');
518
+ warn(`Bot is already running (PID ${existingPid}).`);
519
+ info('Run: cipher bot stop');
520
+ process.exit(1);
521
+ } catch {
522
+ unlinkSync(pidFile); // stale PID file
523
+ }
524
+ }
464
525
 
465
526
  // Load signal config
466
527
  let signalCfg = {};
@@ -480,6 +541,7 @@ if (mode === 'native') {
480
541
  : signalCfg.whitelist || [];
481
542
 
482
543
  if (!svc || !phone) {
544
+ header('Signal Bot');
483
545
  divider();
484
546
  warn('Signal is not configured.');
485
547
  info('Run: cipher setup-signal');
@@ -487,19 +549,52 @@ if (mode === 'native') {
487
549
  process.exit(1);
488
550
  }
489
551
 
490
- divider();
491
- success(`Service: ${svc}`);
492
- success(`Phone: ${phone}`);
493
- if (whitelist.length) {
494
- success(`Whitelist: ${whitelist.join(', ')}`);
552
+ if (foreground) {
553
+ banner(pkg.version);
554
+ header('Signal Bot (foreground)');
555
+ divider();
556
+ success(`Service: ${svc}`);
557
+ success(`Phone: ${phone}`);
558
+ if (whitelist.length) {
559
+ success(`Whitelist: ${whitelist.join(', ')}`);
560
+ } else {
561
+ warn('Whitelist: none (all senders allowed)');
562
+ }
563
+ divider();
564
+ info('Running in foreground (Ctrl+C to stop)...');
565
+
566
+ const { runBot } = await import('../lib/bot/bot.js');
567
+ await runBot({ signalService: svc, phoneNumber: phone, whitelist, sessionTimeout: signalCfg.session_timeout || 3600 });
495
568
  } else {
496
- warn('Whitelist: none (all senders allowed)');
497
- }
498
- divider();
499
- info('Starting bot... (Ctrl+C to stop)');
569
+ // Daemonize: spawn detached child with --foreground flag
570
+ const { spawn } = await import('node:child_process');
571
+ const { openSync, writeFileSync: writePid, mkdirSync } = await import('node:fs');
572
+
573
+ mkdirSync(join(homedir(), '.cipher'), { recursive: true });
574
+ const logFd = openSync(logFile, 'a');
575
+
576
+ const child = spawn(process.execPath, [
577
+ ...process.argv.slice(1), '--foreground',
578
+ ], {
579
+ detached: true,
580
+ stdio: ['ignore', logFd, logFd],
581
+ env: { ...process.env },
582
+ });
583
+
584
+ writePid(pidFile, String(child.pid), 'utf-8');
585
+ child.unref();
500
586
 
501
- const { runBot } = await import('../lib/bot/bot.js');
502
- await runBot({ signalService: svc, phoneNumber: phone, whitelist, sessionTimeout: signalCfg.session_timeout || 3600 });
587
+ header('Signal Bot');
588
+ divider();
589
+ success(`Service: ${svc}`);
590
+ success(`Phone: ${phone}`);
591
+ divider();
592
+ success(`Bot started in background (PID ${child.pid}).`);
593
+ info(`Log: ${logFile}`);
594
+ info('Run: cipher bot stop — stop the bot');
595
+ info('Run: cipher bot status — check if running');
596
+ divider();
597
+ }
503
598
  // ── Query command: streaming via Gateway or non-streaming via handler ──
504
599
  } else if (command === 'query') {
505
600
  const queryText = commandArgs.filter(a => !a.startsWith('-')).join(' ');
package/lib/commands.js CHANGED
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * commands.js — Command routing table for the CIPHER CLI.
7
7
  *
8
- * Maps all 30 CLI commands to one of three dispatch modes:
8
+ * Maps all 28 CLI commands to one of three dispatch modes:
9
9
  * - native: Dispatched directly through Node.js handler functions (no Python)
10
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)
@@ -49,8 +49,6 @@ export const COMMAND_MODES = {
49
49
  // ── Passthrough commands (direct Python spawn, full terminal) ──────────
50
50
  // These were Python-dependent — now ported to Node.js.
51
51
  scan: { mode: 'native', description: 'Run a security scan' },
52
- dashboard: { mode: 'native', description: 'System dashboard' },
53
- web: { mode: 'native', description: 'Web interface (use `cipher api` instead)' },
54
52
  bot: { mode: 'native', description: 'Manage bot integrations (long-running service)' },
55
53
  mcp: { mode: 'native', description: 'MCP server tools (long-running service)' },
56
54
  api: { mode: 'native', description: 'API management (long-running server)' },
@@ -3,7 +3,7 @@
3
3
  // CIPHER is a trademark of defconxt.
4
4
 
5
5
  /**
6
- * commands.js — Node.js handler functions for all 30 CLI commands.
6
+ * commands.js — Node.js handler functions for all 28 CLI commands.
7
7
  *
8
8
  * Each handler accepts a plain args object and returns a plain JS object.
9
9
  * No Rich formatting, no Typer framework — just data. The rendering
@@ -261,14 +261,25 @@ export async function handleMemoryExport(args = {}) {
261
261
  const { CipherMemory } = await import('../memory/index.js');
262
262
  const memory = new CipherMemory(defaultMemoryDir());
263
263
  try {
264
- const results = memory.search('', {}, 10000);
265
- let entries = results.map(r => (typeof r.toDict === 'function' ? r.toDict() : r));
264
+ // Query all entries directly — memory.search('') returns nothing for empty queries
265
+ const rows = memory.symbolic.db.prepare(
266
+ 'SELECT * FROM entries WHERE is_archived = 0 ORDER BY created_at DESC LIMIT 10000'
267
+ ).all();
268
+ let entries = rows.map(r => ({
269
+ entry_id: r.entry_id,
270
+ content: r.content,
271
+ memory_type: r.memory_type,
272
+ severity: r.severity,
273
+ engagement_id: r.engagement_id,
274
+ mitre_attack: r.mitre_attack ? JSON.parse(r.mitre_attack) : [],
275
+ targets: r.targets ? JSON.parse(r.targets) : [],
276
+ tags: r.tags ? JSON.parse(r.tags) : [],
277
+ created_at: r.created_at,
278
+ }));
266
279
  if (args.engagement) {
267
- entries = entries.filter(e =>
268
- (e.engagement_id || e.engagementId) === args.engagement
269
- );
280
+ entries = entries.filter(e => e.engagement_id === args.engagement);
270
281
  }
271
- const output = args.output || 'cipher-memory-export.json';
282
+ const output = getFlagArg(args, 'output', '--output', 'cipher-memory-export.json');
272
283
  const data = { entries, count: entries.length };
273
284
  writeFileSync(output, JSON.stringify(data, null, 2), 'utf-8');
274
285
  debug(`memory-export: ${entries.length} entries → ${output}`);
@@ -388,14 +399,26 @@ export async function handleStatus(args = {}) {
388
399
  */
389
400
  export async function handleDiff(args = {}) {
390
401
  let diffText = getArg(args, 'file');
391
- if (!diffText) {
402
+
403
+ // Check for stdin marker '-' — getArg filters it as a flag, so check raw args
404
+ const isStdin = Array.isArray(args) && args.includes('-');
405
+
406
+ if (!diffText && !isStdin) {
392
407
  return { error: true, message: 'Usage: cipher diff <file.patch>\n cat changes.diff | cipher diff -' };
393
408
  }
394
409
  const { SecurityDiffAnalyzer } = await import('../pipeline/index.js');
395
410
  const analyzer = new SecurityDiffAnalyzer();
396
411
 
412
+ // Read from stdin when argument is '-'
413
+ if (isStdin) {
414
+ const chunks = [];
415
+ for await (const chunk of process.stdin) {
416
+ chunks.push(chunk);
417
+ }
418
+ diffText = Buffer.concat(chunks).toString('utf-8');
419
+ }
397
420
  // If it looks like a file path (no newlines, exists on disk), read it
398
- if (diffText && !diffText.includes('\n') && existsSync(diffText)) {
421
+ else if (diffText && !diffText.includes('\n') && existsSync(diffText)) {
399
422
  diffText = readFileSync(diffText, 'utf-8');
400
423
  }
401
424
 
@@ -472,23 +495,50 @@ export async function handleSarif(args = {}) {
472
495
  export async function handleOsint(args = {}) {
473
496
  const target = getArg(args, 'target');
474
497
  if (!target) {
475
- return { error: true, message: 'Usage: cipher osint <domain|ip> [--type domain|ip]' };
498
+ return { error: true, message: 'Usage: cipher osint <target> [--type domain|ip|username|email|url]\n\nInvestigation types:\n domain DNS, WHOIS, cert transparency, Wayback Machine, web tech, IP geolocation\n ip Reverse DNS, IP classification, geolocation\n username Search 400+ platforms via Sherlock\n email Check account existence on 120+ services via Holehe\n url Wayback Machine + archive.today snapshots' };
476
499
  }
477
500
  const { OSINTPipeline } = await import('../pipeline/index.js');
478
501
  const pipeline = new OSINTPipeline();
479
- const type = getFlagArg(args, 'type', '--type', 'domain');
502
+ const type = getFlagArg(args, 'type', '--type', null);
503
+
504
+ // Auto-detect type if not specified
505
+ let investigationType = type;
506
+ if (!investigationType) {
507
+ if (target.includes('@')) investigationType = 'email';
508
+ else if (target.startsWith('http://') || target.startsWith('https://')) investigationType = 'url';
509
+ else if (/^\d+\.\d+\.\d+\.\d+$/.test(target) || (target.includes(':') && !target.includes('/'))) investigationType = 'ip';
510
+ else if (target.includes('/')) investigationType = 'url';
511
+ else if (target.includes('.')) investigationType = 'domain';
512
+ else investigationType = 'username';
513
+ }
480
514
 
481
- const results = type === 'ip'
482
- ? pipeline.investigateIp(target)
483
- : pipeline.investigateDomain(target);
515
+ let results;
516
+ switch (investigationType) {
517
+ case 'ip':
518
+ results = await pipeline.investigateIp(target);
519
+ break;
520
+ case 'username':
521
+ results = pipeline.investigateUsername(target);
522
+ break;
523
+ case 'email':
524
+ results = pipeline.investigateEmail(target);
525
+ break;
526
+ case 'url':
527
+ results = await pipeline.investigateUrl(target);
528
+ break;
529
+ case 'domain':
530
+ default:
531
+ results = await pipeline.investigateDomain(target);
532
+ break;
533
+ }
484
534
 
485
535
  const data = {
486
536
  target,
487
- type,
537
+ type: investigationType,
488
538
  results: results.map(r => (typeof r.toDict === 'function' ? r.toDict() : r)),
489
539
  summary: pipeline.summary(),
490
540
  };
491
- debug(`osint: ${target} (${type}) → ${results.length} results`);
541
+ debug(`osint: ${target} (${investigationType}) → ${results.length} results`);
492
542
  return data;
493
543
  }
494
544
 
@@ -908,19 +958,74 @@ function countFiles(dir, filename, subdir) {
908
958
  export async function handleScan(args = {}) {
909
959
  const target = Array.isArray(args) ? args.find(a => !a.startsWith('-')) : args.target;
910
960
  if (!target) {
911
- return { error: true, message: 'Usage: cipher scan <target> [--profile <profile>]' };
961
+ return { error: true, message: 'Usage: cipher scan <target> [--profile <profile>]\n\nProfiles: quick, standard, pentest, recon, full\n quick Tech detection + known CVEs only (~30s)\n standard Common vulns + misconfigs (~2-5 min)\n pentest Full pentest template set (~5-15 min)\n recon Discovery + exposure (~2-5 min)\n full All templates (~15-30 min)' };
912
962
  }
913
963
  try {
914
964
  const { NucleiRunner, ScanProfile } = await import('../pipeline/scanner.js');
915
965
  const runner = new NucleiRunner();
966
+
967
+ if (!runner.available) {
968
+ return { error: true, message: 'nuclei is not installed.\nInstall: go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest' };
969
+ }
970
+
916
971
  const profileArg = Array.isArray(args) ? (args.find((a, i) => args[i - 1] === '--profile') || 'standard') : (args.profile || 'standard');
917
- const result = await runner.scan(target, { profile: ScanProfile.fromDomain(profileArg) });
972
+
973
+ // Quick profile — fast tech detection + critical CVEs only
974
+ let scanOpts;
975
+ if (profileArg === 'quick') {
976
+ // Quick mode: use specific fast templates instead of tag-based matching
977
+ // This avoids nuclei loading 900+ templates for the 'tech' tag
978
+ const quickTemplates = [
979
+ 'http/technologies/tech-detect.yaml',
980
+ 'http/technologies/waf-detect.yaml',
981
+ 'dns/dns-waf-detect.yaml',
982
+ ];
983
+ // Resolve template paths — check nuclei's default template dir
984
+ const { homedir: hd } = await import('node:os');
985
+ const { existsSync: exists } = await import('node:fs');
986
+ const { join: pjoin } = await import('node:path');
987
+ const templateBase = pjoin(hd(), 'nuclei-templates');
988
+ const resolvedPaths = quickTemplates
989
+ .map(t => pjoin(templateBase, t))
990
+ .filter(t => exists(t));
991
+
992
+ if (resolvedPaths.length > 0) {
993
+ scanOpts = { templatePaths: resolvedPaths, timeout: 30 };
994
+ } else {
995
+ // Fallback: use tags if template dir not found
996
+ scanOpts = {
997
+ profile: { tags: ['tech'], severity: ['info'], rateLimit: 150, bulkSize: 50, concurrency: 50, requestTimeout: 5, headless: false },
998
+ timeout: 120,
999
+ };
1000
+ }
1001
+ process.stderr.write(' ● Scanning (quick — tech detection, ~5-10s)...\n');
1002
+ } else {
1003
+ scanOpts = { profile: ScanProfile.fromDomain(profileArg) };
1004
+ process.stderr.write(` ● Scanning (${profileArg} profile — this may take several minutes)...\n`);
1005
+ }
1006
+
1007
+ const result = scanOpts.templatePaths
1008
+ ? await runner.scanWithTemplates(target, scanOpts)
1009
+ : await runner.scan(target, scanOpts);
1010
+
1011
+ process.stderr.write(` ✔ Scan complete: ${(result.findings || []).length} finding(s) in ${result.durationSeconds || '?'}s\n`);
1012
+
918
1013
  return {
919
1014
  output: JSON.stringify({
920
1015
  target,
921
1016
  profile: profileArg,
922
- findings: (result.findings || []).length,
923
- status: 'completed',
1017
+ findings_count: (result.findings || []).length,
1018
+ findings: (result.findings || []).map(f => ({
1019
+ name: f.name || f.templateId,
1020
+ severity: f.severity,
1021
+ matched_at: f.matchedAt || f.host,
1022
+ template: f.templateId,
1023
+ description: f.description || '',
1024
+ })),
1025
+ stats: result.stats || {},
1026
+ duration_seconds: result.durationSeconds || 0,
1027
+ status: result.success ? 'completed' : 'completed_with_errors',
1028
+ errors: (result.errors || []).length > 0 ? result.errors : undefined,
924
1029
  }, null, 2),
925
1030
  };
926
1031
  } catch (err) {
@@ -928,36 +1033,6 @@ export async function handleScan(args = {}) {
928
1033
  }
929
1034
  }
930
1035
 
931
- // ---------------------------------------------------------------------------
932
- // Dashboard command — stub for Node.js TUI (Textual TUI not ported)
933
- // ---------------------------------------------------------------------------
934
-
935
- export async function handleDashboard() {
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 {};
945
- }
946
-
947
- // ---------------------------------------------------------------------------
948
- // Web command — delegates to API server
949
- // ---------------------------------------------------------------------------
950
-
951
- export async function handleWeb(args = {}) {
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 {};
959
- }
960
-
961
1036
  // ---------------------------------------------------------------------------
962
1037
  // Setup Signal command — Signal bot configuration
963
1038
  // ---------------------------------------------------------------------------
@@ -60,8 +60,6 @@ export {
60
60
  handleMarketplace,
61
61
  handleCompliance,
62
62
  handleScan,
63
- handleDashboard,
64
- handleWeb,
65
63
  handleSetupSignal,
66
64
  handleUpdate,
67
65
  } from './commands.js';