{"uuid": "6397bdc2-5791-4276-8c4f-1b9d52b87c4c", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-45321", "type": "seen", "source": "https://gist.github.com/Fazzani/7a76d5ae430e05c763c6a86cbca9d42f", "content": "#!/usr/bin/env node\n'use strict';\n\n/**\n * mini-shai-hulud-audit.js\n * Workstation / runner scanner for the Mini Shai-Hulud campaign (CTI Advisory #002)\n * CVE-2026-45321 / GHSA-g7cv-rxg3-hmpx \u2014 CVSS 9.6 Critical\n * Threat actor: TeamPCP (aliases: DeadCatx3, PCPcat, ShellForce, CipherForce)\n *\n * \u26a0\ufe0f  CRITICAL SAFETY WARNING  \u26a0\ufe0f\n * If infection is suspected, DO NOT revoke any tokens before isolating the machine.\n * The malware watchdog detects revocation and triggers: rm -rf ~/\n * Incident response order: ISOLATE \u2192 IMAGE \u2192 KILL DAEMON \u2192 REVOKE \u2192 ROTATE\n *\n * Zero runtime dependencies \u2014 requires Node.js &gt;= 14 only.\n * Usage: node mini-shai-hulud/mini-shai-hulud-audit.js [--root ] [--output ]\n */\n\nconst fs = require('node:fs');\nconst os = require('node:os');\nconst path = require('node:path');\nconst { execSync } = require('node:child_process');\n\nconst { MINI_SHAI_HULUD_PATTERNS } = require('./incident-patterns-mini-shai-hulud');\nconst { createLogger, dedupeByKey, makeProgress, readTextIfExists, scanTextForPatterns, walkFiles, writeReportFile } = require('../incident-utils');\n\nconst REPORT_FILE = 'mini-shai-hulud-audit-report.csv';\nconst ADVISORY_REF = 'CTI Advisory #002 \u2014 CVE-2026-45321 \u2014 TLP:AMBER';\n\nconst CSV_COLUMNS = [\n  'recordType',\n  'auditDate',\n  'host',\n  'platform',\n  'root',\n  'checkType',\n  'targetedFiles',\n  'scannedFiles',\n  'filesWithHits',\n  'totalHits',\n  'criticalHits',\n  'categories',\n  'path',\n  'size',\n  'mtime',\n  'id',\n  'label',\n  'category',\n  'severity',\n  'match',\n  'snippet',\n];\n\n// \u2500\u2500 CLI argument parser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction parseArgs(argv) {\n  const args = argv.slice(2);\n  const opts = {\n    output: REPORT_FILE,\n    root: null,\n    help: false,\n    maxDepth: 4,\n    lockfiles: false, // opt-in: scan lockfiles for IOCs (slow on large repos)\n  };\n\n  for (let i = 0; i &lt; args.length; i++) {\n    switch (args[i]) {\n      case '--output':\n      case '-o':\n        opts.output = args[++i];\n        break;\n      case '--root':\n      case '-r':\n        opts.root = args[++i];\n        break;\n      case '--max-depth':\n      case '--depth':\n        opts.maxDepth = Number(args[++i]);\n        break;\n      case '--lockfiles':\n        opts.lockfiles = true;\n        break;\n      case '--help':\n      case '-h':\n        opts.help = true;\n        break;\n    }\n  }\n\n  return opts;\n}\n\n// \u2500\u2500 Help \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction showHelp() {\n  const { C } = createLogger();\n  console.log(`\n${C.bold}${C.red}\u26a0\ufe0f  CRITICAL SAFETY WARNING${C.reset}\n  Do NOT revoke any tokens before isolating this machine.\n  The worm's watchdog triggers ${C.bold}rm -rf ~/${C.reset} on token revocation.\n\n${C.bold}${C.cyan}mini-shai-hulud-audit${C.reset}\nLocal evidence collector for the Mini Shai-Hulud campaign\n${C.dim}${ADVISORY_REF}${C.reset}\n\nUSAGE\n  node mini-shai-hulud/mini-shai-hulud-audit.js [options]\n\nOPTIONS\n  --root, -r      Extra folder to scan recursively for text evidence\n  --output, -o    Output file (default: ${REPORT_FILE})\n  --max-depth        Max recursion depth for --root (default: 4)\n  --lockfiles           Also scan lockfiles (package-lock.json, yarn.lock...)\n                        for IOC references \u2014 slow but thorough\n  --help, -h            Show this help\n\nDEFAULT SCAN TARGETS\n  Payload files   setup_bun.js, bun_environment.js, router_init.js,\n                  router_runtime.js, tanstack_runner.js (home + temp dirs)\n  Linux           /tmp/transformers.pyz (PyPI mistralai payload)\n  Persistence     ~/.claude/settings.json, .vscode/tasks.json, ~/.claude.json,\n                  */.claude/setup.mjs, */.vscode/setup.mjs\n  Daemons         ~/.config/**/gh-token-monitor*, ~/.local/bin/gh-token-monitor.sh\n  Shell history   bash_history, zsh_history, PSReadLine\n  npm logs        ~/.npm/_logs, AppData npm-cache logs\n\nINCIDENT RESPONSE ORDER (if infection confirmed)\n  1. ISOLATE  \u2014 disconnect machine from network\n  2. IMAGE    \u2014 forensic disk image before any cleanup\n  3. KILL     \u2014 disable gh-token-monitor daemon\n  4. REVOKE   \u2014 npm, GitHub PAT/OAuth, AWS, Azure, GCP, Kubernetes, SSH\n  5. ROTATE   \u2014 all credentials reachable from this host\n  6. AUDIT    \u2014 CloudTrail / Azure Activity / GCP Audit logs\n\nREFERENCES\n  CVE-2026-45321        https://tenable.com/cve/CVE-2026-45321\n  GHSA-g7cv-rxg3-hmpx   https://github.com/TanStack/router/security/advisories/GHSA-g7cv-rxg3-hmpx\n  TanStack postmortem   https://tanstack.com/blog/npm-supply-chain-compromise-postmortem\n`);\n}\n\n// \u2500\u2500 IOC-specific artefact paths \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction joinIfPresent(...parts) {\n  if (parts.some((part) =&gt; !part)) return null;\n  return path.join(...parts);\n}\n\nfunction getDefaultTargets() {\n  const home = os.homedir();\n  const tempDir = process.env.TEMP || process.env.TMP || os.tmpdir();\n  const appData = process.env.APPDATA;\n  const localAppData = process.env.LOCALAPPDATA;\n\n  // \u2500\u2500 Payload files to check for existence \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  // These files should not exist on a clean machine \u2014 their presence is high-confidence.\n  const payloadFileNames = ['setup_bun.js', 'bun_environment.js', 'router_init.js', 'router_runtime.js', 'tanstack_runner.js'];\n\n  const payloadSearchRoots = [home, tempDir, os.tmpdir()].filter(Boolean);\n  const knownPayloadPaths = [];\n  for (const root of payloadSearchRoots) {\n    for (const name of payloadFileNames) {\n      knownPayloadPaths.push(path.join(root, name));\n    }\n  }\n\n  // Linux-specific PyPI payload\n  knownPayloadPaths.push('/tmp/transformers.pyz');\n\n  // \u2500\u2500 Persistence configs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  const persistenceFiles = [\n    joinIfPresent(home, '.claude', 'settings.json'),\n    joinIfPresent(home, '.vscode', 'tasks.json'),\n    joinIfPresent(home, '.claude.json'),\n    joinIfPresent(home, '.claude', 'setup.mjs'),\n    joinIfPresent(home, '.vscode', 'setup.mjs'),\n  ].filter(Boolean);\n\n  // \u2500\u2500 Daemon locations \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  const daemonFiles = [\n    joinIfPresent(home, '.local', 'bin', 'gh-token-monitor.sh'),\n    joinIfPresent(home, '.config', 'systemd', 'user', 'gh-token-monitor.service'),\n    joinIfPresent(home, 'Library', 'LaunchAgents', 'com.gh-token-monitor.plist'),\n  ].filter(Boolean);\n\n  // \u2500\u2500 Shell history \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  const historyFiles = [\n    joinIfPresent(home, '.bash_history'),\n    joinIfPresent(home, '.zsh_history'),\n    joinIfPresent(home, '.local', 'share', 'fish', 'fish_history'),\n    joinIfPresent(appData, 'Microsoft', 'Windows', 'PowerShell', 'PSReadLine', 'ConsoleHost_history.txt'),\n    joinIfPresent(appData, 'Microsoft', 'PowerShell', 'PSReadLine', 'ConsoleHost_history.txt'),\n    joinIfPresent(home, 'AppData', 'Roaming', 'Microsoft', 'Windows', 'PowerShell', 'PSReadLine', 'ConsoleHost_history.txt'),\n  ].filter(Boolean);\n\n  // \u2500\u2500 npm debug logs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  const logDirs = [\n    joinIfPresent(home, '.npm', '_logs'),\n    joinIfPresent(appData, 'npm-cache', '_logs'),\n    joinIfPresent(localAppData, 'npm-cache', '_logs'),\n    joinIfPresent(home, 'Library', 'Logs', 'npm'),\n  ].filter(Boolean);\n\n  // \u2500\u2500 Daemon config dirs to scan \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  const daemonSearchDirs = [joinIfPresent(home, '.config'), joinIfPresent(home, '.local', 'bin'), joinIfPresent(home, 'Library', 'LaunchAgents')].filter(Boolean);\n\n  return {\n    files: [...knownPayloadPaths, ...persistenceFiles, ...daemonFiles, ...historyFiles],\n    dirs: [...logDirs, ...daemonSearchDirs],\n  };\n}\n\n// \u2500\u2500 File filter for directory walks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction isTextLikeEvidence(filePath, opts) {\n  const name = path.basename(filePath).toLowerCase();\n\n  if (!opts.lockfiles) {\n    // Lock files have high noise for most patterns \u2014 skip unless opted in.\n    // Exception: we DO want to check package-lock.json for the poisoned commit ref\n    // and optionalDependencies IOC; caller handles that via specific file checks.\n    if (name === 'yarn.lock' || name === 'pnpm-lock.yaml' || name === 'pnpm-lock.yml' || name === 'composer.lock' || name === 'gemfile.lock' || name === 'poetry.lock' || name === 'cargo.lock')\n      return false;\n  }\n\n  return (\n    name.endsWith('.log') ||\n    name.endsWith('.txt') ||\n    name.endsWith('.json') ||\n    name.endsWith('.yaml') ||\n    name.endsWith('.yml') ||\n    name.endsWith('.mjs') ||\n    name.endsWith('.js') ||\n    name.endsWith('.history') ||\n    name.endsWith('.ps1') ||\n    name.endsWith('.sh') ||\n    name.endsWith('.zsh') ||\n    name.endsWith('.bash') ||\n    name.endsWith('.py') ||\n    name.endsWith('.pyz') ||\n    name.endsWith('.plist') ||\n    name.endsWith('.service') ||\n    name === 'fish_history' ||\n    name === 'consolehost_history.txt' ||\n    name.startsWith('npm-debug') ||\n    name.startsWith('pnpm-debug') ||\n    name.startsWith('yarn-error') ||\n    name === 'gh-token-monitor'\n  );\n}\n\n// \u2500\u2500 File scanner \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction scanFile(filePath) {\n  const file = readTextIfExists(filePath);\n  if (!file) return [];\n\n  const hits = scanTextForPatterns(file.body, MINI_SHAI_HULUD_PATTERNS);\n  return hits.map((hit) =&gt; ({\n    path: file.path,\n    size: file.size,\n    mtime: file.mtime.toISOString(),\n    ...hit,\n  }));\n}\n\nfunction groupByFile(hits) {\n  const byFile = new Map();\n  for (const hit of hits) {\n    const bucket = byFile.get(hit.path) ?? [];\n    bucket.push(hit);\n    byFile.set(hit.path, bucket);\n  }\n  return [...byFile.entries()].map(([filePath, fileHits]) =&gt; ({\n    path: filePath,\n    hits: fileHits,\n  }));\n}\n\n// \u2500\u2500 Payload file existence check \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// These files should not normally exist \u2014 their presence alone is a critical IOC.\n\nfunction checkPayloadFileExists(filePath) {\n  if (!fs.existsSync(filePath)) return null;\n  try {\n    const stats = fs.statSync(filePath);\n    return {\n      path: filePath,\n      size: stats.size,\n      mtime: stats.mtime.toISOString(),\n      id: 'file-exists-' + path.basename(filePath).replace(/\\W+/g, '-'),\n      label: `[FILE EXISTS] ${path.basename(filePath)} \u2014 worm payload detected`,\n      category: 'filesystem',\n      severity: 'critical',\n      match: path.basename(filePath),\n      snippet: `File exists: ${filePath} (${stats.size} bytes)`,\n    };\n  } catch {\n    return null;\n  }\n}\n\n// \u2500\u2500 Process list check \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction checkSuspiciousProcesses(C) {\n  const processHits = [];\n  const suspicious = ['tanstack_runner', 'router_runtime', 'gh-token-monitor'];\n\n  try {\n    const cmd = process.platform === 'win32' ? 'tasklist /FO CSV /NH' : 'ps aux';\n    const output = execSync(cmd, { timeout: 5000, encoding: 'utf8' }).toLowerCase();\n\n    for (const proc of suspicious) {\n      if (output.includes(proc.toLowerCase())) {\n        processHits.push({\n          path: '[process list]',\n          size: 0,\n          mtime: new Date().toISOString(),\n          id: 'process-' + proc.replace(/\\W+/g, '-'),\n          label: `[PROCESS RUNNING] ${proc} \u2014 worm/daemon active`,\n          category: 'persistence',\n          severity: 'critical',\n          match: proc,\n          snippet: `Process \"${proc}\" found in running process list`,\n        });\n      }\n    }\n\n    // Unexpected bun execution (not in standard package manager positions)\n    if (/\\bbun\\b/.test(output) &amp;&amp; !output.includes('bun-as-installer')) {\n      processHits.push({\n        path: '[process list]',\n        size: 0,\n        mtime: new Date().toISOString(),\n        id: 'process-bun-unexpected',\n        label: '[PROCESS RUNNING] bun \u2014 unexpected Bun runtime (evasion technique)',\n        category: 'execution',\n        severity: 'high',\n        match: 'bun',\n        snippet: 'Bun runtime process found running \u2014 may indicate worm preinstall hook activity',\n      });\n    }\n  } catch {\n    // Process list not available \u2014 not a fatal error\n  }\n\n  return processHits;\n}\n\n// \u2500\u2500 Core scan orchestration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction buildScanTargets(opts) {\n  const defaults = getDefaultTargets();\n  const fileTargets = [...defaults.files];\n  const dirTargets = [...defaults.dirs];\n\n  if (opts.root) {\n    const resolvedRoot = path.resolve(opts.root);\n    if (fs.existsSync(resolvedRoot)) {\n      const stats = fs.statSync(resolvedRoot);\n      if (stats.isDirectory()) {\n        dirTargets.push(resolvedRoot);\n      } else if (stats.isFile()) {\n        fileTargets.push(resolvedRoot);\n      }\n    }\n  }\n\n  return { files: fileTargets, dirs: dirTargets };\n}\n\nfunction buildAudit(opts) {\n  const { C, log } = createLogger();\n\n  log.title('MINI SHAI-HULUD AUDIT \u2014 CVE-2026-45321');\n  console.log(`  ${C.red}${C.bold}\u26a0  DO NOT revoke tokens before isolating the machine  \u26a0${C.reset}`);\n  console.log(`  ${C.dim}${ADVISORY_REF}${C.reset}\\n`);\n\n  const targets = buildScanTargets(opts);\n\n  // \u2500\u2500 Step 1: Payload file existence (presence alone = critical) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  log.step('Checking for payload files (existence check)...');\n  const payloadNames = ['setup_bun.js', 'bun_environment.js', 'router_init.js', 'router_runtime.js', 'tanstack_runner.js', 'transformers.pyz'];\n  const payloadExistenceHits = targets.files\n    .filter((f) =&gt; payloadNames.includes(path.basename(f)))\n    .map(checkPayloadFileExists)\n    .filter(Boolean);\n\n  // \u2500\u2500 Step 2: Process list check \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  log.step('Checking running processes...');\n  const processHits = checkSuspiciousProcesses(C);\n\n  // \u2500\u2500 Step 3: Scan known files for IOC patterns \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  log.step('Scanning targeted files for IOC patterns...');\n  const knownFileHits = [];\n  const progress = makeProgress(targets.files.length, C);\n  for (const filePath of targets.files) {\n    progress.tick(path.basename(filePath));\n    knownFileHits.push(...scanFile(filePath));\n  }\n\n  // \u2500\u2500 Step 4: Walk evidence directories \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  log.step('Walking evidence directories...');\n  const walkedFiles = walkFiles(targets.dirs, {\n    maxDepth: Number.isFinite(opts.maxDepth) ? opts.maxDepth : 4,\n    filter: (f) =&gt; isTextLikeEvidence(f, opts),\n  });\n  const walkedHits = [];\n  const walkProgress = makeProgress(walkedFiles.length, C);\n  for (const filePath of walkedFiles) {\n    walkProgress.tick(path.basename(filePath));\n    walkedHits.push(...scanFile(filePath));\n  }\n\n  // \u2500\u2500 Aggregate \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  const allHits = dedupeByKey([...payloadExistenceHits, ...processHits, ...knownFileHits, ...walkedHits], (hit) =&gt; `${hit.path}|${hit.id}|${hit.match}|${hit.snippet}`);\n  const filesWithHits = groupByFile(allHits);\n  const criticalHits = allHits.filter((hit) =&gt; hit.severity === 'critical');\n  const scannedFiles = dedupeByKey([...targets.files, ...walkedFiles], (f) =&gt; f);\n\n  const summary = {\n    targetedFiles: targets.files.length,\n    scannedFiles: scannedFiles.length,\n    filesWithHits: filesWithHits.length,\n    totalHits: allHits.length,\n    criticalHits: criticalHits.length,\n    categories: allHits.reduce((acc, hit) =&gt; {\n      acc[hit.category] = (acc[hit.category] ?? 0) + 1;\n      return acc;\n    }, {}),\n  };\n\n  return {\n    C,\n    log,\n    summary,\n    filesWithHits,\n    criticalHits,\n    report: {\n      auditDate: new Date().toISOString(),\n      advisory: ADVISORY_REF,\n      scope: {\n        root: opts.root ? path.resolve(opts.root) : null,\n        host: os.hostname(),\n        platform: process.platform,\n      },\n      summary,\n      findings: allHits,\n      filesWithHits,\n    },\n  };\n}\n\n// \u2500\u2500 CSV serialisation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction buildCsvRows(result) {\n  const { summary, report, filesWithHits } = result;\n  const rows = [\n    {\n      recordType: 'summary',\n      auditDate: report.auditDate,\n      host: report.scope.host,\n      platform: report.scope.platform,\n      root: report.scope.root,\n      checkType: ADVISORY_REF,\n      targetedFiles: summary.targetedFiles,\n      scannedFiles: summary.scannedFiles,\n      filesWithHits: summary.filesWithHits,\n      totalHits: summary.totalHits,\n      criticalHits: summary.criticalHits,\n      categories: JSON.stringify(summary.categories),\n    },\n  ];\n\n  for (const file of filesWithHits) {\n    for (const hit of file.hits) {\n      rows.push({\n        recordType: 'finding',\n        auditDate: report.auditDate,\n        host: report.scope.host,\n        platform: report.scope.platform,\n        root: report.scope.root,\n        checkType: ADVISORY_REF,\n        path: file.path,\n        size: hit.size,\n        mtime: hit.mtime,\n        id: hit.id,\n        label: hit.label,\n        category: hit.category,\n        severity: hit.severity,\n        match: hit.match,\n        snippet: hit.snippet,\n      });\n    }\n  }\n\n  return rows;\n}\n\n// \u2500\u2500 Terminal report \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction printReport(result) {\n  const { C, log, summary, filesWithHits } = result;\n\n  log.step('Results:');\n\n  if (summary.criticalHits === 0) {\n    log.ok(\n      'CLEAR \u2014 no critical Mini Shai-Hulud IOCs detected.\\n' + '  Checked: payload files, persistence artefacts, C2 domains, package versions,\\n' + '           distinctive strings, daemon processes.',\n    );\n    console.log(`\\n  \ud83d\udcc4 ${result.reportPath}  (${summary.scannedFiles} files scanned)\\n`);\n    return;\n  }\n\n  // \u2500\u2500 Critical IOCs found \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  const criticalFiles = filesWithHits.filter((f) =&gt; f.hits.some((h) =&gt; h.severity === 'critical'));\n\n  console.log(`\\n${C.red}${C.bold}\u2501\u2501\u2501 CRITICAL IOCs DETECTED \u2014 Mini Shai-Hulud \u2501\u2501\u2501${C.reset}`);\n  console.log(`${C.red}${C.bold}  CVE-2026-45321 / GHSA-g7cv-rxg3-hmpx \u2014 CVSS 9.6${C.reset}\\n`);\n\n  for (const file of criticalFiles) {\n    console.log(`  ${C.bold}${file.path}${C.reset}`);\n    for (const hit of file.hits.filter((h) =&gt; h.severity === 'critical')) {\n      console.log(`    ${C.red}${hit.label}${C.reset}  ${C.dim}(${hit.category})${C.reset}`);\n      if (hit.snippet) {\n        console.log(`    ${C.dim}${hit.snippet}${C.reset}`);\n      }\n    }\n    console.log('');\n  }\n\n  console.log(`${C.red}${C.bold}\u2501\u2501\u2501 INCIDENT RESPONSE \u2014 MANDATORY ORDER \u2501\u2501\u2501${C.reset}`);\n  console.log(`\\n  ${C.red}${C.bold}\u26a0  DO NOT revoke tokens before step 3  \u26a0${C.reset}`);\n  console.log(`  ${C.dim}The worm watchdog triggers rm -rf ~/ on token revocation (HTTP 40X).${C.reset}\\n`);\n  console.log(`  ${C.red}1.${C.reset} ISOLATE  \u2014 disconnect machine from network immediately`);\n  console.log(`  ${C.red}2.${C.reset} IMAGE    \u2014 forensic disk image before any cleanup`);\n  console.log(`  ${C.red}3.${C.reset} KILL     \u2014 disable gh-token-monitor daemon and worm processes`);\n  console.log(`  ${C.red}4.${C.reset} REVOKE   \u2014 npm, GitHub PAT/OAuth, AWS, Azure, GCP, Kubernetes, SSH`);\n  console.log(`  ${C.red}5.${C.reset} ROTATE   \u2014 all credentials reachable from this host`);\n  console.log(`  ${C.red}6.${C.reset} AUDIT    \u2014 AWS CloudTrail, Azure Activity Logs, GCP Audit Logs`);\n  console.log(`  ${C.red}7.${C.reset} VERIFY   \u2014 GitHub account: recent Dune-themed public repos, new PATs\\n`);\n  console.log(`  \ud83d\udcc4 ${result.reportPath}  (full execution trace inside)\\n`);\n}\n\n// \u2500\u2500 Entry point \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nasync function main() {\n  const opts = parseArgs(process.argv);\n\n  if (opts.help) {\n    showHelp();\n    process.exit(0);\n  }\n\n  const result = buildAudit(opts);\n  result.reportPath = opts.output;\n  writeReportFile(opts.output, result.report, buildCsvRows(result), CSV_COLUMNS);\n  printReport(result);\n}\n\nmain().catch((err) =&gt; {\n  const { log } = createLogger();\n  log.alert(`Fatal: ${err.message}`);\n  process.exit(1);\n});\n", "creation_timestamp": "2026-05-18T20:22:18.000000Z"}