@automagik/genie 4.260424.18 → 4.260424.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automagik/genie",
3
- "version": "4.260424.18",
3
+ "version": "4.260424.19",
4
4
  "description": "Collaborative terminal toolkit for human + AI workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genie",
3
- "version": "4.260424.18",
3
+ "version": "4.260424.19",
4
4
  "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.",
5
5
  "author": {
6
6
  "name": "Namastex Labs"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genie-plugin",
3
- "version": "4.260424.18",
3
+ "version": "4.260424.19",
4
4
  "private": true,
5
5
  "description": "Runtime dependencies for genie bundled CLIs",
6
6
  "type": "module",
@@ -365,7 +365,63 @@ function renderAuditRow(severity, verb, target, recovery) {
365
365
  process.stderr.write(` ${TTY.dim}recovery: ${TTY.reset}${TTY.cyan}${recovery}${TTY.reset}\n\n`);
366
366
  }
367
367
 
368
- function showPlanSummary(plan, options) {
368
+ function renderBreachImpact(envelope) {
369
+ const breach = envelope?.breachImpact || { enabled: false };
370
+ if (!breach.enabled || (breach.likelyStolen || []).length === 0) return;
371
+
372
+ process.stderr.write('\n');
373
+ hrule('═', TTY.red + TTY.bold);
374
+ banner('☠ BREACH IMPACT — RETRACING THE WORM ☠', SEVERITY.CRITICAL);
375
+ hrule('═', TTY.red + TTY.bold);
376
+ process.stderr.write('\n');
377
+
378
+ process.stderr.write(
379
+ ` ${TTY.dim}Exfil channel:${TTY.reset} ${TTY.red}${TTY.bold}${breach.exfilChannel.host}${TTY.reset} ${breach.exfilChannel.paths.join(', ')}\n`,
380
+ );
381
+ if (breach.compromiseWindow) {
382
+ process.stderr.write(
383
+ ` ${TTY.dim}Window:${TTY.reset} ${breach.compromiseWindow.firstEvidence} .. ${breach.compromiseWindow.lastEvidence}\n`,
384
+ );
385
+ }
386
+ if ((breach.compromisedInstallPaths || []).length > 0) {
387
+ process.stderr.write(` ${TTY.dim}Install path:${TTY.reset} ${breach.compromisedInstallPaths[0]}\n`);
388
+ }
389
+ process.stderr.write('\n');
390
+ process.stderr.write(
391
+ ` ${TTY.bold}${TTY.red}The env-compat.cjs payload ran as this user during the window.${TTY.reset}\n`,
392
+ );
393
+ process.stderr.write(` ${TTY.dim}These credentials were readable to it — assume stolen:${TTY.reset}\n\n`);
394
+
395
+ for (const item of breach.rotationChecklist || []) {
396
+ const sev =
397
+ item.severity === 'CRITICAL'
398
+ ? SEVERITY.CRITICAL.paint(` ${item.severity} `)
399
+ : SEVERITY.DESTRUCTIVE.paint(` ${item.severity} `);
400
+ process.stderr.write(` ${sev} ${TTY.bold}${item.category}${TTY.reset}\n`);
401
+ for (const p of item.paths || []) process.stderr.write(` ${TTY.dim}path:${TTY.reset} ${p}\n`);
402
+ process.stderr.write(` ${TTY.dim}why:${TTY.reset} ${item.reason}\n`);
403
+ if (item.rotationUrl) {
404
+ process.stderr.write(
405
+ ` ${TTY.cyan}rotate:${TTY.reset} ${TTY.underline}${item.rotationUrl}${TTY.reset}\n`,
406
+ );
407
+ }
408
+ process.stderr.write('\n');
409
+ }
410
+
411
+ if ((breach.runningProcessesDuringWindow || []).length > 0) {
412
+ process.stderr.write(` ${TTY.bold}Processes that ran the compromised binary:${TTY.reset}\n`);
413
+ for (const proc of breach.runningProcessesDuringWindow) {
414
+ process.stderr.write(
415
+ ` pid=${proc.pid} elapsed=${proc.elapsed} ${TTY.dim}${(proc.command || '').slice(0, 90)}${TTY.reset}\n`,
416
+ );
417
+ }
418
+ process.stderr.write('\n');
419
+ }
420
+ }
421
+
422
+ function showPlanSummary(plan, options, envelope) {
423
+ renderBreachImpact(envelope);
424
+
369
425
  process.stderr.write('\n');
370
426
  hrule('═', TTY.red + TTY.bold);
371
427
  banner('⚠ DESTRUCTIVE OPERATIONS AUDIT — REVIEW BEFORE ACCEPTING ⚠', SEVERITY.DESTRUCTIVE);
@@ -668,7 +724,7 @@ function main() {
668
724
 
669
725
  const envelope = runScan();
670
726
  const plan = classifyEnvelope(envelope);
671
- const somethingToDo = showPlanSummary(plan, options);
727
+ const somethingToDo = showPlanSummary(plan, options, envelope);
672
728
 
673
729
  if (!somethingToDo) {
674
730
  if (options.json) {
@@ -364,9 +364,24 @@ const TEXT_MATCHERS = [
364
364
  regex: /\b(?:node|bun|bash|sh)\b[^\n]{0,200}env-compat\.(?:cjs|js)\b/i,
365
365
  },
366
366
  {
367
- label: 'network:curl-wget IOC',
367
+ // Hard evidence: curl/wget/fetch to the exact exfil endpoint path.
368
+ // This is what the CanisterWorm payload uses to upload stolen data
369
+ // (POST /v1/telemetry and POST /v1/drop). Matching these = compromise.
370
+ label: 'network:curl-wget IOC-exfil',
368
371
  category: 'network',
369
- regex: /\b(?:curl|wget|fetch|Invoke-WebRequest)\b[^\n]{0,200}(?:telemetry\.api-monitor\.com|raw\.icp0\.io\/drop)/i,
372
+ regex:
373
+ /\b(?:curl|wget|fetch|Invoke-WebRequest)\b[^\n]{0,200}(?:telemetry\.api-monitor\.com\/v1\/(?:telemetry|drop)|raw\.icp0\.io\/drop)/i,
374
+ },
375
+ {
376
+ // Soft evidence: bare-host mention of the exfil domain WITHOUT the
377
+ // /v1/ path. Almost always an incident responder (or documentation)
378
+ // probing the host, not the payload itself — the payload never runs
379
+ // a bare `curl <host>` because there's no endpoint that would accept
380
+ // the uploaded payload. Classify as 'probe' so it shows in the
381
+ // report but doesn't elevate the suspicion score.
382
+ label: 'network:exfil-host-probe',
383
+ category: 'probe',
384
+ regex: /\b(?:curl|wget|fetch|Invoke-WebRequest)\b[^\n]{0,200}telemetry\.api-monitor\.com(?!\/v1\/)/i,
370
385
  },
371
386
  ];
372
387
 
@@ -1415,6 +1430,7 @@ function collectTextIndicators(text) {
1415
1430
  installCommands: [],
1416
1431
  executionCommands: [],
1417
1432
  networkCommands: [],
1433
+ probeMatches: [],
1418
1434
  allMatches: [],
1419
1435
  };
1420
1436
 
@@ -1427,6 +1443,11 @@ function collectTextIndicators(text) {
1427
1443
  if (matcher.category === 'install') indicators.installCommands.push(matcher.label);
1428
1444
  if (matcher.category === 'execution') indicators.executionCommands.push(matcher.label);
1429
1445
  if (matcher.category === 'network') indicators.networkCommands.push(matcher.label);
1446
+ // `probe` is informational-only: it means the text references an
1447
+ // exfil host but WITHOUT the attacker's uploading path. Almost
1448
+ // always a responder probing or documentation. Never elevates
1449
+ // compromise severity.
1450
+ if (matcher.category === 'probe') indicators.probeMatches.push(matcher.label);
1430
1451
  }
1431
1452
 
1432
1453
  for (const trackedPackage of TRACKED_PACKAGES) {
@@ -2639,6 +2660,229 @@ function scanImpactSurface(homes, roots, report, runtime) {
2639
2660
  report.impactSurfaceFindings = uniq(findings.map((entry) => JSON.stringify(entry))).map((entry) => JSON.parse(entry));
2640
2661
  }
2641
2662
 
2663
+ // ---------------------------------------------------------------------------
2664
+ // Breach-impact analysis — retrace the known CanisterWorm payload behavior
2665
+ // against what actually exists on this host.
2666
+ //
2667
+ // The `env-compat.cjs` payload is a postinstall script that runs under the
2668
+ // installing user's identity. Public research + the IOC strings the scanner
2669
+ // already matches (TEL_ENDPOINT, ICP_CANISTER_ID, pkg-telemetry, AES-256-CBC,
2670
+ // RSA-OAEP-SHA256, pypi-pth-exfil, etc.) give us a concrete list of targets:
2671
+ //
2672
+ // 1. Environment variables visible to `npm run postinstall` at install
2673
+ // time (anything in process.env when `env-compat.cjs` executed). The
2674
+ // scanner cannot read historical env state, but install-time env for
2675
+ // shells is commonly .env files + shell profiles.
2676
+ // 2. Credential files under $HOME that the install-user could read.
2677
+ // 3. Browser login-data / cookie stores (chrome/brave/edge/chromium).
2678
+ // 4. Crypto wallets.
2679
+ // 5. SSH keys + known_hosts (for lateral movement).
2680
+ // 6. Session tokens in ~/.config/gh and ~/.config/gcloud and ~/.aws.
2681
+ //
2682
+ // This phase correlates the scanner's own impactSurfaceFindings (what EXISTS
2683
+ // on this host) with the known CanisterWorm targeting to produce
2684
+ // `breachImpact.likelyStolen` — a structured, actionable checklist for
2685
+ // credential rotation rather than a generic "rotate your tokens" line.
2686
+ //
2687
+ // Only fires if hard compromise evidence is present; otherwise the host is
2688
+ // not believed to have been exposed and we emit an empty/disabled report.
2689
+ // ---------------------------------------------------------------------------
2690
+
2691
+ const CANISTERWORM_TARGETS = [
2692
+ // Package registry tokens — highest priority (full supply-chain impact).
2693
+ {
2694
+ match: /(^|\/)\.npmrc$/,
2695
+ category: 'npm-token',
2696
+ severity: 'CRITICAL',
2697
+ rotationUrl: 'https://www.npmjs.com/settings/~/tokens',
2698
+ reason: 'env-compat.cjs reads ~/.npmrc to exfil auth tokens; compromised npm publish rights = supply-chain risk',
2699
+ },
2700
+ {
2701
+ match: /\.config\/gh\/hosts\.yml$/,
2702
+ category: 'github-pat',
2703
+ severity: 'CRITICAL',
2704
+ rotationUrl: 'https://github.com/settings/tokens',
2705
+ reason:
2706
+ 'gh CLI tokens grant repo + workflow-secrets access; public research shows exfil to telemetry.api-monitor.com',
2707
+ },
2708
+ // Cloud IAM — infrastructure access.
2709
+ {
2710
+ match: /\.aws\/(credentials|config)$/,
2711
+ category: 'aws-iam',
2712
+ severity: 'CRITICAL',
2713
+ rotationUrl: 'https://console.aws.amazon.com/iam/home#/security_credentials',
2714
+ reason: 'AWS access keys enable infrastructure takeover',
2715
+ },
2716
+ {
2717
+ match: /\.config\/gcloud\/(application_default_credentials|access_tokens)/,
2718
+ category: 'gcp-iam',
2719
+ severity: 'CRITICAL',
2720
+ rotationUrl: 'https://console.cloud.google.com/iam-admin/serviceaccounts',
2721
+ reason: 'GCP application-default credentials / access tokens enable project takeover',
2722
+ },
2723
+ {
2724
+ match: /\.azure\//,
2725
+ category: 'azure-iam',
2726
+ severity: 'CRITICAL',
2727
+ rotationUrl: 'https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/Overview',
2728
+ reason: 'Azure CLI refresh tokens enable subscription access',
2729
+ },
2730
+ // AI provider keys (the payload specifically targets these per pkg-telemetry).
2731
+ {
2732
+ match: /(^|\/)\.env(\.|$)/,
2733
+ category: 'dotenv',
2734
+ severity: 'CRITICAL',
2735
+ rotationUrl: null,
2736
+ reason: 'env-compat.cjs reads .env files for ANTHROPIC_API_KEY / OPENAI_API_KEY / custom secrets',
2737
+ },
2738
+ // SSH — lateral movement.
2739
+ {
2740
+ match: /\.ssh\/(id_rsa|id_ed25519|id_ecdsa|config|known_hosts)/,
2741
+ category: 'ssh-key',
2742
+ severity: 'HIGH',
2743
+ rotationUrl: null,
2744
+ reason: 'SSH private keys + known_hosts enable lateral movement to other hosts',
2745
+ },
2746
+ // Browser login data — session cookies, stored passwords.
2747
+ {
2748
+ match:
2749
+ /(Application Support|\.config)\/(Google\/Chrome|Chromium|BraveSoftware|Microsoft Edge)\/(Default|Profile\s*\d+)\/?/i,
2750
+ category: 'browser-session',
2751
+ severity: 'HIGH',
2752
+ rotationUrl: null,
2753
+ reason: 'browser Login Data + Cookies files contain session tokens that bypass 2FA on re-use',
2754
+ },
2755
+ // Crypto wallets.
2756
+ {
2757
+ match: /(Application Support|\.local\/share|\.config)\/(Ledger Live|Exodus|Electrum|MetaMask)/i,
2758
+ category: 'crypto-wallet',
2759
+ severity: 'CRITICAL',
2760
+ rotationUrl: null,
2761
+ reason: 'crypto wallet data allows direct fund theft if keystore passphrase is also captured',
2762
+ },
2763
+ ];
2764
+
2765
+ function classifyCanisterWormTarget(path) {
2766
+ for (const target of CANISTERWORM_TARGETS) {
2767
+ if (target.match.test(path)) return target;
2768
+ }
2769
+ return null;
2770
+ }
2771
+
2772
+ function hasHardCompromiseEvidence(report) {
2773
+ if ((report.installFindings || []).length > 0) return true;
2774
+ if ((report.bunCacheFindings || []).length > 0) return true;
2775
+ if ((report.npmTarballFetches || []).length > 0) return true;
2776
+ // Any temp-artifact with known malware hash.
2777
+ for (const entry of report.tempArtifactFindings || []) {
2778
+ if (entry.knownMalwareHash) return true;
2779
+ if ((entry.iocMatches || []).length > 0) return true;
2780
+ }
2781
+ return false;
2782
+ }
2783
+
2784
+ function scanBreachImpact(report) {
2785
+ const hasEvidence = hasHardCompromiseEvidence(report);
2786
+ const breachImpact = {
2787
+ enabled: hasEvidence,
2788
+ exfilChannel: {
2789
+ host: 'telemetry.api-monitor.com',
2790
+ paths: ['/v1/telemetry', '/v1/drop'],
2791
+ observed: false,
2792
+ },
2793
+ compromiseWindow: null,
2794
+ likelyStolen: [],
2795
+ compromisedInstallPaths: [],
2796
+ runningProcessesDuringWindow: [],
2797
+ rotationChecklist: [],
2798
+ };
2799
+
2800
+ if (!hasEvidence) {
2801
+ report.breachImpact = breachImpact;
2802
+ return;
2803
+ }
2804
+
2805
+ // Determine compromise window from evidence timestamps.
2806
+ const evidenceTimes = [];
2807
+ for (const entry of report.installFindings || []) {
2808
+ const t = entry.modifiedAt || null;
2809
+ if (t) evidenceTimes.push(Date.parse(t));
2810
+ breachImpact.compromisedInstallPaths.push(entry.path);
2811
+ }
2812
+ for (const entry of report.bunCacheFindings || []) {
2813
+ const t = entry.modifiedAt || null;
2814
+ if (t) evidenceTimes.push(Date.parse(t));
2815
+ }
2816
+ for (const entry of report.npmTarballFetches || []) {
2817
+ const t = entry.time || entry.cacheRecordTime || null;
2818
+ if (t) evidenceTimes.push(Date.parse(t));
2819
+ }
2820
+ const validTimes = evidenceTimes.filter((t) => Number.isFinite(t) && t > 0);
2821
+ if (validTimes.length > 0) {
2822
+ breachImpact.compromiseWindow = {
2823
+ firstEvidence: new Date(Math.min(...validTimes)).toISOString(),
2824
+ lastEvidence: new Date(Math.max(...validTimes)).toISOString(),
2825
+ };
2826
+ }
2827
+
2828
+ // Cross-reference impact-surface findings against known CanisterWorm targets.
2829
+ // Every match is a credential the payload, running as the installing user,
2830
+ // had read access to during the compromise window.
2831
+ for (const entry of report.impactSurfaceFindings || []) {
2832
+ const target = classifyCanisterWormTarget(entry.path);
2833
+ if (!target) continue;
2834
+ breachImpact.likelyStolen.push({
2835
+ category: target.category,
2836
+ severity: target.severity,
2837
+ path: entry.path,
2838
+ reason: target.reason,
2839
+ rotationUrl: target.rotationUrl,
2840
+ });
2841
+ }
2842
+
2843
+ // Also include .env files discovered in scanImpactSurface (they're in
2844
+ // impactSurfaceFindings with kind='secret-store').
2845
+ for (const entry of report.impactSurfaceFindings || []) {
2846
+ if (entry.kind !== 'secret-store') continue;
2847
+ if (breachImpact.likelyStolen.some((s) => s.path === entry.path)) continue;
2848
+ const target = classifyCanisterWormTarget(entry.path);
2849
+ if (target) continue; // already classified above
2850
+ breachImpact.likelyStolen.push({
2851
+ category: 'dotenv',
2852
+ severity: 'CRITICAL',
2853
+ path: entry.path,
2854
+ reason: 'env-compat.cjs reads .env files for API keys (ANTHROPIC, OPENAI, custom secrets)',
2855
+ rotationUrl: null,
2856
+ });
2857
+ }
2858
+
2859
+ // Correlate live processes with the compromise window: any process whose
2860
+ // elapsed time overlaps the window and which was spawned by the compromised
2861
+ // install path is part of the breach footprint.
2862
+ for (const entry of report.liveProcessFindings || []) {
2863
+ if (!entry.matchedInstallPaths || entry.matchedInstallPaths.length === 0) continue;
2864
+ breachImpact.runningProcessesDuringWindow.push({
2865
+ pid: entry.pid,
2866
+ elapsed: entry.elapsed,
2867
+ command: (entry.command || '').slice(0, 160),
2868
+ });
2869
+ }
2870
+
2871
+ // Build a priority-sorted rotation checklist (deduped by category).
2872
+ const severityRank = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
2873
+ const byCategory = new Map();
2874
+ for (const item of breachImpact.likelyStolen) {
2875
+ const existing = byCategory.get(item.category);
2876
+ if (!existing) byCategory.set(item.category, { ...item, paths: [item.path] });
2877
+ else existing.paths.push(item.path);
2878
+ }
2879
+ breachImpact.rotationChecklist = [...byCategory.values()].sort(
2880
+ (a, b) => (severityRank[a.severity] ?? 99) - (severityRank[b.severity] ?? 99),
2881
+ );
2882
+
2883
+ report.breachImpact = breachImpact;
2884
+ }
2885
+
2642
2886
  function collectTempRoots(platformInfo, homes, roots) {
2643
2887
  const tempRoots = new Set();
2644
2888
 
@@ -3359,6 +3603,35 @@ function printHumanReport(report) {
3359
3603
  }
3360
3604
  }
3361
3605
 
3606
+ const breach = report.breachImpact || { enabled: false };
3607
+ if (breach.enabled && (breach.likelyStolen || []).length > 0) {
3608
+ console.log('');
3609
+ console.log('BREACH IMPACT — retracing CanisterWorm exfil behavior against this host:');
3610
+ if (breach.compromiseWindow) {
3611
+ console.log(
3612
+ ` compromise window: ${breach.compromiseWindow.firstEvidence} .. ${breach.compromiseWindow.lastEvidence}`,
3613
+ );
3614
+ }
3615
+ console.log(
3616
+ ` exfil channel: ${breach.exfilChannel.host} ${breach.exfilChannel.paths.join(', ')}${breach.exfilChannel.observed ? ' (observed)' : ' (not observed in logs — channel targeting only)'}`,
3617
+ );
3618
+ console.log('');
3619
+ console.log(' likely-stolen credentials (grouped, priority-sorted):');
3620
+ for (const item of breach.rotationChecklist || []) {
3621
+ console.log(` [${item.severity}] ${item.category}`);
3622
+ for (const p of item.paths || []) console.log(` path: ${p}`);
3623
+ console.log(` why: ${item.reason}`);
3624
+ if (item.rotationUrl) console.log(` rotate at: ${item.rotationUrl}`);
3625
+ }
3626
+ if ((breach.runningProcessesDuringWindow || []).length > 0) {
3627
+ console.log('');
3628
+ console.log(' processes that ran the compromised binary:');
3629
+ for (const proc of breach.runningProcessesDuringWindow) {
3630
+ console.log(` pid=${proc.pid} elapsed=${proc.elapsed} cmd=${proc.command}`);
3631
+ }
3632
+ }
3633
+ }
3634
+
3362
3635
  if (report.npmCacheMetadata.length > 0) {
3363
3636
  console.log('');
3364
3637
  console.log('npm cache metadata observations:');
@@ -3523,6 +3796,7 @@ async function main() {
3523
3796
  tempArtifactFindings: [],
3524
3797
  liveProcessFindings: [],
3525
3798
  impactSurfaceFindings: [],
3799
+ breachImpact: { enabled: false, likelyStolen: [], rotationChecklist: [] },
3526
3800
  timeline: [],
3527
3801
  errors: [],
3528
3802
  };
@@ -3621,6 +3895,14 @@ async function main() {
3621
3895
  () => scanLiveProcesses(report),
3622
3896
  report,
3623
3897
  );
3898
+ await runPhase(
3899
+ runtime,
3900
+ 'scanBreachImpact',
3901
+ 'breach-impact',
3902
+ '(breach analysis)',
3903
+ () => scanBreachImpact(report),
3904
+ report,
3905
+ );
3624
3906
 
3625
3907
  report.timeline = sortTimeline(report.timeline);
3626
3908
  report.summary = summarize(report);