{"vulnerability": "ghsa-g7cv-rxg3-hmpx", "sightings": [{"uuid": "d9dc4501-ea66-4b88-9133-d597e1e1be4f", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "GHSA-g7cv-rxg3-hmpx", "type": "seen", "source": "https://gist.github.com/nrajlekhak/84bda8dc75e9212c7699a79bb34fa4dc", "content": "#!/usr/bin/env bash\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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# detect-mini-shai-hulud.sh\n#\n# Scans every project under the CWD for npm packages compromised in the\n# \"Mini Shai-Hulud\" supply-chain attack (npm ecosystem, 2026).\n#\n# Compromised package list source: StepSecurity blog\n#   https://www.stepsecurity.io/blog/mini-shai-hulud-is-back-a-self-spreading-supply-chain-attack-hits-the-npm-ecosystem\n# @tanstack/* patched versions cross-verified against GitHub Security Advisory:\n#   https://github.com/TanStack/router/security/advisories/GHSA-g7cv-rxg3-hmpx\n#\n# Usage:\n#   cd ~/projects\n#   bash detect-mini-shai-hulud.sh\n#\n# Scans: package-lock.json, yarn.lock, pnpm-lock.yaml  (skips node_modules)\n# Note:  bun.lockb is binary \u2014 for Bun projects:  bun pm ls | less\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\nset -euo pipefail\n\n# \u2500\u2500\u2500 Compromised packages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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# Format: \"|[,,...]|\"\n# Add new entries here as advisories drop.\nVULN_LIST=(\n  \"@uipath/docsai-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/packager-tool-apiworkflow|0.0.19|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/packager-tool-workflowcompiler-browser|0.0.34|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/packager-tool-functions|0.1.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/agent.sdk|0.0.18|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/filesystem|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/admin-tool|0.1.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/llmgw-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tanstack/arktype-adapter|1.166.12,1.166.15|upgrade to 1.166.16\"\n  \"@tanstack/eslint-plugin-router|1.161.9,1.161.12|upgrade to 1.161.13\"\n  \"@tanstack/eslint-plugin-start|0.0.4,0.0.7|upgrade to 0.0.8\"\n  \"@tanstack/history|1.161.9,1.161.12|upgrade to 1.161.13\"\n  \"@tanstack/nitro-v2-vite-plugin|1.154.12,1.154.15|upgrade to 1.154.16\"\n  \"@tanstack/react-router|1.169.5,1.169.8|upgrade to 1.169.9\"\n  \"@tanstack/react-router-devtools|1.166.16,1.166.19|upgrade to 1.166.20\"\n  \"@tanstack/react-router-ssr-query|1.166.15,1.166.18|upgrade to 1.166.19\"\n  \"@tanstack/react-start|1.167.68,1.167.71|upgrade to 1.167.72\"\n  \"@tanstack/react-start-client|1.166.51,1.166.54|upgrade to 1.166.55\"\n  \"@tanstack/react-start-rsc|0.0.47,0.0.50|upgrade to 0.0.51\"\n  \"@tanstack/react-start-server|1.166.55,1.166.58|upgrade to 1.166.59\"\n  \"@tanstack/router-cli|1.166.46,1.166.49|upgrade to 1.166.50\"\n  \"@tanstack/router-core|1.169.5,1.169.8|upgrade to 1.169.9\"\n  \"@tanstack/router-devtools|1.166.16,1.166.19|upgrade to 1.166.20\"\n  \"@tanstack/router-devtools-core|1.167.6,1.167.9|upgrade to 1.167.10\"\n  \"@tanstack/router-generator|1.166.45,1.166.48|upgrade to 1.166.49\"\n  \"@tanstack/router-plugin|1.167.38,1.167.41|upgrade to 1.167.42\"\n  \"@tanstack/router-ssr-query-core|1.168.3,1.168.6|upgrade to 1.168.7\"\n  \"@tanstack/router-utils|1.161.11,1.161.14|upgrade to 1.161.15\"\n  \"@tanstack/router-vite-plugin|1.166.53,1.166.56|upgrade to 1.166.57\"\n  \"@tanstack/solid-router|1.169.5,1.169.8|upgrade to 1.169.9\"\n  \"@tanstack/solid-router-devtools|1.166.16,1.166.19|upgrade to 1.166.20\"\n  \"@tanstack/solid-router-ssr-query|1.166.15,1.166.18|upgrade to 1.166.19\"\n  \"@tanstack/solid-start|1.167.65,1.167.68|upgrade to 1.167.69\"\n  \"@tanstack/solid-start-client|1.166.50,1.166.53|upgrade to 1.166.54\"\n  \"@tanstack/solid-start-server|1.166.54,1.166.57|upgrade to 1.166.58\"\n  \"@tanstack/start-client-core|1.168.5,1.168.8|upgrade to 1.168.9\"\n  \"@tanstack/start-fn-stubs|1.161.9,1.161.12|upgrade to 1.161.13\"\n  \"@tanstack/start-plugin-core|1.169.23,1.169.26|upgrade to 1.169.27\"\n  \"@tanstack/start-server-core|1.167.33,1.167.36|upgrade to 1.167.37\"\n  \"@tanstack/start-static-server-functions|1.166.44,1.166.47|upgrade to 1.166.48\"\n  \"@tanstack/start-storage-context|1.166.38,1.166.41|upgrade to 1.166.42\"\n  \"@tanstack/valibot-adapter|1.166.12,1.166.15|upgrade to 1.166.16\"\n  \"@tanstack/virtual-file-routes|1.161.10,1.161.13|upgrade to 1.161.14\"\n  \"@tanstack/vue-router|1.169.5,1.169.8|upgrade to 1.169.9\"\n  \"@tanstack/vue-router-devtools|1.166.16,1.166.19|upgrade to 1.166.20\"\n  \"@tanstack/vue-router-ssr-query|1.166.15,1.166.18|upgrade to 1.166.19\"\n  \"@tanstack/vue-start|1.167.61,1.167.64|upgrade to 1.167.65\"\n  \"@tanstack/vue-start-client|1.166.46,1.166.49|upgrade to 1.166.50\"\n  \"@tanstack/vue-start-server|1.166.50,1.166.53|upgrade to 1.166.54\"\n  \"@tanstack/zod-adapter|1.166.12,1.166.15|upgrade to 1.166.16\"\n  \"@draftauth/client|0.2.1,0.2.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@draftauth/core|0.13.1,0.13.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@draftlab/auth|0.24.1,0.24.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@draftlab/auth-router|0.5.1,0.5.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@draftlab/db|0.16.1,0.16.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@taskflow-corp/cli|0.1.24,0.1.25,0.1.26,0.1.27,0.1.28,0.1.29|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tolka/cli|1.0.2,1.0.3,1.0.4,1.0.6|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/access-policy-sdk|0.3.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/access-policy-tool|0.3.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/agent-sdk|1.0.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/agent-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/aops-policy-tool|0.3.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/ap-chat|1.5.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/api-workflow-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/apollo-core|5.9.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/apollo-react|4.24.5|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/apollo-wind|2.16.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/auth|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/case-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/cli|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/codedagent-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/codedagents-tool|0.1.12|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/codedapp-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/common|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/context-grounding-tool|0.1.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/data-fabric-tool|1.0.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/flow-tool|1.0.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/functions-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/gov-tool|0.3.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/identity-tool|0.1.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/insights-sdk|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/insights-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/integrationservice-sdk|1.0.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/integrationservice-tool|1.0.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/maestro-sdk|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/maestro-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/orchestrator-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/packager-tool-bpmn|0.0.9|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/packager-tool-case|0.0.9|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/packager-tool-connector|0.0.19|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/packager-tool-flow|0.0.19|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/packager-tool-webapp|1.0.6|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/packager-tool-workflowcompiler|0.0.16|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/platform-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/project-packager|1.1.16|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/resource-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/resourcecatalog-tool|0.1.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/resources-tool|0.1.11|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/robot|1.3.4|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/rpa-legacy-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/rpa-tool|0.9.5|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/solution-packager|0.0.35|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/solution-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/solutionpackager-sdk|1.0.11|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/solutionpackager-tool-core|0.0.34|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/tasks-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/telemetry|0.0.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/test-manager-tool|1.0.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/tool-workflowcompiler|0.0.12|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/traces-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/ui-widgets-multi-file-upload|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/uipath-python-bridge|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/vertical-solutions-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/vss|0.1.6|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/widget.sdk|1.2.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"safe-action|0.8.3,0.8.4|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@supersurkhet/cli|0.0.2,0.0.3,0.0.4,0.0.5,0.0.6,0.0.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@supersurkhet/sdk|0.0.2,0.0.3,0.0.4,0.0.5,0.0.6,0.0.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"cmux-agent-mcp|0.1.3,0.1.4,0.1.5,0.1.6,0.1.7,0.1.8|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"git-git-git|1.0.8,1.0.9,1.0.10,1.0.12|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"git-branch-selector|1.3.3,1.3.4,1.3.5,1.3.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"nextmove-mcp|0.1.3,0.1.4,0.1.5,0.1.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@beproduct/nestjs-auth|0.1.2,0.1.3,0.1.4,0.1.5,0.1.6,0.1.7,0.1.8,0.1.9,0.1.10,0.1.11,0.1.12,0.1.13,0.1.14,0.1.15,0.1.16,0.1.17,0.1.19|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@dirigible-ai/sdk|0.6.2,0.6.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@ml-toolkit-ts/preprocessing|1.0.2,1.0.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@ml-toolkit-ts/xgboost|1.0.3,1.0.4|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"agentwork-cli|0.1.4,0.1.5|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"ml-toolkit-ts|1.0.4,1.0.5|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/airport-data|0.7.4,0.7.5,0.7.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/airports|0.6.2,0.6.3,0.6.5|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/airspace|0.8.1,0.8.2,0.8.4|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/airspace-data|0.5.3,0.5.4,0.5.6|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/airway-data|0.5.4,0.5.5,0.5.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/airways|0.4.2,0.4.3,0.4.5|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/fix-data|0.6.4,0.6.5,0.6.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/fixes|0.3.2,0.3.3,0.3.5|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/flight-math|0.5.4,0.5.5,0.5.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/flightplan|0.5.2,0.5.3,0.5.5|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/geo|0.4.4,0.4.5,0.4.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/icao-registry|0.5.2,0.5.3,0.5.5|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/icao-registry-data|0.8.4,0.8.5,0.8.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/mcp|0.9.1,0.9.2,0.9.4|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/navaid-data|0.6.4,0.6.5,0.6.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/navaids|0.4.2,0.4.3,0.4.5|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/notams|0.3.6,0.3.7,0.3.9|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/procedure-data|0.7.3,0.7.4,0.7.6|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/procedures|0.5.2,0.5.3,0.5.5|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/types|0.8.1,0.8.2,0.8.4|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/units|0.4.3,0.4.4,0.4.6|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/weather|0.5.6,0.5.7,0.5.9|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"wot-api|0.8.1,0.8.2,0.8.4|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"cross-stitch|1.1.3,1.1.4,1.1.6|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"ts-dna|3.0.1,3.0.2,3.0.4|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tallyui/components|1.0.1,1.0.2,1.0.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tallyui/connector-medusa|1.0.1,1.0.2,1.0.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tallyui/connector-shopify|1.0.1,1.0.2,1.0.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tallyui/connector-vendure|1.0.1,1.0.2,1.0.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tallyui/connector-woocommerce|1.0.1,1.0.2,1.0.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tallyui/core|0.2.1,0.2.2,0.2.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tallyui/database|1.0.1,1.0.2,1.0.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tallyui/pos|0.1.1,0.1.2,0.1.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tallyui/storage-sqlite|0.2.1,0.2.2,0.2.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tallyui/theme|0.2.1,0.2.2,0.2.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@mesadev/rest|0.28.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@mesadev/saguaro|0.4.22|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@mesadev/sdk|0.28.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@mistralai/mistralai|2.2.3,2.2.4|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@mistralai/mistralai-azure|1.7.2,1.7.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@mistralai/mistralai-gcp|1.7.2,1.7.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n)\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\n# Serialize VULN_LIST to JSON for the per-lockfile Python parser\nVULN_RAW=$(printf '%s\\n' \"${VULN_LIST[@]}\")\nexport VULN_RAW\nVULN_JSON=$(python3 -c '\nimport json, os\nout = {}\nfor line in os.environ[\"VULN_RAW\"].strip().split(\"\\n\"):\n    pkg, bad_csv, fix = line.split(\"|\", 2)\n    out[pkg] = {\"bad\": bad_csv.split(\",\"), \"fix\": fix}\nprint(json.dumps(out))\n')\nexport VULN_JSON\n\nNUM_PKGS=${#VULN_LIST[@]}\n\nLOCKFILES=()\nwhile IFS= read -r line; do\n  LOCKFILES+=(\"$line\")\ndone &lt; &lt;(find . \\( -name package-lock.json -o -name yarn.lock -o -name pnpm-lock.yaml \\) -not -path '*/node_modules/*' 2&gt;/dev/null | sort)\n\nif [[ ${#LOCKFILES[@]} -eq 0 ]]; then\n  echo \"No lock files found under $(pwd)\"\n  exit 0\nfi\n\necho \"Scanning ${#LOCKFILES[@]} lock file(s) for $NUM_PKGS compromised packages (Mini Shai-Hulud campaign)...\"\necho\n\nTOTAL_HITS=0\nAFFECTED_PROJECTS=()\n\nfor lock in \"${LOCKFILES[@]}\"; do\n  project=$(dirname \"$lock\" | sed 's|^\\./||')\n  base=$(basename \"$lock\")\n\n  hits=$(LOCK_PATH=\"$lock\" LOCK_TYPE=\"$base\" python3 - &lt;&lt;'PY'\nimport json, os, re, sys\n\nvuln = json.loads(os.environ['VULN_JSON'])\npath = os.environ['LOCK_PATH']\nkind = os.environ['LOCK_TYPE']\nfound = []  # list of (pkg, version, fix)\n\ndef check(pkg, ver):\n    if pkg in vuln and ver in vuln[pkg]['bad']:\n        found.append((pkg, ver, vuln[pkg]['fix']))\n\n# Pre-built alternation of all watched package names for fast scanning\npkg_re_alt = '|'.join(re.escape(p) for p in vuln.keys())\n\ntry:\n    with open(path, 'r', encoding='utf-8', errors='replace') as f:\n        content = f.read()\nexcept Exception as e:\n    print(f\"ERROR_READING:{e}\", file=sys.stderr)\n    sys.exit(0)\n\nif kind == 'package-lock.json':\n    try:\n        d = json.loads(content)\n    except json.JSONDecodeError:\n        sys.exit(0)\n    # npm v7+ \"packages\" map\n    pkg_suffix_re = re.compile(r'node_modules/(' + pkg_re_alt + r')$')\n    for key, meta in (d.get('packages') or {}).items():\n        m = pkg_suffix_re.search(key)\n        if m:\n            check(m.group(1), meta.get('version', ''))\n    # legacy \"dependencies\" tree (npm v6)\n    def walk(deps):\n        for name, meta in (deps or {}).items():\n            if name in vuln:\n                check(name, meta.get('version', ''))\n            walk(meta.get('dependencies'))\n    walk(d.get('dependencies'))\n\nelif kind == 'yarn.lock':\n    # Yarn v1 / Berry blocks\n    blocks = re.split(r'\\n(?=[^\\s])', content)\n    header_pkg_re = re.compile(r'\"?(' + pkg_re_alt + r')@')\n    ver_line_re   = re.compile(r'^\\s+version\\s+\"([^\"]+)\"', re.M)\n    for block in blocks:\n        first = block.split('\\n', 1)[0]\n        pkgs_in_header = header_pkg_re.findall(first)\n        if not pkgs_in_header:\n            continue\n        ver_match = ver_line_re.search(block)\n        if not ver_match:\n            continue\n        version = ver_match.group(1)\n        for pkg in set(pkgs_in_header):\n            check(pkg, version)\n\nelif kind == 'pnpm-lock.yaml':\n    # pnpm: every package@version reference is captured\n    occ_re = re.compile(r'(' + pkg_re_alt + r')@([0-9][0-9a-zA-Z.+-]*)')\n    for m in occ_re.finditer(content):\n        check(m.group(1), m.group(2))\n\nseen = set()\nfor pkg, ver, fix in found:\n    key = (pkg, ver)\n    if key in seen: continue\n    seen.add(key)\n    print(f\"{pkg}|{ver}|{fix}\")\nPY\n)\n\n  if [[ -n \"$hits\" ]]; then\n    AFFECTED_PROJECTS+=(\"$project\")\n    echo \"\u274c $project  ($base)\"\n    while IFS='|' read -r pkg ver fix; do\n      echo \"      $pkg@$ver   \u2192  $fix\"\n      TOTAL_HITS=$((TOTAL_HITS + 1))\n    done &lt;&lt;&lt; \"$hits\"\n    echo\n  fi\ndone\n\necho \"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\"\nif [[ $TOTAL_HITS -eq 0 ]]; then\n  echo \"\u2705 No compromised packages found in ${#LOCKFILES[@]} lock file(s).\"\nelse\n  uniq_projects=$(printf '%s\\n' \"${AFFECTED_PROJECTS[@]}\" | sort -u | wc -l | tr -d ' ')\n  echo \"\u274c Found $TOTAL_HITS compromised dependency entries across $uniq_projects project(s).\"\n  echo\n  echo \"Remediation:\"\n  echo \"  1. Upgrade/remove each affected package per the message above.\"\n  echo \"  2. Delete node_modules and the lock file, then reinstall.\"\n  echo \"  3. Rotate any secrets accessible to your dev/build/CI environment\"\n  echo \"     (the worm exfiltrates env vars, .env files, cloud/GitHub/npm tokens, SSH keys).\"\n  echo \"  4. Audit CI logs for outbound calls to attacker-controlled GitHub repos.\"\nfi\n", "creation_timestamp": "2026-05-12T04:52:42.000000Z"}, {"uuid": "dac12874-baec-456c-8149-12e040ed3e34", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "GHSA-g7cv-rxg3-hmpx", "type": "seen", "source": "https://gist.github.com/maskeynihal/e780ed1e48c56592fc6612591a4bd420", "content": "#!/usr/bin/env bash\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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# detect-mini-shai-hulud.sh\n#\n# Scans every git project under the CWD across ALL branches (local + already\n# fetched remote-tracking branches) for npm packages compromised in the\n# \"Mini Shai-Hulud\" supply-chain attack (npm ecosystem, 2026).\n#\n# For every watched package found in any lockfile on any branch, the script\n# reports the project, branch, package, version, and whether that version is\n# in the known-bad set.\n#\n# Compromised package list source: StepSecurity blog\n#   https://www.stepsecurity.io/blog/mini-shai-hulud-is-back-a-self-spreading-supply-chain-attack-hits-the-npm-ecosystem\n# @tanstack/* patched versions cross-verified against GitHub Security Advisory:\n#   https://github.com/TanStack/router/security/advisories/GHSA-g7cv-rxg3-hmpx\n#\n# Usage:\n#   cd ~/projects\n#   bash detect-mini-shai-hulud.sh           # scan all repos under CWD\n#   bash detect-mini-shai-hulud.sh /some/dir # scan repos under /some/dir\n#\n# Scans: package-lock.json, yarn.lock, pnpm-lock.yaml in tracked tree of every\n#        branch (skips node_modules; never checks anything out).\n# Note:  bun.lockb is binary \u2014 for Bun projects:  bun pm ls | less\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\nset -euo pipefail\n\n# \u2500\u2500\u2500 Compromised packages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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# Format: \"|[,,...]|\"\n# Add new entries here as advisories drop.\nVULN_LIST=(\n  \"@uipath/docsai-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/packager-tool-apiworkflow|0.0.19|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/packager-tool-workflowcompiler-browser|0.0.34|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/packager-tool-functions|0.1.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/agent.sdk|0.0.18|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/filesystem|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/admin-tool|0.1.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/llmgw-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tanstack/arktype-adapter|1.166.12,1.166.15|upgrade to 1.166.16\"\n  \"@tanstack/eslint-plugin-router|1.161.9,1.161.12|upgrade to 1.161.13\"\n  \"@tanstack/eslint-plugin-start|0.0.4,0.0.7|upgrade to 0.0.8\"\n  \"@tanstack/history|1.161.9,1.161.12|upgrade to 1.161.13\"\n  \"@tanstack/nitro-v2-vite-plugin|1.154.12,1.154.15|upgrade to 1.154.16\"\n  \"@tanstack/react-router|1.169.5,1.169.8|upgrade to 1.169.9\"\n  \"@tanstack/react-router-devtools|1.166.16,1.166.19|upgrade to 1.166.20\"\n  \"@tanstack/react-router-ssr-query|1.166.15,1.166.18|upgrade to 1.166.19\"\n  \"@tanstack/react-start|1.167.68,1.167.71|upgrade to 1.167.72\"\n  \"@tanstack/react-start-client|1.166.51,1.166.54|upgrade to 1.166.55\"\n  \"@tanstack/react-start-rsc|0.0.47,0.0.50|upgrade to 0.0.51\"\n  \"@tanstack/react-start-server|1.166.55,1.166.58|upgrade to 1.166.59\"\n  \"@tanstack/router-cli|1.166.46,1.166.49|upgrade to 1.166.50\"\n  \"@tanstack/router-core|1.169.5,1.169.8|upgrade to 1.169.9\"\n  \"@tanstack/router-devtools|1.166.16,1.166.19|upgrade to 1.166.20\"\n  \"@tanstack/router-devtools-core|1.167.6,1.167.9|upgrade to 1.167.10\"\n  \"@tanstack/router-generator|1.166.45,1.166.48|upgrade to 1.166.49\"\n  \"@tanstack/router-plugin|1.167.38,1.167.41|upgrade to 1.167.42\"\n  \"@tanstack/router-ssr-query-core|1.168.3,1.168.6|upgrade to 1.168.7\"\n  \"@tanstack/router-utils|1.161.11,1.161.14|upgrade to 1.161.15\"\n  \"@tanstack/router-vite-plugin|1.166.53,1.166.56|upgrade to 1.166.57\"\n  \"@tanstack/solid-router|1.169.5,1.169.8|upgrade to 1.169.9\"\n  \"@tanstack/solid-router-devtools|1.166.16,1.166.19|upgrade to 1.166.20\"\n  \"@tanstack/solid-router-ssr-query|1.166.15,1.166.18|upgrade to 1.166.19\"\n  \"@tanstack/solid-start|1.167.65,1.167.68|upgrade to 1.167.69\"\n  \"@tanstack/solid-start-client|1.166.50,1.166.53|upgrade to 1.166.54\"\n  \"@tanstack/solid-start-server|1.166.54,1.166.57|upgrade to 1.166.58\"\n  \"@tanstack/start-client-core|1.168.5,1.168.8|upgrade to 1.168.9\"\n  \"@tanstack/start-fn-stubs|1.161.9,1.161.12|upgrade to 1.161.13\"\n  \"@tanstack/start-plugin-core|1.169.23,1.169.26|upgrade to 1.169.27\"\n  \"@tanstack/start-server-core|1.167.33,1.167.36|upgrade to 1.167.37\"\n  \"@tanstack/start-static-server-functions|1.166.44,1.166.47|upgrade to 1.166.48\"\n  \"@tanstack/start-storage-context|1.166.38,1.166.41|upgrade to 1.166.42\"\n  \"@tanstack/valibot-adapter|1.166.12,1.166.15|upgrade to 1.166.16\"\n  \"@tanstack/virtual-file-routes|1.161.10,1.161.13|upgrade to 1.161.14\"\n  \"@tanstack/vue-router|1.169.5,1.169.8|upgrade to 1.169.9\"\n  \"@tanstack/vue-router-devtools|1.166.16,1.166.19|upgrade to 1.166.20\"\n  \"@tanstack/vue-router-ssr-query|1.166.15,1.166.18|upgrade to 1.166.19\"\n  \"@tanstack/vue-start|1.167.61,1.167.64|upgrade to 1.167.65\"\n  \"@tanstack/vue-start-client|1.166.46,1.166.49|upgrade to 1.166.50\"\n  \"@tanstack/vue-start-server|1.166.50,1.166.53|upgrade to 1.166.54\"\n  \"@tanstack/zod-adapter|1.166.12,1.166.15|upgrade to 1.166.16\"\n  \"@draftauth/client|0.2.1,0.2.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@draftauth/core|0.13.1,0.13.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@draftlab/auth|0.24.1,0.24.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@draftlab/auth-router|0.5.1,0.5.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@draftlab/db|0.16.1,0.16.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@taskflow-corp/cli|0.1.24,0.1.25,0.1.26,0.1.27,0.1.28,0.1.29|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tolka/cli|1.0.2,1.0.3,1.0.4,1.0.6|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/access-policy-sdk|0.3.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/access-policy-tool|0.3.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/agent-sdk|1.0.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/agent-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/aops-policy-tool|0.3.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/ap-chat|1.5.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/api-workflow-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/apollo-core|5.9.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/apollo-react|4.24.5|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/apollo-wind|2.16.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/auth|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/case-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/cli|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/codedagent-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/codedagents-tool|0.1.12|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/codedapp-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/common|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/context-grounding-tool|0.1.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/data-fabric-tool|1.0.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/flow-tool|1.0.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/functions-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/gov-tool|0.3.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/identity-tool|0.1.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/insights-sdk|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/insights-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/integrationservice-sdk|1.0.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/integrationservice-tool|1.0.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/maestro-sdk|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/maestro-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/orchestrator-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/packager-tool-bpmn|0.0.9|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/packager-tool-case|0.0.9|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/packager-tool-connector|0.0.19|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/packager-tool-flow|0.0.19|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/packager-tool-webapp|1.0.6|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/packager-tool-workflowcompiler|0.0.16|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/platform-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/project-packager|1.1.16|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/resource-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/resourcecatalog-tool|0.1.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/resources-tool|0.1.11|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/robot|1.3.4|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/rpa-legacy-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/rpa-tool|0.9.5|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/solution-packager|0.0.35|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/solution-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/solutionpackager-sdk|1.0.11|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/solutionpackager-tool-core|0.0.34|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/tasks-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/telemetry|0.0.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/test-manager-tool|1.0.2|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/tool-workflowcompiler|0.0.12|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/traces-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/ui-widgets-multi-file-upload|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/uipath-python-bridge|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/vertical-solutions-tool|1.0.1|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/vss|0.1.6|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@uipath/widget.sdk|1.2.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"safe-action|0.8.3,0.8.4|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@supersurkhet/cli|0.0.2,0.0.3,0.0.4,0.0.5,0.0.6,0.0.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@supersurkhet/sdk|0.0.2,0.0.3,0.0.4,0.0.5,0.0.6,0.0.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"cmux-agent-mcp|0.1.3,0.1.4,0.1.5,0.1.6,0.1.7,0.1.8|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"git-git-git|1.0.8,1.0.9,1.0.10,1.0.12|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"git-branch-selector|1.3.3,1.3.4,1.3.5,1.3.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"nextmove-mcp|0.1.3,0.1.4,0.1.5,0.1.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@beproduct/nestjs-auth|0.1.2,0.1.3,0.1.4,0.1.5,0.1.6,0.1.7,0.1.8,0.1.9,0.1.10,0.1.11,0.1.12,0.1.13,0.1.14,0.1.15,0.1.16,0.1.17,0.1.19|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@dirigible-ai/sdk|0.6.2,0.6.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@ml-toolkit-ts/preprocessing|1.0.2,1.0.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@ml-toolkit-ts/xgboost|1.0.3,1.0.4|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"agentwork-cli|0.1.4,0.1.5|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"ml-toolkit-ts|1.0.4,1.0.5|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/airport-data|0.7.4,0.7.5,0.7.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/airports|0.6.2,0.6.3,0.6.5|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/airspace|0.8.1,0.8.2,0.8.4|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/airspace-data|0.5.3,0.5.4,0.5.6|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/airway-data|0.5.4,0.5.5,0.5.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/airways|0.4.2,0.4.3,0.4.5|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/fix-data|0.6.4,0.6.5,0.6.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/fixes|0.3.2,0.3.3,0.3.5|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/flight-math|0.5.4,0.5.5,0.5.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/flightplan|0.5.2,0.5.3,0.5.5|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/geo|0.4.4,0.4.5,0.4.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/icao-registry|0.5.2,0.5.3,0.5.5|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/icao-registry-data|0.8.4,0.8.5,0.8.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/mcp|0.9.1,0.9.2,0.9.4|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/navaid-data|0.6.4,0.6.5,0.6.7|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/navaids|0.4.2,0.4.3,0.4.5|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/notams|0.3.6,0.3.7,0.3.9|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/procedure-data|0.7.3,0.7.4,0.7.6|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/procedures|0.5.2,0.5.3,0.5.5|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/types|0.8.1,0.8.2,0.8.4|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/units|0.4.3,0.4.4,0.4.6|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@squawk/weather|0.5.6,0.5.7,0.5.9|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"wot-api|0.8.1,0.8.2,0.8.4|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"cross-stitch|1.1.3,1.1.4,1.1.6|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"ts-dna|3.0.1,3.0.2,3.0.4|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tallyui/components|1.0.1,1.0.2,1.0.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tallyui/connector-medusa|1.0.1,1.0.2,1.0.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tallyui/connector-shopify|1.0.1,1.0.2,1.0.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tallyui/connector-vendure|1.0.1,1.0.2,1.0.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tallyui/connector-woocommerce|1.0.1,1.0.2,1.0.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tallyui/core|0.2.1,0.2.2,0.2.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tallyui/database|1.0.1,1.0.2,1.0.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tallyui/pos|0.1.1,0.1.2,0.1.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tallyui/storage-sqlite|0.2.1,0.2.2,0.2.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@tallyui/theme|0.2.1,0.2.2,0.2.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@mesadev/rest|0.28.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@mesadev/saguaro|0.4.22|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@mesadev/sdk|0.28.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@mistralai/mistralai|2.2.3,2.2.4|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@mistralai/mistralai-azure|1.7.2,1.7.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n  \"@mistralai/mistralai-gcp|1.7.2,1.7.3|remove/replace \u2014 no known clean patched version; rotate any creds touched at install\"\n)\n# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\nVULN_RAW=$(printf '%s\\n' \"${VULN_LIST[@]}\")\nexport VULN_RAW\nexport SCAN_ROOT=\"${1:-$(pwd)}\"\n\npython3 &lt;&lt;'PYEND'\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nfrom pathlib import Path\n\nraw = os.environ[\"VULN_RAW\"]\nvuln = {}\nfor line in raw.strip().split(\"\\n\"):\n    pkg, bad_csv, fix = line.split(\"|\", 2)\n    vuln[pkg] = {\"bad\": set(bad_csv.split(\",\")), \"fix\": fix}\n\nwatch = set(vuln.keys())\npkg_re_alt = \"|\".join(re.escape(p) for p in watch)\n\nroot = Path(os.environ[\"SCAN_ROOT\"]).resolve()\n\nSKIP_DIRS = {\n    \"node_modules\", \".next\", \"dist\", \"build\", \".venv\", \"venv\",\n    \"__pycache__\", \".turbo\", \".cache\", \".pnpm-store\", \"target\",\n}\n\ndef discover_repos(root: Path):\n    repos = []\n    for dirpath, dirnames, filenames in os.walk(root, followlinks=False):\n        if \".git\" in dirnames or \".git\" in filenames:\n            repos.append(Path(dirpath))\n            dirnames.clear()\n            continue\n        dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS and not d.startswith(\".\")]\n    return sorted(repos)\n\nLOCK_RE = re.compile(r\"(?:^|/)(package-lock\\.json|yarn\\.lock|pnpm-lock\\.yaml)$\")\n\ndef run(args, cwd=None, binary=False):\n    return subprocess.run(\n        args,\n        cwd=cwd,\n        capture_output=True,\n        text=not binary,\n        errors=\"replace\" if not binary else None,\n    )\n\ndef list_branches(repo: Path):\n    r = run(\n        [\n            \"git\", \"for-each-ref\",\n            \"--format=%(refname)|%(refname:short)\",\n            \"refs/heads\", \"refs/remotes\",\n        ],\n        cwd=repo,\n    )\n    if r.returncode != 0:\n        return []\n    out = []\n    for line in r.stdout.strip().split(\"\\n\"):\n        if not line:\n            continue\n        full, short = line.split(\"|\", 1)\n        if full.startswith(\"refs/remotes/\") and full.endswith(\"/HEAD\"):\n            continue\n        out.append((full, short))\n    return out\n\ndef list_lockfiles_with_sha(repo: Path, ref: str):\n    r = run([\"git\", \"ls-tree\", \"-r\", ref], cwd=repo)\n    if r.returncode != 0:\n        return []\n    out = []\n    for line in r.stdout.split(\"\\n\"):\n        if not line:\n            continue\n        try:\n            meta, name = line.split(\"\\t\", 1)\n        except ValueError:\n            continue\n        parts = meta.split(\" \")\n        if len(parts) != 3:\n            continue\n        _, typ, sha = parts\n        if typ != \"blob\":\n            continue\n        if \"/node_modules/\" in name or name.startswith(\"node_modules/\"):\n            continue\n        if LOCK_RE.search(name):\n            out.append((name, sha))\n    return out\n\ndef read_blob(repo: Path, sha: str):\n    r = run([\"git\", \"cat-file\", \"-p\", sha], cwd=repo)\n    if r.returncode != 0:\n        return None\n    return r.stdout\n\ndef parse_lock(kind: str, content: str):\n    found = set()\n\n    def add(p, v):\n        if p in watch and v:\n            found.add((p, v))\n\n    if kind == \"package-lock.json\":\n        try:\n            d = json.loads(content)\n        except json.JSONDecodeError:\n            return found\n        pkg_suffix_re = re.compile(r\"node_modules/(\" + pkg_re_alt + r\")$\")\n        for key, meta in (d.get(\"packages\") or {}).items():\n            m = pkg_suffix_re.search(key)\n            if m:\n                add(m.group(1), meta.get(\"version\", \"\"))\n\n        def walk(deps):\n            for name, meta in (deps or {}).items():\n                if name in watch:\n                    add(name, meta.get(\"version\", \"\"))\n                walk(meta.get(\"dependencies\"))\n\n        walk(d.get(\"dependencies\"))\n\n    elif kind == \"yarn.lock\":\n        blocks = re.split(r\"\\n(?=[^\\s])\", content)\n        header_pkg_re = re.compile(r'\"?(' + pkg_re_alt + r\")@\")\n        ver_line_re = re.compile(r'^\\s+version\\s+\"([^\"]+)\"', re.M)\n        for block in blocks:\n            first = block.split(\"\\n\", 1)[0]\n            pkgs = header_pkg_re.findall(first)\n            if not pkgs:\n                continue\n            vm = ver_line_re.search(block)\n            if not vm:\n                continue\n            ver = vm.group(1)\n            for p in set(pkgs):\n                add(p, ver)\n\n    elif kind == \"pnpm-lock.yaml\":\n        occ_re = re.compile(r\"(\" + pkg_re_alt + r\")@([0-9][0-9a-zA-Z.+-]*)\")\n        for m in occ_re.finditer(content):\n            add(m.group(1), m.group(2))\n\n    return found\n\nrepos = discover_repos(root)\nprint(\n    f\"Scanning {len(repos)} git repo(s) under {root}, across all branches, \"\n    f\"for {len(watch)} watched packages...\\n\",\n    flush=True,\n)\n\nresults = []\nblob_cache = {}\nvulnerable_branches = set()\nscanned_projects = []\nprojects_with_hits = set()\n\nfor repo in repos:\n    rel_project = str(repo.relative_to(root)) if repo != root else \".\"\n    branches = list_branches(repo)\n    if not branches:\n        continue\n    scanned_projects.append(rel_project)\n    print(f\"\u2022 {rel_project}  ({len(branches)} ref(s))\", flush=True)\n\n    for full_ref, short_ref in branches:\n        for lock_path, blob_sha in list_lockfiles_with_sha(repo, full_ref):\n            base = os.path.basename(lock_path)\n\n            cache_key = (blob_sha, base)\n            if cache_key in blob_cache:\n                hits = blob_cache[cache_key]\n            else:\n                content = read_blob(repo, blob_sha)\n                hits = parse_lock(base, content) if content else set()\n                blob_cache[cache_key] = hits\n\n            for pkg, ver in hits:\n                is_vuln = ver in vuln[pkg][\"bad\"]\n                results.append({\n                    \"project\": rel_project,\n                    \"branch\": short_ref,\n                    \"lockfile\": lock_path,\n                    \"package\": pkg,\n                    \"version\": ver,\n                    \"bad_versions\": \", \".join(sorted(vuln[pkg][\"bad\"])),\n                    \"vulnerable\": is_vuln,\n                    \"fix\": vuln[pkg][\"fix\"],\n                })\n                projects_with_hits.add(rel_project)\n                if is_vuln:\n                    vulnerable_branches.add((rel_project, short_ref))\n\nprint()\nprint(\"\u2500\" * 78)\n\nif not scanned_projects:\n    print(\"No git repositories with branches found under \" + str(root))\n    sys.exit(0)\n\nseen = set()\nunique = []\nfor r in results:\n    key = (r[\"project\"], r[\"branch\"], r[\"package\"], r[\"version\"])\n    if key in seen:\n        continue\n    seen.add(key)\n    unique.append(r)\n\nCLEAN_MSG = \"no vulnerable package installed\"\nclean_projects = [p for p in scanned_projects if p not in projects_with_hits]\nfor project in clean_projects:\n    unique.append({\n        \"project\": project,\n        \"branch\": \"\u2014\",\n        \"package\": CLEAN_MSG,\n        \"version\": \"\u2014\",\n        \"bad_versions\": \"\u2014\",\n        \"vulnerable\": False,\n        \"_clean\": True,\n    })\n\ndef sort_key(r):\n    clean = r.get(\"_clean\", False)\n    return (\n        not r[\"vulnerable\"],\n        clean,\n        r[\"project\"],\n        r[\"branch\"],\n        r[\"package\"],\n        r[\"version\"],\n    )\n\nunique.sort(key=sort_key)\n\nheaders = [\"Project\", \"Branch\", \"Package\", \"Version\", \"Bad Versions\", \"Vulnerable\"]\nrows = [\n    [\n        r[\"project\"],\n        r[\"branch\"],\n        r[\"package\"],\n        r[\"version\"],\n        r[\"bad_versions\"],\n        \"YES\" if r[\"vulnerable\"] else \"no\",\n    ]\n    for r in unique\n]\n\nwidths = [len(h) for h in headers]\nfor row in rows:\n    for i, cell in enumerate(row):\n        widths[i] = max(widths[i], len(cell))\n\ndef hr():\n    return \"+\" + \"+\".join(\"-\" * (w + 2) for w in widths) + \"+\"\n\ndef fmt(row):\n    return \"| \" + \" | \".join(cell.ljust(widths[i]) for i, cell in enumerate(row)) + \" |\"\n\nprint()\nprint(hr())\nprint(fmt(headers))\nprint(hr().replace(\"-\", \"=\"))\nfor row in rows:\n    print(fmt(row))\nprint(hr())\n\nvuln_count = sum(1 for r in unique if r[\"vulnerable\"])\nhit_count = sum(1 for r in unique if not r.get(\"_clean\", False))\nprint()\nprint(f\"Projects scanned:          {len(scanned_projects)}\")\nprint(f\"Projects with hits:        {len(projects_with_hits)}\")\nprint(f\"Clean projects:            {len(clean_projects)}\")\nprint(f\"Watched-package matches:   {hit_count}\")\nprint(f\"Vulnerable matches:        {vuln_count}\")\nprint(f\"Vulnerable (project,branch) pairs: {len(vulnerable_branches)}\")\n\nif vuln_count:\n    print()\n    print(\"Fix actions for vulnerable packages:\")\n    seen_fix = {}\n    for r in unique:\n        if r[\"vulnerable\"]:\n            seen_fix.setdefault(r[\"package\"], r[\"fix\"])\n    for pkg, fix in sorted(seen_fix.items()):\n        print(f\"  \u2022 {pkg}: {fix}\")\n    print()\n    print(\"Remediation reminder:\")\n    print(\"  1. Upgrade/remove each affected package per the messages above.\")\n    print(\"  2. Delete node_modules and the lock file, then reinstall.\")\n    print(\"  3. Rotate any secrets accessible to your dev/build/CI environment\")\n    print(\"     (the worm exfiltrates env vars, .env files, cloud/GitHub/npm tokens, SSH keys).\")\n    print(\"  4. Audit CI logs for outbound calls to attacker-controlled GitHub repos.\")\nPYEND\n", "creation_timestamp": "2026-05-12T11:45:22.000000Z"}, {"uuid": "c335ab70-dd97-4524-9da5-d76224cbfe6f", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "GHSA-g7cv-rxg3-hmpx", "type": "seen", "source": "https://gist.github.com/danw-relaytech/d18d8484b1f37e6e84349fe1e8e49732", "content": "#!/usr/bin/env bash\n# tanstack_check.sh \u2014 local detection for the 2026-05-11 TanStack npm compromise\n# (a.k.a. \"Mini Shai-Hulud\", GHSA-g7cv-rxg3-hmpx).\n#\n# Run on dev laptops. Exit code 0 = clean, 1 = hit, 2 = some checks skipped.\n# Window: 2026-05-11 19:20-21:30 UTC (epoch 1778527200-1778535000)\n#\n# Sources / further reading:\n#   - TanStack postmortem:  https://tanstack.com/blog/npm-supply-chain-compromise-postmortem\n#   - GH advisory:          https://github.com/TanStack/router/security/advisories/GHSA-g7cv-rxg3-hmpx\n#   - GH tracking issue:    https://github.com/TanStack/router/issues/7383\n#   - HN thread (IOCs):     https://news.ycombinator.com/item?id=48100706\n#   - StepSecurity writeup: https://www.stepsecurity.io/blog/mini-shai-hulud-is-back-a-self-spreading-supply-chain-attack-hits-the-npm-ecosystem\n#   - Aikido writeup:       https://www.aikido.dev/blog/mini-shai-hulud-is-back-tanstack-compromised\n#   - Wiz writeup:          https://www.wiz.io/blog/mini-shai-hulud-strikes-again-tanstack-more-npm-packages-compromised\n#   - Snyk writeup:         https://snyk.io/blog/tanstack-npm-packages-compromised/\n#\n# IMPORTANT: If anything below flags as compromised, do NOT rotate your GitHub\n# token until the gh-token-monitor LaunchAgent/systemd unit (check 6 below) has\n# been removed. The malware polls api.github.com/user every 60s with the stolen\n# token; on revocation it executes `rm -rf ~/`. Source: HN/StepSecurity.\n\nset -u\n\nUSER_ID=\"${USER:-$(whoami)}\"\nHOST_ID=\"$(hostname)\"\necho \"=== tanstack_check.sh ===\"\necho \"user:    $USER_ID\"\necho \"host:    $HOST_ID\"\necho \"date:    $(date -u +%Y-%m-%dT%H:%M:%SZ)\"\necho\n\nhits=0\nskips=0\n\n# Early exit: if no JS package manager or runtime is installed, the install-time\n# payload had no way to execute. We still scan for in-repo persistence artifacts\n# (.claude/, .vscode/) because those could land via `git clone` of a poisoned\n# repo, but the host-level credential-theft vector requires node/npm/pnpm/yarn/bun.\nHAS_JS_TOOLING=0\nfor bin in node npm pnpm yarn bun; do\n  if command -v \"$bin\" &gt;/dev/null 2&gt;&amp;1; then\n    HAS_JS_TOOLING=1\n    break\n  fi\ndone\nif [ \"$HAS_JS_TOOLING\" -eq 0 ]; then\n  echo \"No JS package manager or runtime found (node/npm/pnpm/yarn/bun).\"\n  echo \"Install-time credential theft was not possible on this host.\"\n  echo \"RESULT: not applicable.\"\n  exit 0\nfi\n\n# Known-bad payload hashes (Aikido / StepSecurity / Wiz)\nBAD_SHA_ROUTER_INIT=\"ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c\"\nBAD_SHA_TANSTACK_RUNNER=\"2ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96\"\nBAD_SHA_SETUP_PACKAGE=\"7c12d8614c624c70d6dd6fc2ee289332474abaa38f70ebe2cdef064923ca3a9b\"\nBAD_SHA_SETUP_MJS=\"2258284d65f63829bd67eaba01ef6f1ada2f593f9bbe41678b2df360bd90d3df\"\n\n# Full list of malicious @tanstack/* versions from GHSA-g7cv-rxg3-hmpx\n# (42 packages x 2 versions = 84 malicious versions). Written to a temp file\n# and fed to `grep -E -f` so we don't have to inline a multi-kilobyte regex.\nBAD_TANSTACK_PATTERNS=\"$(mktemp -t tanstack_bad.XXXXXX)\"\ntrap 'rm -f \"$BAD_TANSTACK_PATTERNS\"' EXIT\ncat &gt; \"$BAD_TANSTACK_PATTERNS\" &lt;&lt;'PATTERNS'\n@tanstack/arktype-adapter[\":@ ]+1\\.166\\.(12|15)\n@tanstack/eslint-plugin-router[\":@ ]+1\\.161\\.(9|12)\n@tanstack/eslint-plugin-start[\":@ ]+0\\.0\\.(4|7)\n@tanstack/history[\":@ ]+1\\.161\\.(9|12)\n@tanstack/nitro-v2-vite-plugin[\":@ ]+1\\.154\\.(12|15)\n@tanstack/react-router[\":@ ]+1\\.169\\.(5|8)\n@tanstack/react-router-devtools[\":@ ]+1\\.166\\.(16|19)\n@tanstack/react-router-ssr-query[\":@ ]+1\\.166\\.(15|18)\n@tanstack/react-start[\":@ ]+1\\.167\\.(68|71)\n@tanstack/react-start-client[\":@ ]+1\\.166\\.(51|54)\n@tanstack/react-start-rsc[\":@ ]+0\\.0\\.(47|50)\n@tanstack/react-start-server[\":@ ]+1\\.166\\.(55|58)\n@tanstack/router-cli[\":@ ]+1\\.166\\.(46|49)\n@tanstack/router-core[\":@ ]+1\\.169\\.(5|8)\n@tanstack/router-devtools[\":@ ]+1\\.166\\.(16|19)\n@tanstack/router-devtools-core[\":@ ]+1\\.167\\.(6|9)\n@tanstack/router-generator[\":@ ]+1\\.166\\.(45|48)\n@tanstack/router-plugin[\":@ ]+1\\.167\\.(38|41)\n@tanstack/router-ssr-query-core[\":@ ]+1\\.168\\.(3|6)\n@tanstack/router-utils[\":@ ]+1\\.161\\.(11|14)\n@tanstack/router-vite-plugin[\":@ ]+1\\.166\\.(53|56)\n@tanstack/solid-router[\":@ ]+1\\.169\\.(5|8)\n@tanstack/solid-router-devtools[\":@ ]+1\\.166\\.(16|19)\n@tanstack/solid-router-ssr-query[\":@ ]+1\\.166\\.(15|18)\n@tanstack/solid-start[\":@ ]+1\\.167\\.(65|68)\n@tanstack/solid-start-client[\":@ ]+1\\.166\\.(50|53)\n@tanstack/solid-start-server[\":@ ]+1\\.166\\.(54|57)\n@tanstack/start-client-core[\":@ ]+1\\.168\\.(5|8)\n@tanstack/start-fn-stubs[\":@ ]+1\\.161\\.(9|12)\n@tanstack/start-plugin-core[\":@ ]+1\\.169\\.(23|26)\n@tanstack/start-server-core[\":@ ]+1\\.167\\.(33|36)\n@tanstack/start-static-server-functions[\":@ ]+1\\.166\\.(44|47)\n@tanstack/start-storage-context[\":@ ]+1\\.166\\.(38|41)\n@tanstack/valibot-adapter[\":@ ]+1\\.166\\.(12|15)\n@tanstack/virtual-file-routes[\":@ ]+1\\.161\\.(10|13)\n@tanstack/vue-router[\":@ ]+1\\.169\\.(5|8)\n@tanstack/vue-router-devtools[\":@ ]+1\\.166\\.(16|19)\n@tanstack/vue-router-ssr-query[\":@ ]+1\\.166\\.(15|18)\n@tanstack/vue-start[\":@ ]+1\\.167\\.(61|64)\n@tanstack/vue-start-client[\":@ ]+1\\.166\\.(46|49)\n@tanstack/vue-start-server[\":@ ]+1\\.166\\.(50|53)\n@tanstack/zod-adapter[\":@ ]+1\\.166\\.(12|15)\nPATTERNS\n\n# Scope filesystem searches to every relaytech-co/relaycode checkout under $HOME.\n# Hard-coded paths (~/code, ~/repositories, ~/_git, ...) were too brittle \u2014 devs\n# clone wherever they like \u2014 so we discover roots by finding .git entries at\n# shallow depth and confirming via `git config remote.origin.url`. This handles\n# regular clones, bare clones, and linked worktrees uniformly.\n#\n# Override / extend via env var:\n#   TANSTACK_EXTRA_ROOTS=\"/path/a:/path/b\" ./tanstack_check.sh\nSEARCH_ROOTS=()\nrelay_repo_match() { case \"$1\" in *relaytech-co/relaycode*) return 0;; esac; return 1; }\n\ngit_candidates=\"$(find \"$HOME\" -maxdepth 5 \\\n  \\( -path '*/Library' -o -path '*/.Trash' -o -path '*/Caches' -o -path '*/.cache' \\\n     -o -path '*/node_modules' -o -path '*/.npm' -o -path '*/Applications' \\\n     -o -path '*/.rbenv' -o -path '*/.pyenv' -o -path '*/.nvm' \\\n     -o -path '*/Downloads' \\) -prune \\\n  -o -name '.git' -print 2&gt;/dev/null)\"\nwhile IFS= read -r gitpath; do\n  [ -z \"$gitpath\" ] &amp;&amp; continue\n  repo=\"${gitpath%/.git}\"\n  [ -z \"$repo\" ] &amp;&amp; continue\n  url=\"$(git -C \"$repo\" config --get remote.origin.url 2&gt;/dev/null || true)\"\n  relay_repo_match \"$url\" &amp;&amp; SEARCH_ROOTS+=(\"$repo\")\ndone &lt;&lt;&lt; \"$git_candidates\"\n\n# Include the cwd's git toplevel too, in case the script is run from a checkout\n# stashed outside $HOME or pruned by the find above.\nif cwd_top=\"$(git rev-parse --show-toplevel 2&gt;/dev/null)\" \\\n    &amp;&amp; cwd_url=\"$(git -C \"$cwd_top\" config --get remote.origin.url 2&gt;/dev/null)\" \\\n    &amp;&amp; relay_repo_match \"$cwd_url\"; then\n  case \" ${SEARCH_ROOTS[*]-} \" in *\" $cwd_top \"*) ;; *) SEARCH_ROOTS+=(\"$cwd_top\");; esac\nfi\n\n# Manual additions via TANSTACK_EXTRA_ROOTS (colon-separated, like PATH).\nif [ -n \"${TANSTACK_EXTRA_ROOTS:-}\" ]; then\n  IFS=':' read -r -a _extra_roots &lt;&lt;&lt; \"$TANSTACK_EXTRA_ROOTS\"\n  for r in \"${_extra_roots[@]}\"; do\n    [ -d \"$r\" ] &amp;&amp; SEARCH_ROOTS+=(\"$r\")\n  done\nfi\n\nif [ ${#SEARCH_ROOTS[@]} -gt 0 ]; then\n  echo \"discovered relaycode checkouts:\"\n  printf '  %s\\n' \"${SEARCH_ROOTS[@]}\"\nelse\n  echo \"warning: no relaytech-co/relaycode checkout found under \\$HOME\"\n  echo \"         (set TANSTACK_EXTRA_ROOTS=/path/to/repo if it lives elsewhere)\"\nfi\necho\n\ncheck() {\n  local label=\"$1\"; shift\n  local out\n  out=\"$(\"$@\" 2&gt;/dev/null)\"\n  if [ -n \"$out\" ]; then\n    echo \"[FAIL] $label\"\n    echo \"$out\" | sed 's/^/    /'\n    hits=$((hits+1))\n  else\n    echo \"[ ok ] $label\"\n  fi\n}\n\n###############################################################################\n# 1. Shell history during the malicious publish window\n#    (Source: postmortem timeline; clearable by attacker \u2014 corroborating only)\n###############################################################################\nif [ -r \"$HOME/.zsh_history\" ]; then\n  check \"1. zsh history install during 19:20-21:30 UTC on 2026-05-11\" \\\n    bash -c \"LC_ALL=C awk -F'[:;]' '/^: [0-9]+:0;/ &amp;&amp; \\$2&gt;=1778527200 &amp;&amp; \\$2&lt;=1778535000' \\\"$HOME/.zsh_history\\\" \\\n      | grep -iE 'npm i|pnpm i|yarn (install|add)'\"\nelse\n  echo \"[skip] 1. zsh history (file not readable)\"\n  skips=$((skips+1))\nfi\n\nif [ -r \"$HOME/.bash_history\" ]; then\n  # Bash history with HISTTIMEFORMAT writes `#` on its own line before\n  # each command. Track the most recent timestamp and emit commands that fall\n  # in the malicious-publish window. If HISTTIMEFORMAT was never set, every\n  # line is command-only and t stays 0, so nothing prints (no false negatives;\n  # nothing we can do without timestamps).\n  check \"1b. bash history install during 19:20-21:30 UTC on 2026-05-11\" \\\n    bash -c \"awk '/^#[0-9]+\\$/ { t = substr(\\$0, 2)+0; next } t&gt;=1778527200 &amp;&amp; t&lt;=1778535000' \\\"$HOME/.bash_history\\\" \\\n      | grep -iE 'npm i|pnpm i|yarn (install|add)'\"\nelse\n  echo \"[skip] 1b. bash history (file not readable)\"\n  skips=$((skips+1))\nfi\n\n###############################################################################\n# 2. Claude Code session activity during the window\n###############################################################################\nif [ -d \"$HOME/.claude/projects\" ]; then\n  check \"2. Claude Code install during window\" \\\n    bash -c \"grep -rhE '\\\"timestamp\\\":\\\"2026-05-11T(19:[2-5][0-9]|20:[0-5][0-9]|21:[0-2][0-9]|21:30)' \\\"$HOME/.claude/projects/\\\" \\\n      | grep -iE 'npm install|pnpm install|yarn install|npm ci'\"\nelse\n  echo \"[skip] 2. Claude Code logs (~/.claude/projects/ not present)\"\n  skips=$((skips+1))\nfi\n\n###############################################################################\n# 3. Malicious @tanstack version pinned in any lockfile / package.json,\n#    plus orphan commit / @tanstack/setup / attacker handle fingerprints in\n#    both lockfiles AND installed node_modules/@tanstack/ manifests.\n#    (Sources: GHSA-g7cv-rxg3-hmpx, Snyk)\n###############################################################################\nif [ ${#SEARCH_ROOTS[@]} -gt 0 ]; then\n  check \"3. malicious @tanstack version pinned in lockfile/package.json (all 42 pkgs)\" \\\n    bash -c \"find \\\"${SEARCH_ROOTS[@]}\\\" \\\n      \\\\( -name 'node_modules' -o -name '.git' \\\\) -prune -o \\\n      \\\\( -name 'package.json' -o -name 'package-lock.json' -o -name 'pnpm-lock.yaml' -o -name 'yarn.lock' \\\\) -print 2&gt;/dev/null \\\n      | xargs grep -lE -f \\\"$BAD_TANSTACK_PATTERNS\\\" 2&gt;/dev/null\"\n\n  # 3b. Orphan-commit fingerprint / fictitious @tanstack/setup / attacker handle\n  #     in lockfiles or package.json files (excluding node_modules)\n  check \"3b. malicious @tanstack/setup, orphan commit ref, or attacker handle in lockfiles\" \\\n    bash -c \"find \\\"${SEARCH_ROOTS[@]}\\\" \\\n      \\\\( -name 'node_modules' -o -name '.git' \\\\) -prune -o \\\n      \\\\( -name 'package.json' -o -name 'package-lock.json' -o -name 'pnpm-lock.yaml' -o -name 'yarn.lock' \\\\) -print 2&gt;/dev/null \\\n      | xargs grep -lE '79ac49eedf774dd4b0cfa308722bc463cfe5885c|@tanstack/setup|voicproducoes|zblgg' 2&gt;/dev/null\"\n\n  # 3c. Same fingerprints but in INSTALLED node_modules/@tanstack/*/package.json\n  #     (Source: Snyk). This is the strongest signal \u2014 a malicious manifest on\n  #     disk in node_modules proves the install actually completed.\n  check \"3c. malicious markers in installed node_modules/@tanstack/*/package.json\" \\\n    bash -c \"find \\\"${SEARCH_ROOTS[@]}\\\" -type d -name '@tanstack' -path '*/node_modules/*' 2&gt;/dev/null \\\n      | xargs -I{} find {} -name 'package.json' 2&gt;/dev/null \\\n      | xargs grep -lE '79ac49eedf774dd4b0cfa308722bc463cfe5885c|@tanstack/setup|voicproducoes|zblgg' 2&gt;/dev/null\"\nelse\n  echo \"[skip] 3. no common code dirs found\"\n  skips=$((skips+3))\nfi\n\n###############################################################################\n# 4. router_init.js / tanstack_runner.js / router_runtime.js / vite_setup.mjs\n#    payload files anywhere on disk; SHA-256 match against known-bad hashes.\n#    (Source: Aikido, StepSecurity, postmortem)\n#    Uses Spotlight (mdfind) on macOS for instant filesystem-wide search.\n###############################################################################\nPAYLOAD_NAMES=(router_init.js tanstack_runner.js router_runtime.js vite_setup.mjs opensearch_init.js)\nPAYLOAD_FILES=\"\"\n# Strategy:\n#   - When mdfind is present: trust Spotlight for global coverage, then run a\n#     targeted find ONLY over SEARCH_ROOTS as a backstop for unindexed code dirs\n#     (e.g. users who excluded ~/code from Spotlight). Avoids find-over-$HOME,\n#     which is too slow for a fleet script.\n#   - When mdfind is absent (Linux): find over $HOME + /tmp + SEARCH_ROOTS.\nif command -v mdfind &gt;/dev/null 2&gt;&amp;1; then\n  for name in \"${PAYLOAD_NAMES[@]}\"; do\n    found=\"$(mdfind -name \"$name\" 2&gt;/dev/null)\"\n    [ -n \"$found\" ] &amp;&amp; PAYLOAD_FILES=\"$PAYLOAD_FILES\"$'\\n'\"$found\"\n  done\n  FIND_ROOTS=()\n  [ ${#SEARCH_ROOTS[@]} -gt 0 ] &amp;&amp; FIND_ROOTS=(\"${SEARCH_ROOTS[@]}\")\nelse\n  FIND_ROOTS=(\"$HOME\" /tmp)\n  [ ${#SEARCH_ROOTS[@]} -gt 0 ] &amp;&amp; for r in \"${SEARCH_ROOTS[@]}\"; do\n    case \"$r\" in \"$HOME\"/*) ;; *) FIND_ROOTS+=(\"$r\") ;; esac\n  done\nfi\nif [ ${#FIND_ROOTS[@]} -gt 0 ]; then\n  found_via_find=\"$(find \"${FIND_ROOTS[@]}\" \\\n    \\( -path '*/Library' -o -path '*/.git' -o -path '*/Caches' -o -name '.Trash' \\) -prune \\\n    -o -type f \\( -name 'router_init.js' -o -name 'tanstack_runner.js' \\\n              -o -name 'router_runtime.js' -o -name 'vite_setup.mjs' \\\n              -o -name 'opensearch_init.js' \\) -print 2&gt;/dev/null)\"\n  [ -n \"$found_via_find\" ] &amp;&amp; PAYLOAD_FILES=\"$PAYLOAD_FILES\"$'\\n'\"$found_via_find\"\nfi\nPAYLOAD_FILES=\"$(echo \"$PAYLOAD_FILES\" | sed '/^$/d' | sort -u)\"\n\nif [ -n \"$PAYLOAD_FILES\" ]; then\n  echo \"[FAIL] 4. payload file(s) found on disk \u2014 hashing for IOC match:\"\n  while IFS= read -r f; do\n    [ -z \"$f\" ] &amp;&amp; continue\n    sha=\"$(shasum -a 256 \"$f\" 2&gt;/dev/null | awk '{print $1}')\"\n    tag=\"[unknown sha]\"\n    case \"$sha\" in\n      \"$BAD_SHA_ROUTER_INIT\")    tag=\"[!!! KNOWN-BAD router_init.js]\" ;;\n      \"$BAD_SHA_TANSTACK_RUNNER\") tag=\"[!!! KNOWN-BAD tanstack_runner.js]\" ;;\n      \"$BAD_SHA_SETUP_PACKAGE\")   tag=\"[!!! KNOWN-BAD @tanstack/setup pkg]\" ;;\n    esac\n    echo \"    $tag  $sha  $f\"\n  done &lt;&lt;&lt; \"$PAYLOAD_FILES\"\n  hits=$((hits+1))\nelse\n  echo \"[ ok ] 4. no router_init.js / tanstack_runner.js / router_runtime.js / vite_setup.mjs / opensearch_init.js on disk\"\nfi\n\n###############################################################################\n# 4b. setup.mjs SHA check (Source: Wiz).\n#     setup.mjs is too common a filename to flag on presence alone, so we\n#     enumerate every setup.mjs under SEARCH_ROOTS (including node_modules,\n#     where the malicious copy lives) and only flag SHA-256 matches against\n#     the known-bad hash 2258284d65f63829bd67eaba01ef6f1ada2f593f9bbe41678b2df360bd90d3df.\n###############################################################################\nif [ ${#SEARCH_ROOTS[@]} -gt 0 ]; then\n  # Pre-filter by exact byte size (malicious setup.mjs = 5047 bytes per Wiz).\n  # This avoids hashing every setup.mjs in every node_modules tree, which is\n  # too slow for a fleet script. `-size 5047c` is portable across BSD/GNU find.\n  SETUP_MJS_FILES=\"$(find \"${SEARCH_ROOTS[@]}\" -name '.git' -prune -o \\\n    -type f -name 'setup.mjs' -size 5047c -print 2&gt;/dev/null)\"\n  bad_setup_found=0\n  while IFS= read -r f; do\n    [ -z \"$f\" ] &amp;&amp; continue\n    sha=\"$(shasum -a 256 \"$f\" 2&gt;/dev/null | awk '{print $1}')\"\n    if [ \"$sha\" = \"$BAD_SHA_SETUP_MJS\" ]; then\n      [ \"$bad_setup_found\" -eq 0 ] &amp;&amp; echo \"[FAIL] 4b. setup.mjs with known-bad SHA found:\"\n      echo \"    [!!! KNOWN-BAD setup.mjs]  $sha  $f\"\n      bad_setup_found=1\n    fi\n  done &lt;&lt;&lt; \"$SETUP_MJS_FILES\"\n  if [ \"$bad_setup_found\" -eq 0 ]; then\n    echo \"[ ok ] 4b. no setup.mjs files match known-bad SHA\"\n  else\n    hits=$((hits+1))\n  fi\nelse\n  echo \"[skip] 4b. no common code dirs (setup.mjs SHA check)\"\n  skips=$((skips+1))\nfi\n\n###############################################################################\n# 5. In-repo persistence: .claude/ and .vscode/ artifacts that survive npm uninstall\n#    (Source: StepSecurity)\n#    Looks for SessionStart hook in .claude/settings.json, folderOpen task in\n#    .vscode/tasks.json, and any setup.mjs / router_runtime.js in those dirs.\n###############################################################################\nif [ ${#SEARCH_ROOTS[@]} -gt 0 ]; then\n  check \"5. persistence files in .claude/ or .vscode/ inside repos\" \\\n    bash -c \"find \\\"${SEARCH_ROOTS[@]}\\\" \\\n      \\\\( -name 'node_modules' -o -name '.git' \\\\) -prune -o \\\n      -type f \\\\( -name 'setup.mjs' -o -name 'router_runtime.js' \\\\) \\\n      \\\\( -path '*/.claude/*' -o -path '*/.vscode/*' \\\\) -print 2&gt;/dev/null\"\n\n  check \"5b. SessionStart hook in .claude/settings.json or folderOpen task in .vscode/tasks.json\" \\\n    bash -c \"find \\\"${SEARCH_ROOTS[@]}\\\" \\\n      \\\\( -name 'node_modules' -o -name '.git' \\\\) -prune -o \\\n      \\\\( -path '*/.claude/settings.json' -o -path '*/.vscode/tasks.json' \\\\) -print 2&gt;/dev/null \\\n      | xargs grep -lE 'SessionStart|folderOpen|setup\\\\.mjs|router_runtime' 2&gt;/dev/null\"\n\n  # 5c. Injected codeql_analysis.yml that exfiltrates secrets\n  check \"5c. suspicious .github/workflows/codeql_analysis.yml (injected by worm)\" \\\n    bash -c \"find \\\"${SEARCH_ROOTS[@]}\\\" \\\n      \\\\( -name 'node_modules' \\\\) -prune -o \\\n      -path '*/.github/workflows/codeql_analysis.yml' -print 2&gt;/dev/null \\\n      | xargs grep -lE 'masscan|getsession|git-tanstack|secrets\\\\.toJSON|toJson\\\\(secrets' 2&gt;/dev/null\"\nelse\n  echo \"[skip] 5. no common code dirs (in-repo persistence checks)\"\n  skips=$((skips+3))\nfi\n\n###############################################################################\n# 6. gh-token-monitor dead-man's switch (host-level persistence)\n#    (Source: HN thread / StepSecurity)\n#    Polls GitHub every 60s; on token revocation, runs `rm -rf ~/`.\n#    THIS MUST BE REMOVED *BEFORE* ANY CREDENTIAL ROTATION.\n###############################################################################\ncheck \"6. ~/.local/bin/gh-token-monitor.sh dead-man's switch script\" \\\n  bash -c \"[ -e \\\"$HOME/.local/bin/gh-token-monitor.sh\\\" ] &amp;&amp; echo \\\"$HOME/.local/bin/gh-token-monitor.sh\\\"\"\n\ncheck \"6b. ~/.config/gh-token-monitor/ token storage dir\" \\\n  bash -c \"[ -d \\\"$HOME/.config/gh-token-monitor\\\" ] &amp;&amp; echo \\\"$HOME/.config/gh-token-monitor\\\"\"\n\nif [ \"$(uname)\" = \"Darwin\" ]; then\n  check \"6c. macOS LaunchAgent com.user.gh-token-monitor.plist\" \\\n    bash -c \"ls \\\"$HOME/Library/LaunchAgents/\\\" 2&gt;/dev/null | grep -iE 'gh-token-monitor|com\\\\.user\\\\.gh' || launchctl list 2&gt;/dev/null | grep -i 'gh-token-monitor'\"\nelse\n  check \"6c. Linux systemd user service gh-token-monitor\" \\\n    bash -c \"ls \\\"$HOME/.config/systemd/user/\\\" 2&gt;/dev/null | grep -i 'gh-token-monitor' || systemctl --user list-unit-files 2&gt;/dev/null | grep -i 'gh-token-monitor'\"\nfi\n\n###############################################################################\n# 7. Malicious npm token (literal smoking gun)\n#    (Source: StepSecurity)\n###############################################################################\nif command -v npm &gt;/dev/null 2&gt;&amp;1; then\n  check \"7. malicious npm token 'IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner'\" \\\n    bash -c \"npm token list 2&gt;/dev/null | grep -i 'IfYouRevokeThisToken'\"\nelse\n  echo \"[skip] 7. npm not installed\"\n  skips=$((skips+1))\nfi\n\n###############################################################################\n# 8. Shell rc modifications (malware overrides `sudo` to capture password)\n#    (Source: HN thread)\n###############################################################################\ncheck \"8. shell rc files modified to override 'sudo' or reference gh-token-monitor\" \\\n  bash -c \"grep -lE 'function sudo|gh-token-monitor' \\\n    \\\"$HOME/.bashrc\\\" \\\"$HOME/.bash_profile\\\" \\\"$HOME/.zshrc\\\" \\\"$HOME/.zprofile\\\" \\\"$HOME/.profile\\\" 2&gt;/dev/null\"\n\n###############################################################################\n# 9. Outbound network IOCs \u2014 DNS resolver cache + recent unified log (24h)\n#    (Source: postmortem IOC list \u2014 Session network + Aikido/StepSecurity C2)\n#    The full `log show --last 7d` is too slow for a fleet script; we use a\n#    24h window and require an opt-in flag (TANSTACK_DEEP_LOG=1) to go longer.\n###############################################################################\nif [ \"$(uname)\" = \"Darwin\" ] &amp;&amp; command -v log &gt;/dev/null 2&gt;&amp;1; then\n  WINDOW=\"${TANSTACK_DEEP_LOG:+7d}\"; WINDOW=\"${WINDOW:-24h}\"\n  out=\"$(log show --last \"$WINDOW\" --style compact 2&gt;/dev/null \\\n    | grep -iE 'getsession\\.org|catbox\\.moe|git-tanstack\\.com|masscan\\.cloud' \\\n    | head -20)\"\n  if [ -n \"$out\" ]; then\n    echo \"[FAIL] 9. macOS unified log ($WINDOW) mentions IOC domain:\"\n    echo \"$out\" | sed 's/^/    /'\n    hits=$((hits+1))\n  else\n    echo \"[ ok ] 9. no IOC domains in last $WINDOW of macOS unified log\"\n  fi\nelse\n  echo \"[skip] 9. macOS unified log not available\"\n  skips=$((skips+1))\nfi\n\n###############################################################################\n# 10. Suspicious bun / python3 process reading another process's memory\n#     (Source: StepSecurity, runner-memory OIDC theft technique)\n###############################################################################\n# Require a `bun`/`node` interpreter prefix on the payload-name match so\n# searches like `find ... router_init.js` don't false-positive.\ncheck \"10. running bun/node process executing tanstack/opensearch payload, or gh-token-monitor\" \\\n  bash -c \"ps -axww -o command= 2&gt;/dev/null \\\n    | grep -iE '(bun|node|deno)[^|]*(tanstack_runner|router_init|opensearch_init)|gh-token-monitor' \\\n    | grep -vE 'grep|find|tanstack_check'\"\n\n###############################################################################\necho\necho \"=== summary ===\"\necho \"user:  $USER_ID\"\necho \"host:  $HOST_ID\"\necho \"hits:  $hits\"\necho \"skips: $skips\"\n\nif [ \"$hits\" -gt 0 ]; then\n  cat &lt;&lt;'BANNER'\nRESULT: POTENTIAL COMPROMISE.\n  1. DISCONNECT from network.\n  2. Escalate to platform eng immediately.\nBANNER\n  exit 1\nelif [ \"$skips\" -gt 0 ]; then\n  echo \"RESULT: clean for checks that ran, but $skips check(s) skipped.\"\n  exit 2\nelse\n  echo \"RESULT: clean.\"\n  exit 0\nfi\n", "creation_timestamp": "2026-05-12T16:58:08.000000Z"}, {"uuid": "d3ec4a03-5376-4c0c-9509-c251d7a20722", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "GHSA-g7cv-rxg3-hmpx", "type": "seen", "source": "https://gist.github.com/srijanshukla18/49821c8704c4222e2f84221b21340e3f", "content": "#!/usr/bin/env python3\n\"\"\"\nRead-only Mini Shai-Hulud / TanStack campaign scanner.\n\nSources checked 2026-05-13:\n- TanStack GHSA-g7cv-rxg3-hmpx\n- Wiz \"Mini Shai-Hulud Strikes Again\"\n- SafeDep \"Mass Supply Chain Attack Hits TanStack, Mistral AI npm and PyPI Packages\"\n- Snyk \"TanStack npm packages compromised\"\n\nExit codes:\n  0: no known indicators found\n  1: likely affected / exposed / suspicious indicators found\n  2: scanner error\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport hashlib\nimport importlib.metadata\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import Iterable\n\n\nAFFECTED_NPM: dict[str, set[str]] = {\n    \"@tanstack/arktype-adapter\": {\"1.166.12\", \"1.166.15\"},\n    \"@tanstack/eslint-plugin-router\": {\"1.161.9\", \"1.161.12\"},\n    \"@tanstack/eslint-plugin-start\": {\"0.0.4\", \"0.0.7\"},\n    \"@tanstack/history\": {\"1.161.9\", \"1.161.12\"},\n    \"@tanstack/nitro-v2-vite-plugin\": {\"1.154.12\", \"1.154.15\"},\n    \"@tanstack/react-router\": {\"1.169.5\", \"1.169.8\"},\n    \"@tanstack/react-router-devtools\": {\"1.166.16\", \"1.166.19\"},\n    \"@tanstack/react-router-ssr-query\": {\"1.166.15\", \"1.166.18\"},\n    \"@tanstack/react-start\": {\"1.167.68\", \"1.167.71\"},\n    \"@tanstack/react-start-client\": {\"1.166.51\", \"1.166.54\"},\n    \"@tanstack/react-start-rsc\": {\"0.0.47\", \"0.0.50\"},\n    \"@tanstack/react-start-server\": {\"1.166.55\", \"1.166.58\"},\n    \"@tanstack/router-cli\": {\"1.166.46\", \"1.166.49\"},\n    \"@tanstack/router-core\": {\"1.169.5\", \"1.169.8\"},\n    \"@tanstack/router-devtools\": {\"1.166.16\", \"1.166.19\"},\n    \"@tanstack/router-devtools-core\": {\"1.167.6\", \"1.167.9\"},\n    \"@tanstack/router-generator\": {\"1.166.45\", \"1.166.48\"},\n    \"@tanstack/router-plugin\": {\"1.167.38\", \"1.167.41\"},\n    \"@tanstack/router-ssr-query-core\": {\"1.168.3\", \"1.168.6\"},\n    \"@tanstack/router-utils\": {\"1.161.11\", \"1.161.14\"},\n    \"@tanstack/router-vite-plugin\": {\"1.166.53\", \"1.166.56\"},\n    \"@tanstack/solid-router\": {\"1.169.5\", \"1.169.8\"},\n    \"@tanstack/solid-router-devtools\": {\"1.166.16\", \"1.166.19\"},\n    \"@tanstack/solid-router-ssr-query\": {\"1.166.15\", \"1.166.18\"},\n    \"@tanstack/solid-start\": {\"1.167.65\", \"1.167.68\"},\n    \"@tanstack/solid-start-client\": {\"1.166.50\", \"1.166.53\"},\n    \"@tanstack/solid-start-server\": {\"1.166.54\", \"1.166.57\"},\n    \"@tanstack/start-client-core\": {\"1.168.5\", \"1.168.8\"},\n    \"@tanstack/start-fn-stubs\": {\"1.161.9\", \"1.161.12\"},\n    \"@tanstack/start-plugin-core\": {\"1.169.23\", \"1.169.26\"},\n    \"@tanstack/start-server-core\": {\"1.167.33\", \"1.167.36\"},\n    \"@tanstack/start-static-server-functions\": {\"1.166.44\", \"1.166.47\"},\n    \"@tanstack/start-storage-context\": {\"1.166.38\", \"1.166.41\"},\n    \"@tanstack/valibot-adapter\": {\"1.166.12\", \"1.166.15\"},\n    \"@tanstack/virtual-file-routes\": {\"1.161.10\", \"1.161.13\"},\n    \"@tanstack/vue-router\": {\"1.169.5\", \"1.169.8\"},\n    \"@tanstack/vue-router-devtools\": {\"1.166.16\", \"1.166.19\"},\n    \"@tanstack/vue-router-ssr-query\": {\"1.166.15\", \"1.166.18\"},\n    \"@tanstack/vue-start\": {\"1.167.61\", \"1.167.64\"},\n    \"@tanstack/vue-start-client\": {\"1.166.46\", \"1.166.49\"},\n    \"@tanstack/vue-start-server\": {\"1.166.50\", \"1.166.53\"},\n    \"@tanstack/zod-adapter\": {\"1.166.12\", \"1.166.15\"},\n    \"@mistralai/mistralai\": {\"2.2.2\", \"2.2.3\", \"2.2.4\"},\n    \"@mistralai/mistralai-azure\": {\"1.7.1\", \"1.7.2\", \"1.7.3\"},\n    \"@mistralai/mistralai-gcp\": {\"1.7.1\", \"1.7.2\", \"1.7.3\"},\n    \"@uipath/access-policy-sdk\": {\"0.3.1\"},\n    \"@uipath/access-policy-tool\": {\"0.3.1\"},\n    \"@uipath/admin-tool\": {\"0.1.1\"},\n    \"@uipath/agent-sdk\": {\"1.0.2\"},\n    \"@uipath/agent-tool\": {\"1.0.1\"},\n    \"@uipath/agent.sdk\": {\"0.0.18\"},\n    \"@uipath/aops-policy-tool\": {\"0.3.1\"},\n    \"@uipath/ap-chat\": {\"1.5.7\"},\n    \"@uipath/api-workflow-tool\": {\"1.0.1\"},\n    \"@uipath/apollo-core\": {\"5.9.2\"},\n    \"@uipath/apollo-react\": {\"4.24.5\"},\n    \"@uipath/apollo-wind\": {\"2.16.2\"},\n    \"@uipath/auth\": {\"1.0.1\"},\n    \"@uipath/case-tool\": {\"1.0.1\"},\n    \"@uipath/cli\": {\"1.0.1\"},\n    \"@uipath/codedagent-tool\": {\"1.0.1\"},\n    \"@uipath/codedagents-tool\": {\"0.1.12\"},\n    \"@uipath/codedapp-tool\": {\"1.0.1\"},\n    \"@uipath/common\": {\"1.0.1\"},\n    \"@uipath/context-grounding-tool\": {\"0.1.1\"},\n    \"@uipath/data-fabric-tool\": {\"1.0.2\"},\n    \"@uipath/docsai-tool\": {\"1.0.1\"},\n    \"@uipath/filesystem\": {\"1.0.1\"},\n    \"@uipath/flow-tool\": {\"1.0.2\"},\n    \"@uipath/functions-tool\": {\"1.0.1\"},\n    \"@uipath/gov-tool\": {\"0.3.1\"},\n    \"@uipath/identity-tool\": {\"0.1.1\"},\n    \"@uipath/insights-sdk\": {\"1.0.1\"},\n    \"@uipath/insights-tool\": {\"1.0.1\"},\n    \"@uipath/integrationservice-sdk\": {\"1.0.2\"},\n    \"@uipath/integrationservice-tool\": {\"1.0.2\"},\n    \"@uipath/llmgw-tool\": {\"1.0.1\"},\n    \"@uipath/maestro-sdk\": {\"1.0.1\"},\n    \"@uipath/maestro-tool\": {\"1.0.1\"},\n    \"@uipath/orchestrator-tool\": {\"1.0.1\"},\n    \"@uipath/packager-tool-apiworkflow\": {\"0.0.19\"},\n    \"@uipath/packager-tool-bpmn\": {\"0.0.9\"},\n    \"@uipath/packager-tool-case\": {\"0.0.9\"},\n    \"@uipath/packager-tool-connector\": {\"0.0.19\"},\n    \"@uipath/packager-tool-flow\": {\"0.0.19\"},\n    \"@uipath/packager-tool-functions\": {\"0.1.1\"},\n    \"@uipath/packager-tool-webapp\": {\"1.0.6\"},\n    \"@uipath/packager-tool-workflowcompiler\": {\"0.0.16\"},\n    \"@uipath/packager-tool-workflowcompiler-browser\": {\"0.0.34\"},\n    \"@uipath/platform-tool\": {\"1.0.1\"},\n    \"@uipath/project-packager\": {\"1.1.16\"},\n    \"@uipath/resource-tool\": {\"1.0.1\"},\n    \"@uipath/resourcecatalog-tool\": {\"0.1.1\"},\n    \"@uipath/resources-tool\": {\"0.1.11\"},\n    \"@uipath/robot\": {\"1.3.4\"},\n    \"@uipath/rpa-legacy-tool\": {\"1.0.1\"},\n    \"@uipath/rpa-tool\": {\"0.9.5\"},\n    \"@uipath/solution-packager\": {\"0.0.35\"},\n    \"@uipath/solution-tool\": {\"1.0.1\"},\n    \"@uipath/solutionpackager-sdk\": {\"1.0.11\"},\n    \"@uipath/solutionpackager-tool-core\": {\"0.0.34\"},\n    \"@uipath/tasks-tool\": {\"1.0.1\"},\n    \"@uipath/telemetry\": {\"0.0.7\"},\n    \"@uipath/test-manager-tool\": {\"1.0.2\"},\n    \"@uipath/tool-workflowcompiler\": {\"0.0.12\"},\n    \"@uipath/traces-tool\": {\"1.0.1\"},\n    \"@uipath/ui-widgets-multi-file-upload\": {\"1.0.1\"},\n    \"@uipath/uipath-python-bridge\": {\"1.0.1\"},\n    \"@uipath/vertical-solutions-tool\": {\"1.0.1\"},\n    \"@uipath/vss\": {\"0.1.6\"},\n    \"@uipath/widget.sdk\": {\"1.2.3\"},\n    \"@squawk/airport-data\": {\"0.7.4\", \"0.7.5\", \"0.7.6\", \"0.7.7\", \"0.7.8\"},\n    \"@squawk/airports\": {\"0.6.2\", \"0.6.3\", \"0.6.4\", \"0.6.5\", \"0.6.6\"},\n    \"@squawk/airspace\": {\"0.8.1\", \"0.8.2\", \"0.8.3\", \"0.8.4\", \"0.8.5\"},\n    \"@squawk/airspace-data\": {\"0.5.3\", \"0.5.4\", \"0.5.5\", \"0.5.6\", \"0.5.7\"},\n    \"@squawk/airway-data\": {\"0.5.4\", \"0.5.5\", \"0.5.6\", \"0.5.7\", \"0.5.8\"},\n    \"@squawk/airways\": {\"0.4.2\", \"0.4.3\", \"0.4.4\", \"0.4.5\", \"0.4.6\"},\n    \"@squawk/fix-data\": {\"0.6.4\", \"0.6.5\", \"0.6.6\", \"0.6.7\", \"0.6.8\"},\n    \"@squawk/fixes\": {\"0.3.2\", \"0.3.3\", \"0.3.4\", \"0.3.5\", \"0.3.6\"},\n    \"@squawk/flight-math\": {\"0.5.4\", \"0.5.5\", \"0.5.6\", \"0.5.7\", \"0.5.8\"},\n    \"@squawk/flightplan\": {\"0.5.2\", \"0.5.3\", \"0.5.4\", \"0.5.5\", \"0.5.6\"},\n    \"@squawk/geo\": {\"0.4.4\", \"0.4.5\", \"0.4.6\", \"0.4.7\", \"0.4.8\"},\n    \"@squawk/icao-registry\": {\"0.5.2\", \"0.5.3\", \"0.5.4\", \"0.5.5\", \"0.5.6\"},\n    \"@squawk/icao-registry-data\": {\"0.8.4\", \"0.8.5\", \"0.8.6\", \"0.8.7\", \"0.8.8\"},\n    \"@squawk/mcp\": {\"0.9.1\", \"0.9.2\", \"0.9.3\", \"0.9.4\", \"0.9.5\"},\n    \"@squawk/navaid-data\": {\"0.6.4\", \"0.6.5\", \"0.6.6\", \"0.6.7\", \"0.6.8\"},\n    \"@squawk/navaids\": {\"0.4.2\", \"0.4.3\", \"0.4.4\", \"0.4.5\", \"0.4.6\"},\n    \"@squawk/notams\": {\"0.3.6\", \"0.3.7\", \"0.3.8\", \"0.3.9\", \"0.3.10\"},\n    \"@squawk/procedure-data\": {\"0.7.3\", \"0.7.4\", \"0.7.5\", \"0.7.6\", \"0.7.7\"},\n    \"@squawk/procedures\": {\"0.5.2\", \"0.5.3\", \"0.5.4\", \"0.5.5\", \"0.5.6\"},\n    \"@squawk/types\": {\"0.8.1\", \"0.8.2\", \"0.8.3\", \"0.8.4\", \"0.8.5\"},\n    \"@squawk/units\": {\"0.4.3\", \"0.4.4\", \"0.4.5\", \"0.4.6\", \"0.4.7\"},\n    \"@squawk/weather\": {\"0.5.6\", \"0.5.7\", \"0.5.8\", \"0.5.9\", \"0.5.10\"},\n    \"@tallyui/components\": {\"1.0.1\", \"1.0.2\", \"1.0.3\"},\n    \"@tallyui/connector-medusa\": {\"1.0.1\", \"1.0.2\", \"1.0.3\"},\n    \"@tallyui/connector-shopify\": {\"1.0.1\", \"1.0.2\", \"1.0.3\"},\n    \"@tallyui/connector-vendure\": {\"1.0.1\", \"1.0.2\", \"1.0.3\"},\n    \"@tallyui/connector-woocommerce\": {\"1.0.1\", \"1.0.2\", \"1.0.3\"},\n    \"@tallyui/core\": {\"0.2.1\", \"0.2.2\", \"0.2.3\"},\n    \"@tallyui/database\": {\"1.0.1\", \"1.0.2\", \"1.0.3\"},\n    \"@tallyui/pos\": {\"0.1.1\", \"0.1.2\", \"0.1.3\"},\n    \"@tallyui/storage-sqlite\": {\"0.2.1\", \"0.2.2\", \"0.2.3\"},\n    \"@tallyui/theme\": {\"0.2.1\", \"0.2.2\", \"0.2.3\"},\n    \"@beproduct/nestjs-auth\": {\n        \"0.1.2\", \"0.1.3\", \"0.1.4\", \"0.1.5\", \"0.1.6\", \"0.1.7\",\n        \"0.1.8\", \"0.1.9\", \"0.1.10\", \"0.1.11\", \"0.1.12\", \"0.1.13\",\n        \"0.1.14\", \"0.1.15\", \"0.1.16\", \"0.1.17\", \"0.1.18\", \"0.1.19\",\n    },\n    \"@draftauth/client\": {\"0.2.1\", \"0.2.2\"},\n    \"@draftauth/core\": {\"0.13.1\", \"0.13.2\"},\n    \"@draftlab/auth\": {\"0.24.1\", \"0.24.2\"},\n    \"@draftlab/auth-router\": {\"0.5.1\", \"0.5.2\"},\n    \"@draftlab/db\": {\"0.16.1\", \"0.16.2\"},\n    \"@supersurkhet/cli\": {\"0.0.2\", \"0.0.3\", \"0.0.4\", \"0.0.5\", \"0.0.6\", \"0.0.7\"},\n    \"@supersurkhet/sdk\": {\"0.0.2\", \"0.0.3\", \"0.0.4\", \"0.0.5\", \"0.0.6\", \"0.0.7\"},\n    \"@taskflow-corp/cli\": {\"0.1.24\", \"0.1.25\", \"0.1.26\", \"0.1.27\", \"0.1.28\", \"0.1.29\"},\n    \"@tolka/cli\": {\"1.0.2\", \"1.0.3\", \"1.0.4\", \"1.0.5\", \"1.0.6\"},\n    \"@mesadev/rest\": {\"0.28.3\"},\n    \"@mesadev/saguaro\": {\"0.4.22\"},\n    \"@mesadev/sdk\": {\"0.28.3\"},\n    \"@ml-toolkit-ts/preprocessing\": {\"1.0.2\", \"1.0.3\"},\n    \"@ml-toolkit-ts/xgboost\": {\"1.0.3\", \"1.0.4\"},\n    \"@dirigible-ai/sdk\": {\"0.6.2\", \"0.6.3\"},\n    \"@opensearch-project/opensearch\": {\"3.5.3\", \"3.6.2\", \"3.7.0\", \"3.8.0\"},\n    \"agentwork-cli\": {\"0.1.4\", \"0.1.5\"},\n    \"cmux-agent-mcp\": {\"0.1.3\", \"0.1.4\", \"0.1.5\", \"0.1.6\", \"0.1.7\", \"0.1.8\"},\n    \"cross-stitch\": {\"1.1.3\", \"1.1.4\", \"1.1.5\", \"1.1.6\", \"1.1.7\"},\n    \"git-branch-selector\": {\"1.3.3\", \"1.3.4\", \"1.3.5\", \"1.3.6\", \"1.3.7\"},\n    \"git-git-git\": {\"1.0.8\", \"1.0.9\", \"1.0.10\", \"1.0.11\", \"1.0.12\"},\n    \"ml-toolkit-ts\": {\"1.0.4\", \"1.0.5\"},\n    \"nextmove-mcp\": {\"0.1.3\", \"0.1.4\", \"0.1.5\", \"0.1.6\", \"0.1.7\"},\n    \"safe-action\": {\"0.8.3\", \"0.8.4\"},\n    \"ts-dna\": {\"3.0.1\", \"3.0.2\", \"3.0.3\", \"3.0.4\", \"3.0.5\"},\n    \"wot-api\": {\"0.8.1\", \"0.8.2\", \"0.8.3\", \"0.8.4\"},\n}\n\nAFFECTED_PYPI: dict[str, set[str]] = {\n    \"mistralai\": {\"2.4.6\"},\n    \"guardrails-ai\": {\"0.10.1\"},\n}\n\nKNOWN_SHA256 = {\n    \"ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c\": \"router_init.js\",\n    \"2ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96\": \"router_init.js/tanstack_runner.js\",\n    \"2258284d65f63829bd67eaba01ef6f1ada2f593f9bbe41678b2df360bd90d3df\": \"setup.mjs\",\n    \"1e8538c6e0563d50da0f2e097e979ebd5294ce1defe01d0b9fe361ba3bed1898\": \"trojanized tarball\",\n}\nKNOWN_SHA1 = {\n    \"e7d582b98ca80690883175470e96f703ef6dc497\": \"router_init.js/tanstack_runner.js\",\n    \"12f35b1081b17d21815b35feb57ab03d02482116\": \"setup.mjs\",\n    \"820fa07a7328b6cf2b417078e103721d4d8f2e79\": \"opensearch_init.js\",\n}\n\nIOC_RE = re.compile(\n    r\"(\"\n    r\"79ac49eedf774dd4b0cfa308722bc463cfe5885c|\"\n    r\"@tanstack/setup|github:tanstack/router|\"\n    r\"router_init\\.js|router_runtime\\.js|tanstack_runner\\.js|opensearch_init\\.js|\"\n    r\"git-tanstack\\.com|83\\.142\\.209\\.194|filev2\\.getsession\\.org|\"\n    r\"seed[123]\\.getsession\\.org|api\\.masscan\\.cloud|litter\\.catbox\\.moe|\"\n    r\"gh-token-monitor|IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner|\"\n    r\"svksjrhjkcejg\"\n    r\")\",\n    re.IGNORECASE,\n)\n\nSUSPICIOUS_FILENAMES = {\n    \"router_init.js\",\n    \"router_runtime.js\",\n    \"tanstack_runner.js\",\n    \"opensearch_init.js\",\n    \"transformers.pyz\",\n    \"gh-token-monitor.sh\",\n    \"com.user.gh-token-monitor.plist\",\n}\n\nLOCKFILE_NAMES = {\n    \"package-lock.json\",\n    \"npm-shrinkwrap.json\",\n    \"pnpm-lock.yaml\",\n    \"yarn.lock\",\n    \"package.json\",\n    \"poetry.lock\",\n    \"uv.lock\",\n    \"Pipfile.lock\",\n    \"pyproject.toml\",\n}\n\nSKIP_DIRS = {\n    \".git\",\n    \".hg\",\n    \".svn\",\n    \".cache\",\n    \".Trash\",\n    \"Trash\",\n    \"__pycache__\",\n    \".mypy_cache\",\n    \".pytest_cache\",\n    \".next\",\n    \".turbo\",\n    \".parcel-cache\",\n    \"dist\",\n    \"build\",\n    \"target\",\n}\n\n\n@dataclass(frozen=True)\nclass Finding:\n    severity: str\n    category: str\n    path: str\n    detail: str\n\n\nclass Scanner:\n    def __init__(self, roots: list[Path], max_file_mb: int = 25, verbose: bool = False) -&gt; None:\n        self.roots = roots\n        self.max_file_bytes = max_file_mb * 1024 * 1024\n        self.verbose = verbose\n        self.findings: list[Finding] = []\n        self.errors: list[str] = []\n        self.node_modules_dirs: set[Path] = set()\n        self.seen_files: set[Path] = set()\n\n    def add(self, severity: str, category: str, path: Path | str, detail: str) -&gt; None:\n        self.findings.append(Finding(severity, category, str(path), detail))\n\n    def error(self, path: Path | str, detail: str) -&gt; None:\n        self.errors.append(f\"{path}: {detail}\")\n\n    def scan(self) -&gt; int:\n        self.scan_known_paths()\n        self.scan_processes()\n        self.scan_active_python_env()\n        self.walk_roots()\n        self.add_global_node_modules_dirs()\n        self.scan_node_modules_dirs()\n        self.print_report()\n        if self.has_actionable_findings():\n            return 1\n        return 0\n\n    def has_actionable_findings(self) -&gt; bool:\n        return any(f.severity in {\"CRITICAL\", \"HIGH\", \"MEDIUM\"} for f in self.findings)\n\n    def scan_known_paths(self) -&gt; None:\n        home = Path.home()\n        known = [\n            home / \"Library/LaunchAgents/com.user.gh-token-monitor.plist\",\n            home / \".config/systemd/user/gh-token-monitor.service\",\n            home / \".local/bin/gh-token-monitor.sh\",\n            Path(\"/tmp/transformers.pyz\"),\n        ]\n        for path in known:\n            if path.exists():\n                self.add(\"CRITICAL\", \"persistence/artifact\", path, \"known Mini Shai-Hulud persistence or payload path exists\")\n                self.scan_suspicious_file(path)\n\n    def scan_processes(self) -&gt; None:\n        try:\n            proc = subprocess.run(\n                [\"ps\", \"-axo\", \"pid=,command=\"],\n                text=True,\n                capture_output=True,\n                timeout=8,\n                check=False,\n            )\n        except Exception as exc:\n            self.error(\"ps\", f\"could not inspect process list: {exc}\")\n            return\n        needles = (\"gh-token-monitor\", \"router_runtime.js\", \"router_init.js\", \"tanstack_runner.js\", \"git-tanstack.com\")\n        for line in proc.stdout.splitlines():\n            if any(n in line for n in needles):\n                if Path(__file__).name in line:\n                    continue\n                self.add(\"CRITICAL\", \"process\", \"ps\", line.strip())\n\n    def scan_active_python_env(self) -&gt; None:\n        for dist in importlib.metadata.distributions():\n            try:\n                name = norm_pypi_name(dist.metadata[\"Name\"])\n                version = dist.version\n            except Exception:\n                continue\n            self.check_pypi_version(name, version, Path(str(dist.locate_file(\"\"))), \"active Python environment\")\n\n    def walk_roots(self) -&gt; None:\n        for root in self.roots:\n            if not root.exists():\n                self.error(root, \"root does not exist\")\n                continue\n            for dirpath, dirnames, filenames in os.walk(root, followlinks=False):\n                current = Path(dirpath)\n                self.prune_dirs(current, dirnames)\n\n                if current.name == \"node_modules\":\n                    self.node_modules_dirs.add(current)\n                    dirnames[:] = []\n                    continue\n\n                self.scan_dist_info_dir(current)\n\n                for name in filenames:\n                    path = current / name\n                    if path in self.seen_files:\n                        continue\n                    self.seen_files.add(path)\n                    self.inspect_interesting_file(path)\n\n    def prune_dirs(self, current: Path, dirnames: list[str]) -&gt; None:\n        home = Path.home()\n        keep: list[str] = []\n        for name in dirnames:\n            if name in SKIP_DIRS:\n                continue\n            if current == home and name == \"Library\":\n                # Known LaunchAgent path is checked directly; the rest is too large/noisy.\n                continue\n            if name.endswith(\".photoslibrary\"):\n                continue\n            keep.append(name)\n        dirnames[:] = keep\n\n    def inspect_interesting_file(self, path: Path) -&gt; None:\n        name = path.name\n        lower_path = path.as_posix().lower()\n\n        if name == \"settings.json\" and \"/.claude/\" in lower_path:\n            self.scan_claude_settings(path)\n            return\n        if name == \"tasks.json\" and \"/.vscode/\" in lower_path:\n            self.scan_vscode_tasks(path)\n            return\n\n        if name in SUSPICIOUS_FILENAMES:\n            self.scan_suspicious_file(path)\n            return\n        if name == \"setup.mjs\" and (\"/.claude/\" in lower_path or \"/.vscode/\" in lower_path):\n            self.add(\"HIGH\", \"ai-editor-persistence\", path, \"setup.mjs under .claude/.vscode\")\n            self.scan_suspicious_file(path)\n            return\n\n        if name in LOCKFILE_NAMES or is_requirements_file(name):\n            self.scan_lock_or_manifest(path)\n\n    def scan_claude_settings(self, path: Path) -&gt; None:\n        text = self.read_text(path)\n        if text is None:\n            return\n        if IOC_RE.search(text):\n            self.add(\"CRITICAL\", \"claude-code-persistence\", path, \"Claude settings contain Mini Shai-Hulud IOC\")\n        elif re.search(r\"SessionStart\", text, re.IGNORECASE):\n            self.add(\"MEDIUM\", \"claude-code-hook\", path, \"Claude SessionStart hook exists; review manually\")\n\n    def scan_vscode_tasks(self, path: Path) -&gt; None:\n        text = self.read_text(path)\n        if text is None:\n            return\n        has_folder_open = re.search(r\"runOn[\\\"'\\s:]+folderOpen\", text, re.IGNORECASE) or (\n            \"runOn\" in text and \"folderOpen\" in text\n        )\n        if IOC_RE.search(text):\n            self.add(\"CRITICAL\", \"vscode-persistence\", path, \"VS Code tasks contain Mini Shai-Hulud IOC\")\n        elif has_folder_open:\n            self.add(\"MEDIUM\", \"vscode-folder-open-task\", path, \"VS Code folder-open task exists; review manually\")\n\n    def scan_suspicious_file(self, path: Path) -&gt; None:\n        try:\n            size = path.stat().st_size\n        except OSError as exc:\n            self.error(path, f\"stat failed: {exc}\")\n            return\n\n        if size &gt; self.max_file_bytes:\n            self.add(\"MEDIUM\", \"large-suspicious-file\", path, f\"{path.name} exists but is larger than scan limit ({size} bytes)\")\n            return\n\n        try:\n            data = path.read_bytes()\n        except OSError as exc:\n            self.error(path, f\"read failed: {exc}\")\n            return\n\n        sha256 = hashlib.sha256(data).hexdigest()\n        sha1 = hashlib.sha1(data).hexdigest()\n        if sha256 in KNOWN_SHA256:\n            self.add(\"CRITICAL\", \"malware-hash\", path, f\"SHA256 matches {KNOWN_SHA256[sha256]} ({sha256})\")\n            return\n        if sha1 in KNOWN_SHA1:\n            self.add(\"CRITICAL\", \"malware-hash\", path, f\"SHA1 matches {KNOWN_SHA1[sha1]} ({sha1})\")\n            return\n\n        text_sample = data[: min(len(data), 5 * 1024 * 1024)].decode(\"utf-8\", errors=\"ignore\")\n        if IOC_RE.search(text_sample):\n            self.add(\"CRITICAL\", \"malware-string\", path, f\"{path.name} contains Mini Shai-Hulud IOC\")\n        elif path.name in {\"router_init.js\", \"router_runtime.js\", \"tanstack_runner.js\", \"opensearch_init.js\", \"transformers.pyz\"}:\n            self.add(\"HIGH\", \"suspicious-filename\", path, f\"{path.name} exists; hash/string did not match known IOC\")\n\n    def scan_lock_or_manifest(self, path: Path) -&gt; None:\n        if path.name in {\"package-lock.json\", \"npm-shrinkwrap.json\", \"package.json\"}:\n            if self.scan_json_npm_file(path):\n                return\n        text = self.read_text(path)\n        if text is None:\n            return\n        if path.name in {\"pnpm-lock.yaml\", \"yarn.lock\", \"bun.lock\", \"package-lock.json\", \"npm-shrinkwrap.json\", \"package.json\"}:\n            self.scan_npm_text(path, text)\n        if path.name in {\"poetry.lock\", \"uv.lock\", \"Pipfile.lock\", \"pyproject.toml\"} or is_requirements_file(path.name):\n            self.scan_pypi_text(path, text)\n        if IOC_RE.search(text):\n            self.add(\"HIGH\", \"lockfile-ioc\", path, \"lockfile/manifest contains Mini Shai-Hulud IOC\")\n\n    def scan_json_npm_file(self, path: Path) -&gt; bool:\n        text = self.read_text(path)\n        if text is None:\n            return False\n        try:\n            data = json.loads(text)\n        except json.JSONDecodeError:\n            return False\n\n        if isinstance(data, dict):\n            if path.name == \"package.json\":\n                self.scan_package_manifest_json(path, data)\n            packages = data.get(\"packages\")\n            if isinstance(packages, dict):\n                for key, meta in packages.items():\n                    if not isinstance(meta, dict):\n                        continue\n                    name = meta.get(\"name\") or name_from_node_modules_key(key)\n                    version = meta.get(\"version\")\n                    if isinstance(name, str) and isinstance(version, str):\n                        self.check_npm_version(name, version, path, f\"lockfile package entry {key}\")\n                    self.scan_manifest_dependency_iocs(path, meta, f\"lockfile package entry {key}\")\n            deps = data.get(\"dependencies\")\n            if isinstance(deps, dict):\n                self.scan_dependency_tree(path, deps, \"lockfile dependencies\")\n        return True\n\n    def scan_package_manifest_json(self, path: Path, data: dict) -&gt; None:\n        name = data.get(\"name\")\n        version = data.get(\"version\")\n        if isinstance(name, str) and isinstance(version, str):\n            self.check_npm_version(name, version, path, \"package.json package identity\")\n        self.scan_manifest_dependency_iocs(path, data, \"package.json dependencies\")\n        for section in (\"dependencies\", \"devDependencies\", \"optionalDependencies\", \"peerDependencies\"):\n            deps = data.get(section)\n            if not isinstance(deps, dict):\n                continue\n            for dep, spec in deps.items():\n                if isinstance(dep, str) and isinstance(spec, str):\n                    if dep in AFFECTED_NPM and spec.lstrip(\"=v\") in AFFECTED_NPM[dep]:\n                        self.add(\"HIGH\", \"npm-manifest-exposure\", path, f\"{dep}@{spec} in {section}\")\n                    if dep == \"@tanstack/setup\" or \"github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c\" in spec:\n                        self.add(\"CRITICAL\", \"npm-manifest-ioc\", path, f\"{dep}: {spec}\")\n\n    def scan_manifest_dependency_iocs(self, path: Path, data: dict, context: str) -&gt; None:\n        for section in (\"dependencies\", \"devDependencies\", \"optionalDependencies\", \"peerDependencies\"):\n            deps = data.get(section)\n            if not isinstance(deps, dict):\n                continue\n            for dep, spec in deps.items():\n                joined = f\"{dep}: {spec}\"\n                if IOC_RE.search(joined):\n                    self.add(\"CRITICAL\", \"npm-dependency-ioc\", path, f\"{context}: {joined}\")\n\n    def scan_dependency_tree(self, path: Path, deps: dict, context: str) -&gt; None:\n        for name, meta in deps.items():\n            if isinstance(meta, dict):\n                version = meta.get(\"version\")\n                if isinstance(version, str):\n                    self.check_npm_version(name, version, path, context)\n                child = meta.get(\"dependencies\")\n                if isinstance(child, dict):\n                    self.scan_dependency_tree(path, child, context)\n\n    def scan_npm_text(self, path: Path, text: str) -&gt; None:\n        for pkg, versions in AFFECTED_NPM.items():\n            if pkg not in text:\n                continue\n            for version in versions:\n                patterns = [\n                    re.escape(pkg) + r\"@\" + re.escape(version) + r\"(\\b|[^0-9A-Za-z_.-])\",\n                    re.escape(pkg) + r\"['\\\"]?\\s*[:@]\\s*['\\\"]?[\\^~=&gt; ]*\" + re.escape(version) + r\"\\b\",\n                    re.escape(pkg) + r\"[\\s\\S]{0,500}?version['\\\"]?\\s*[:=]\\s*['\\\"]\" + re.escape(version) + r\"['\\\"]\",\n                ]\n                if any(re.search(p, text) for p in patterns):\n                    self.add(\"HIGH\", \"npm-lockfile-exposure\", path, f\"{pkg}@{version}\")\n                    break\n\n    def scan_pypi_text(self, path: Path, text: str) -&gt; None:\n        norm_text = text.lower()\n        for pkg, versions in AFFECTED_PYPI.items():\n            if pkg not in norm_text and pkg.replace(\"-\", \"_\") not in norm_text:\n                continue\n            for version in versions:\n                pkg_re = re.escape(pkg).replace(\"\\\\-\", \"[-_.]\")\n                patterns = [\n                    r\"(?im)^\\s*\" + pkg_re + r\"\\s*(==|===)\\s*\" + re.escape(version) + r\"\\b\",\n                    r\"name\\s*=\\s*['\\\"]\" + pkg_re + r\"['\\\"][\\s\\S]{0,300}?version\\s*=\\s*['\\\"]\" + re.escape(version) + r\"['\\\"]\",\n                    pkg_re + r\"[\\s\\S]{0,300}?\" + re.escape(version),\n                ]\n                if any(re.search(p, text, flags=re.IGNORECASE) for p in patterns):\n                    self.add(\"HIGH\", \"pypi-lockfile-exposure\", path, f\"{pkg}=={version}\")\n                    break\n\n    def scan_dist_info_dir(self, path: Path) -&gt; None:\n        name = path.name\n        if not name.endswith(\".dist-info\"):\n            return\n        match = re.match(r\"(?P.+)-(?P[0-9][^-]*)\\.dist-info$\", name)\n        if match:\n            pkg = norm_pypi_name(match.group(\"name\"))\n            version = match.group(\"version\")\n            self.check_pypi_version(pkg, version, path, \"installed Python dist-info\")\n            return\n        metadata = path / \"METADATA\"\n        if metadata.exists():\n            text = self.read_text(metadata)\n            if text:\n                pkg_match = re.search(r\"(?im)^Name:\\s*(.+)$\", text)\n                version_match = re.search(r\"(?im)^Version:\\s*(.+)$\", text)\n                if pkg_match and version_match:\n                    self.check_pypi_version(\n                        norm_pypi_name(pkg_match.group(1).strip()),\n                        version_match.group(1).strip(),\n                        metadata,\n                        \"installed Python metadata\",\n                    )\n\n    def add_global_node_modules_dirs(self) -&gt; None:\n        for cmd in ([\"npm\", \"root\", \"-g\"], [\"pnpm\", \"root\", \"-g\"]):\n            try:\n                proc = subprocess.run(cmd, text=True, capture_output=True, timeout=8, check=False)\n            except Exception:\n                continue\n            if proc.returncode == 0:\n                path = Path(proc.stdout.strip())\n                if path.exists():\n                    self.node_modules_dirs.add(path)\n\n    def scan_node_modules_dirs(self) -&gt; None:\n        for nm in sorted(self.node_modules_dirs):\n            for pkg, versions in AFFECTED_NPM.items():\n                pkg_path = nm / package_to_path(pkg)\n                package_json = pkg_path / \"package.json\"\n                if package_json.exists():\n                    text = self.read_text(package_json)\n                    if not text:\n                        continue\n                    try:\n                        data = json.loads(text)\n                    except json.JSONDecodeError:\n                        self.scan_npm_text(package_json, text)\n                        continue\n                    version = data.get(\"version\")\n                    if isinstance(version, str):\n                        self.check_npm_version(pkg, version, package_json, \"installed node_modules package\")\n                    for suspicious in (\"router_init.js\", \"tanstack_runner.js\", \"router_runtime.js\", \"setup.mjs\", \"opensearch_init.js\"):\n                        candidate = pkg_path / suspicious\n                        if candidate.exists():\n                            self.scan_suspicious_file(candidate)\n\n    def check_npm_version(self, name: str, version: str, path: Path, context: str) -&gt; None:\n        if name in AFFECTED_NPM and version in AFFECTED_NPM[name]:\n            self.add(\"HIGH\", \"npm-exposure\", path, f\"{name}@{version} ({context})\")\n\n    def check_pypi_version(self, name: str, version: str, path: Path, context: str) -&gt; None:\n        norm = norm_pypi_name(name)\n        if norm in AFFECTED_PYPI and version in AFFECTED_PYPI[norm]:\n            self.add(\"HIGH\", \"pypi-exposure\", path, f\"{norm}=={version} ({context})\")\n\n    def read_text(self, path: Path) -&gt; str | None:\n        try:\n            size = path.stat().st_size\n            if size &gt; self.max_file_bytes:\n                if self.verbose:\n                    self.error(path, f\"skipped large file ({size} bytes)\")\n                return None\n            return path.read_text(encoding=\"utf-8\", errors=\"ignore\")\n        except OSError as exc:\n            self.error(path, f\"read failed: {exc}\")\n            return None\n\n    def print_report(self) -&gt; None:\n        print(\"Mini Shai-Hulud / TanStack campaign scanner\")\n        print(f\"Roots scanned: {', '.join(str(p) for p in self.roots)}\")\n        print(f\"Node modules roots inspected: {len(self.node_modules_dirs)}\")\n        print()\n\n        severity_order = {\"CRITICAL\": 0, \"HIGH\": 1, \"MEDIUM\": 2, \"INFO\": 3}\n        findings = sorted(self.findings, key=lambda f: (severity_order.get(f.severity, 9), f.path, f.detail))\n        if findings:\n            print(\"FINDINGS:\")\n            for f in findings:\n                print(f\"- [{f.severity}] {f.category}: {f.path}\")\n                print(f\"  {f.detail}\")\n            print()\n        else:\n            print(\"FINDINGS: none\")\n            print()\n\n        if self.errors and self.verbose:\n            print(\"SCAN WARNINGS:\")\n            for err in self.errors[:50]:\n                print(f\"- {err}\")\n            if len(self.errors) &gt; 50:\n                print(f\"- ... {len(self.errors) - 50} more\")\n            print()\n\n        if any(f.severity in {\"CRITICAL\", \"HIGH\"} for f in findings):\n            print(\"RESULT: LIKELY AFFECTED OR EXPOSED\")\n            print(\"Do not rotate GitHub/npm/cloud tokens until gh-token-monitor persistence is contained/removed.\")\n        elif any(f.severity == \"MEDIUM\" for f in findings):\n            print(\"RESULT: SUSPICIOUS CONFIG FOUND; REVIEW MANUALLY\")\n        else:\n            print(\"RESULT: NO KNOWN MINI SHAI-HULUD INDICATORS FOUND\")\n\n\ndef is_requirements_file(name: str) -&gt; bool:\n    lower = name.lower()\n    return lower == \"requirements.txt\" or (lower.startswith(\"requirements\") and lower.endswith(\".txt\"))\n\n\ndef norm_pypi_name(name: str) -&gt; str:\n    return re.sub(r\"[-_.]+\", \"-\", name).lower()\n\n\ndef package_to_path(package: str) -&gt; Path:\n    return Path(*package.split(\"/\"))\n\n\ndef name_from_node_modules_key(key: str) -&gt; str | None:\n    prefix = \"node_modules/\"\n    if prefix not in key:\n        return None\n    return key.rsplit(prefix, 1)[-1]\n\n\ndef default_roots() -&gt; list[Path]:\n    home = Path.home().resolve()\n    cwd = Path.cwd().resolve()\n    roots = [home]\n    if home not in cwd.parents and cwd != home:\n        roots.append(cwd)\n    return roots\n\n\ndef parse_args(argv: list[str]) -&gt; argparse.Namespace:\n    parser = argparse.ArgumentParser(description=\"Read-only scanner for Mini Shai-Hulud campaign indicators.\")\n    parser.add_argument(\"roots\", nargs=\"*\", type=Path, help=\"roots to scan; defaults to your home directory\")\n    parser.add_argument(\"--max-file-mb\", type=int, default=25, help=\"max file size to inspect, default 25\")\n    parser.add_argument(\"--verbose\", action=\"store_true\", help=\"print scan warnings\")\n    return parser.parse_args(argv)\n\n\ndef main(argv: list[str]) -&gt; int:\n    args = parse_args(argv)\n    roots = [p.expanduser().resolve() for p in args.roots] if args.roots else default_roots()\n    try:\n        scanner = Scanner(roots=roots, max_file_mb=args.max_file_mb, verbose=args.verbose)\n        return scanner.scan()\n    except KeyboardInterrupt:\n        print(\"Interrupted\", file=sys.stderr)\n        return 2\n    except Exception as exc:\n        print(f\"Scanner error: {exc}\", file=sys.stderr)\n        return 2\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main(sys.argv[1:]))\n", "creation_timestamp": "2026-05-12T20:26:22.000000Z"}, {"uuid": "6ff16318-9616-4954-ab37-b29fc8b231d6", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "GHSA-g7cv-rxg3-hmpx", "type": "seen", "source": "https://gist.github.com/gabrielstuff/1ec8cb4a351745895c76dfadc789090a", "content": "#!/usr/bin/env bash\n# Mini Shai-Hulud / TanStack npm supply-chain compromise check.\n# Safe to run \u2014 read-only, no network calls, no modifications.\n# Works in bash and zsh. macOS + Linux.\n#\n# Based on IOCs published by Socket.dev (2026-05-11) and OpenAI's response\n# (2026-05-13). Covers TanStack, Mistral AI, OpenSearch, Guardrails AI,\n# Squawk, and the gh-token-monitor stealer drop.\n\nset +e\nset -u\n\n# ---------- colors ----------\nRED=$'\\033[31m'; GRN=$'\\033[32m'; YLW=$'\\033[33m'; BLD=$'\\033[1m'; DIM=$'\\033[2m'; RST=$'\\033[0m'\n[ -t 1 ] || { RED=\"\"; GRN=\"\"; YLW=\"\"; BLD=\"\"; DIM=\"\"; RST=\"\"; }\n\nHITS=0\nFINDINGS=()\n\nsection() { printf '\\n%s\u2500\u2500 %s \u2500\u2500%s\\n' \"$BLD\" \"$1\" \"$RST\"; }\nok()      { printf '  %sok%s    %s\\n' \"$GRN\" \"$RST\" \"$1\"; }\nbad()     { HITS=$((HITS + 1)); FINDINGS+=(\"$1\"); printf '  %sHIT%s   %s\\n' \"$RED\" \"$RST\" \"$1\"; }\nnote()    { printf '  %s%s%s\\n' \"$DIM\" \"$1\" \"$RST\"; }\n\n# Known malicious SHA-256s (Socket.dev + Mistral AI advisory)\nSHA_ROUTER_INIT=\"ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c\"\nSHA_TANSTACK_RUNNER=\"2ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96\"\nSHA_TANSTACK_SETUP_PKG=\"7c12d8614c624c70d6dd6fc2ee289332474abaa38f70ebe2cdef064923ca3a9b\"\n\n# Compromised commits in TanStack/router fork network\nBAD_COMMIT=\"79ac49eedf774dd4b0cfa308722bc463cfe5885c\"\nBAD_COMMIT_FORK=\"65bf499d16a5e8d25ba95d69ec9790a6dd4a1f14\"\n\n# Known C2 / attacker infra (domains + IP)\nBAD_HOSTS='filev2\\.getsession\\.org|seed[123]\\.getsession\\.org|git-tanstack\\.com|api\\.masscan\\.cloud|litter\\.catbox\\.moe|83\\.142\\.209\\.194'\n\n# Attacker GitHub identities\nBAD_GH_USERS='zblgg|voicproducoes|zblgg/configuration'\n\n# Compromised npm/PyPI artifacts (specific versions only)\n# Format: name@version, one per line\nBAD_PKGS=$(cat &lt;&lt;'EOF'\n@mistralai/mistralai@2.2.2\n@mistralai/mistralai@2.2.3\n@mistralai/mistralai@2.2.4\n@mistralai/mistralai-azure@1.7.1\n@mistralai/mistralai-azure@1.7.2\n@mistralai/mistralai-azure@1.7.3\n@mistralai/mistralai-gcp@1.7.1\n@mistralai/mistralai-gcp@1.7.2\n@mistralai/mistralai-gcp@1.7.3\n@opensearch-project/opensearch@3.5.3\n@opensearch-project/opensearch@3.6.2\n@opensearch-project/opensearch@3.7.0\n@opensearch-project/opensearch@3.8.0\n@squawk/mcp@0.9.2\n@squawk/mcp@0.9.3\n@squawk/mcp@0.9.4\n@squawk/mcp@0.9.5\n@squawk/weather@0.5.10\n@squawk/flightplan@0.5.6\nguardrails-ai@0.10.1\nmistralai@2.4.6\ncross-stitch@1.1.4\ncross-stitch@1.1.5\ncross-stitch@1.1.6\ncross-stitch@1.1.7\nts-dna@3.0.3\nts-dna@3.0.4\nts-dna@3.0.5\ngit-git-git@1.0.10\ngit-git-git@1.0.11\ngit-git-git@1.0.12\ngit-branch-selector@1.3.6\ngit-branch-selector@1.3.7\nnextmove-mcp@0.1.5\nnextmove-mcp@0.1.6\nnextmove-mcp@0.1.7\ncmux-agent-mcp@0.1.7\ncmux-agent-mcp@0.1.8\nEOF\n)\n\ncheck_path() {\n  local label=\"$1\" p=\"$2\"\n  if [ -e \"$p\" ] || [ -L \"$p\" ]; then\n    bad \"$label exists \u2192 $p\"\n    ls -la \"$p\" 2&gt;/dev/null | sed 's/^/         /'\n  else\n    ok \"$label not present\"\n  fi\n}\n\n# pick a sha256 tool\nSHA=\"\"\ncommand -v shasum &gt;/dev/null 2&gt;&amp;1 &amp;&amp; SHA=\"shasum -a 256\"\n[ -z \"$SHA\" ] &amp;&amp; command -v sha256sum &gt;/dev/null 2&gt;&amp;1 &amp;&amp; SHA=\"sha256sum\"\n\n# ---------- header ----------\ncat &lt;/dev/null)\nuser:  $(id -un 2&gt;/dev/null)\nos:    $(uname -srm 2&gt;/dev/null)\nshell: ${SHELL:-unknown}\ndate:  $(date -u '+%Y-%m-%dT%H:%M:%SZ')\nEOF\n\n# ---------- 1. malicious payload files anywhere (incl. node_modules) ----------\nsection \"1. Payload files (router_init.js, tanstack_runner.js, vite_setup.mjs)\"\nnote \"IOC files dropped into compromised TanStack/Mistral/etc. packages\"\nnote \"scanning node_modules \u2014 that's where the files live\"\n\nRI_LIST=$(find \"$HOME\" \\\n            \\( -name .Trash -o -name Library \\) -prune \\\n            -o -type f \\( -name router_init.js -o -name tanstack_runner.js -o -name vite_setup.mjs \\) -print 2&gt;/dev/null)\n\nif [ -n \"$RI_LIST\" ]; then\n  bad \"malicious payload file(s) found\"\n  while IFS= read -r f; do\n    printf '         %s\\n' \"$f\"\n    if [ -n \"$SHA\" ]; then\n      h=$($SHA \"$f\" 2&gt;/dev/null | awk '{print $1}')\n      case \"$h\" in\n        \"$SHA_ROUTER_INIT\"|\"$SHA_TANSTACK_RUNNER\")\n          printf '         %s\u21b3 SHA-256 MATCHES KNOWN MALWARE: %s%s\\n' \"$RED\" \"$h\" \"$RST\" ;;\n        *)\n          printf '         %s\u21b3 sha256: %s%s\\n' \"$DIM\" \"$h\" \"$RST\" ;;\n      esac\n    fi\n  done &lt;&lt;&lt; \"$RI_LIST\"\nelse\n  ok \"no router_init.js / tanstack_runner.js / vite_setup.mjs found\"\nfi\n\n# ---------- 2. gh-token-monitor stealer persistence ----------\nsection \"2. gh-token-monitor stealer persistence\"\ncheck_path \"launch script    \" \"$HOME/.local/bin/gh-token-monitor.sh\"\ncheck_path \"systemd unit     \" \"$HOME/.config/systemd/user/gh-token-monitor.service\"\ncheck_path \"macOS LaunchAgent\" \"$HOME/Library/LaunchAgents/com.user.gh-token-monitor.plist\"\n\n# ---------- 3. agent / editor config dir drops ----------\nsection \"3. Suspicious scripts in agent + editor config dirs\"\nnote \"covers .claude .codex .opencode .cursor .windsurf .aider .continue .zed .trae .vscode\"\n\nAGENT_DIRS='.claude|.codex|.opencode|.cursor|.windsurf|.aider|.continue|.zed|.trae|.vscode|.copilot|.github-copilot'\nDROP_HITS=$(find \"$HOME\" \\\n              \\( -name node_modules -o -name extensions -o -name .Trash -o -name Library \\) -prune \\\n              -o -type f \\( -name 'setup.mjs' -o -name 'router_runtime.js' -o -name 'router_init.js' -o -name 'tanstack_runner.js' \\) \\\n              -print 2&gt;/dev/null \\\n            | grep -E \"/($AGENT_DIRS)/\")\nif [ -n \"$DROP_HITS\" ]; then\n  bad \"suspicious script(s) in agent/editor config dirs\"\n  printf '%s\\n' \"$DROP_HITS\" | sed 's/^/         /'\nelse\n  ok \"no suspicious scripts in agent/editor config dirs\"\nfi\n\n# ---------- 4. .claude/settings.json hooks ----------\nsection \"4. .claude/settings.json with unexpected hooks\"\nSETTINGS_HITS=\"\"\nwhile IFS= read -r f; do\n  case \"$f\" in */node_modules/*) continue ;; esac\n  if grep -Ei \"router_init|router_runtime|tanstack_runner|vite_setup|setup\\.mjs|$BAD_HOSTS\" \"$f\" &gt;/dev/null 2&gt;&amp;1; then\n    SETTINGS_HITS=\"${SETTINGS_HITS}${f}\"$'\\n'\n  fi\ndone &lt; &lt;(find \"$HOME\" \\\n            \\( -name node_modules -o -name .Trash -o -name Library \\) -prune \\\n            -o -path '*/.claude/settings.json' -type f -print 2&gt;/dev/null)\nif [ -n \"$SETTINGS_HITS\" ]; then\n  bad \".claude/settings.json contains IOC strings\"\n  printf '%s' \"$SETTINGS_HITS\" | sed 's/^/         /'\nelse\n  ok \".claude/settings.json files clean\"\nfi\n\n# ---------- 5. .vscode/tasks.json content ----------\nsection \"5. .vscode/tasks.json containing payload\"\nnote \"tasks.json is normal \u2014 only flagged if it contains IOC strings\"\nTJ_HITS=\"\"\nwhile IFS= read -r f; do\n  case \"$f\" in */node_modules/*|*/extensions/*) continue ;; esac\n  if grep -lEi \"gh-token-monitor|router_init|router_runtime|tanstack_runner|vite_setup|setup\\.mjs|$BAD_HOSTS|curl .*\\| *(sh|bash|node)|wget .*\\| *(sh|bash|node)\" \"$f\" &gt;/dev/null 2&gt;&amp;1; then\n    TJ_HITS=\"${TJ_HITS}${f}\"$'\\n'\n  fi\ndone &lt; &lt;(find \"$HOME\" \\\n            \\( -name node_modules -o -name extensions -o -name .Trash -o -name Library \\) -prune \\\n            -o -path '*/.vscode/tasks.json' -type f -print 2&gt;/dev/null)\nif [ -n \"$TJ_HITS\" ]; then\n  bad \"tasks.json with malware payload\"\n  printf '%s' \"$TJ_HITS\" | sed 's/^/         /'\nelse\n  ok \"no tasks.json contains payload strings\"\nfi\n\n# ---------- 6. /tmp/transformers.pyz (guardrails-ai variant) ----------\nsection \"6. /tmp/transformers.pyz (guardrails-ai payload)\"\nif [ -e /tmp/transformers.pyz ]; then\n  bad \"/tmp/transformers.pyz exists\"\n  ls -la /tmp/transformers.pyz 2&gt;/dev/null | sed 's/^/         /'\nelse\n  ok \"/tmp/transformers.pyz not present\"\nfi\n\n# ---------- 7. compromised packages in lockfiles ----------\nsection \"7. Compromised package versions in lockfiles\"\nnote \"scans package-lock.json / pnpm-lock.yaml / yarn.lock / package.json / requirements.txt\"\n\nLOCK_HITS=\"\"\nLOCK_FILES=$(find \"$HOME\" \\\n              \\( -name node_modules -o -name .Trash -o -name Library -o -name .git \\) -prune \\\n              -o -type f \\( -name 'package-lock.json' -o -name 'pnpm-lock.yaml' -o -name 'yarn.lock' -o -name 'package.json' -o -name 'requirements.txt' -o -name 'poetry.lock' -o -name 'uv.lock' \\) -print 2&gt;/dev/null)\n\nwhile IFS= read -r pv; do\n  [ -z \"$pv\" ] &amp;&amp; continue\n  name=\"${pv%@*}\"\n  ver=\"${pv##*@}\"\n  # Build a tolerant pattern. e.g. \"@tanstack/react-router\" at \"1.2.3\"\n  # Match common lockfile encodings.\n  while IFS= read -r lf; do\n    [ -z \"$lf\" ] &amp;&amp; continue\n    if grep -F -e \"\\\"${name}\\\": \\\"${ver}\\\"\" \\\n                -e \"\\\"${name}@${ver}\\\"\" \\\n                -e \"${name}@${ver}:\" \\\n                -e \"${name}==${ver}\" \\\n                -e \"name = \\\"${name}\\\"\" \"$lf\" &gt;/dev/null 2&gt;&amp;1; then\n      # second pass: if matched name= alone (poetry/uv), verify version proximity\n      if grep -F -e \"\\\"${name}\\\": \\\"${ver}\\\"\" -e \"\\\"${name}@${ver}\\\"\" -e \"${name}@${ver}:\" -e \"${name}==${ver}\" \"$lf\" &gt;/dev/null 2&gt;&amp;1 \\\n         || awk -v n=\"$name\" -v v=\"$ver\" '\n             /^\\[\\[package\\]\\]/ {pkg=\"\"; ver=\"\"}\n             $1==\"name\" {gsub(/\"/,\"\",$3); pkg=$3}\n             $1==\"version\" {gsub(/\"/,\"\",$3); ver=$3}\n             pkg==n &amp;&amp; ver==v {found=1; exit}\n             END{exit !found}\n           ' \"$lf\" 2&gt;/dev/null; then\n        LOCK_HITS=\"${LOCK_HITS}${pv}  \u2190  ${lf}\"$'\\n'\n      fi\n    fi\n  done &lt;&lt;&lt; \"$LOCK_FILES\"\ndone &lt;&lt;&lt; \"$BAD_PKGS\"\n\nif [ -n \"$LOCK_HITS\" ]; then\n  bad \"compromised package version(s) installed/locked\"\n  printf '%s' \"$LOCK_HITS\" | sed 's/^/         /'\nelse\n  ok \"no compromised package versions found in lockfiles\"\nfi\n\n# ---------- 8. malicious optionalDependencies / commit hash ----------\nsection \"8. package.json with attacker optionalDependencies or commit hash\"\nOPT_HITS=\"\"\nwhile IFS= read -r pj; do\n  if grep -F -e \"$BAD_COMMIT\" -e \"$BAD_COMMIT_FORK\" -e '@tanstack/setup' -e 'github:tanstack/router#79ac49ee' -e 'zblgg/configuration' \"$pj\" &gt;/dev/null 2&gt;&amp;1; then\n    OPT_HITS=\"${OPT_HITS}${pj}\"$'\\n'\n  fi\ndone &lt; &lt;(find \"$HOME\" \\\n            \\( -name .Trash -o -name Library \\) -prune \\\n            -o -type f -name 'package.json' -print 2&gt;/dev/null)\nif [ -n \"$OPT_HITS\" ]; then\n  bad \"package.json references attacker commit / @tanstack/setup\"\n  printf '%s' \"$OPT_HITS\" | sed 's/^/         /'\nelse\n  ok \"no package.json references attacker commit\"\nfi\n\n# ---------- 9. running processes ----------\nsection \"9. Running processes\"\nPROC_HITS=$(ps -Aewwo pid,user,command 2&gt;/dev/null \\\n            | grep -Ei 'gh-token-monitor|router_init|router_runtime|tanstack_runner|vite_setup|\\.claude/setup\\.mjs|\\.vscode/setup\\.mjs|transformers\\.pyz|MISTRAL_INIT=1' \\\n            | grep -v grep)\nif [ -n \"$PROC_HITS\" ]; then\n  bad \"suspicious process(es) running\"\n  printf '%s\\n' \"$PROC_HITS\" | sed 's/^/         /'\nelse\n  ok \"no suspicious processes running\"\nfi\n\n# ---------- 10. LaunchAgents/Daemons (macOS) ----------\nif [ \"$(uname)\" = \"Darwin\" ]; then\n  section \"10. macOS LaunchAgents/Daemons referencing malware\"\n  LA_HITS=$(find \\\n              \"$HOME/Library/LaunchAgents\" \\\n              \"/Library/LaunchAgents\" \\\n              \"/Library/LaunchDaemons\" \\\n              -type f -name '*.plist' 2&gt;/dev/null \\\n            | xargs grep -lEi \"gh-token-monitor|router_init|router_runtime|tanstack_runner|vite_setup|$BAD_HOSTS\" 2&gt;/dev/null)\n  if [ -n \"$LA_HITS\" ]; then\n    bad \"LaunchAgent/Daemon references malware\"\n    printf '%s\\n' \"$LA_HITS\" | sed 's/^/         /'\n  else\n    ok \"no LaunchAgent/Daemon references malware\"\n  fi\nfi\n\n# ---------- 11. shell rc + cron ----------\nsection \"11. Shell startup files + crontab\"\nRC_FILES=(\"$HOME/.bashrc\" \"$HOME/.bash_profile\" \"$HOME/.zshrc\" \"$HOME/.zprofile\" \"$HOME/.profile\")\nRC_HITS=\"\"\nfor rc in \"${RC_FILES[@]}\"; do\n  [ -f \"$rc\" ] || continue\n  if grep -Ei \"gh-token-monitor|router_init|router_runtime|tanstack_runner|vite_setup|setup\\.mjs|$BAD_HOSTS|MISTRAL_INIT\" \"$rc\" &gt;/dev/null 2&gt;&amp;1; then\n    RC_HITS=\"${RC_HITS}${rc}\"$'\\n'\n  fi\ndone\nif [ -n \"$RC_HITS\" ]; then\n  bad \"shell startup file modified\"\n  printf '%s' \"$RC_HITS\" | sed 's/^/         /'\nelse\n  ok \"shell startup files clean\"\nfi\nCRON_OUT=$(crontab -l 2&gt;/dev/null | grep -Ei \"gh-token-monitor|router_init|router_runtime|vite_setup|setup\\.mjs|transformers\\.pyz|$BAD_HOSTS\")\nif [ -n \"$CRON_OUT\" ]; then\n  bad \"crontab contains malware reference\"\n  printf '%s\\n' \"$CRON_OUT\" | sed 's/^/         /'\nelse\n  ok \"crontab clean\"\nfi\n\n# ---------- 12. C2 hosts in /etc/hosts ----------\nsection \"12. C2 domains in /etc/hosts\"\nHOSTS_HITS=$(grep -Ei \"$BAD_HOSTS\" /etc/hosts 2&gt;/dev/null)\nif [ -n \"$HOSTS_HITS\" ]; then\n  bad \"/etc/hosts mentions C2 domain\"\n  printf '%s\\n' \"$HOSTS_HITS\" | sed 's/^/         /'\nelse\n  ok \"/etc/hosts does not mention known C2 domains\"\nfi\n\n# ---------- 13. git commits attributed to claude@users.noreply ----------\n# Only spot-check repos under common dev dirs; full-disk scan would be too slow.\nsection \"13. Recent git commits as claude@users.noreply.github.com\"\nnote \"looks across ~/Developer ~/Projects ~/Code ~/src ~/repos for repos with such commits\"\nnote \"Socket.dev: attacker forged commits under this identity\"\nGIT_HITS=\"\"\nfor base in \"$HOME/Developer\" \"$HOME/Projects\" \"$HOME/Code\" \"$HOME/src\" \"$HOME/repos\" \"$HOME/work\"; do\n  [ -d \"$base\" ] || continue\n  while IFS= read -r gd; do\n    repo=\"${gd%/.git}\"\n    out=$(git -C \"$repo\" log --all --since='2026-04-01' --author='claude@users.noreply.github.com' --pretty='%h %ad %s' --date=short 2&gt;/dev/null | head -n 5)\n    if [ -n \"$out\" ]; then\n      GIT_HITS=\"${GIT_HITS}${repo}:\"$'\\n'\"${out}\"$'\\n\\n'\n    fi\n  done &lt; &lt;(find \"$base\" -maxdepth 4 -type d -name .git 2&gt;/dev/null)\ndone\nif [ -n \"$GIT_HITS\" ]; then\n  bad \"commits authored as claude@users.noreply.github.com (verify each was made by the legit Claude Code App)\"\n  printf '%s' \"$GIT_HITS\" | sed 's/^/         /'\nelse\n  ok \"no recent commits by claude@users.noreply.github.com in scanned repos\"\nfi\n\n# ---------- 14. git remotes pointing at attacker fork ----------\nsection \"14. Git remotes referencing attacker fork or commits\"\nREMOTE_HITS=\"\"\nfor base in \"$HOME/Developer\" \"$HOME/Projects\" \"$HOME/Code\" \"$HOME/src\" \"$HOME/repos\" \"$HOME/work\"; do\n  [ -d \"$base\" ] || continue\n  while IFS= read -r gd; do\n    repo=\"${gd%/.git}\"\n    rem=$(git -C \"$repo\" remote -v 2&gt;/dev/null | grep -Ei \"$BAD_GH_USERS\")\n    if [ -n \"$rem\" ]; then\n      REMOTE_HITS=\"${REMOTE_HITS}${repo}:\"$'\\n'\"${rem}\"$'\\n\\n'\n    fi\n    # also check if the attacker commits are reachable in any local clone\n    for c in \"$BAD_COMMIT\" \"$BAD_COMMIT_FORK\"; do\n      if git -C \"$repo\" cat-file -e \"$c^{commit}\" 2&gt;/dev/null; then\n        REMOTE_HITS=\"${REMOTE_HITS}${repo}: contains attacker commit $c\"$'\\n\\n'\n      fi\n    done\n  done &lt; &lt;(find \"$base\" -maxdepth 4 -type d -name .git 2&gt;/dev/null)\ndone\nif [ -n \"$REMOTE_HITS\" ]; then\n  bad \"git remote/commit references attacker account or commit\"\n  printf '%s' \"$REMOTE_HITS\" | sed 's/^/         /'\nelse\n  ok \"no git remotes/commits reference attacker accounts\"\nfi\n\n# ---------- summary ----------\nprintf '\\n%s\u2550\u2550\u2550\u2550 Result \u2550\u2550\u2550\u2550%s\\n' \"$BLD\" \"$RST\"\nif [ \"$HITS\" -eq 0 ]; then\n  cat &lt;\"\n\nReferences:\n  - https://openai.com/index/our-response-to-the-tanstack-npm-supply-chain-attack/\n  - https://socket.dev/blog/tanstack-npm-packages-compromised-mini-shai-hulud-supply-chain-attack\n  - https://tanstack.com/blog/npm-supply-chain-compromise-postmortem\n  - https://docs.mistral.ai/resources/security-advisories\n  - GitHub Security Advisory: GHSA-g7cv-rxg3-hmpx (TanStack)\n  - GitHub Security Advisory: GHSA-jgg6-4rpr-wfh7 (Mistral npm)\n  - GitHub Security Advisory: GHSA-wx9m-wx4f-4cmg (Mistral PyPI)\n\nverdict: SUSPECT\nEOF\nfi\n\nexit 0", "creation_timestamp": "2026-05-15T08:34:35.000000Z"}, {"uuid": "b38da67f-af3d-4542-837c-407bb3a81fe1", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "GHSA-G7CV-RXG3-HMPX", "type": "seen", "source": "https://t.me/bdufstecru/3161", "content": "\u0423\u044f\u0437\u0432\u0438\u043c\u043e\u0441\u0442\u044c \u043d\u0430\u0431\u043e\u0440\u0430 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a TanStack \u0441\u0432\u044f\u0437\u0430\u043d\u0430 \u0441 \u043d\u0430\u043b\u0438\u0447\u0438\u0435\u043c \u043d\u0435\u0434\u0435\u043a\u043b\u0430\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0435\u0439. \u042d\u043a\u0441\u043f\u043b\u0443\u0430\u0442\u0430\u0446\u0438\u044f \u0443\u044f\u0437\u0432\u0438\u043c\u043e\u0441\u0442\u0438 \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u044c \u043d\u0430\u0440\u0443\u0448\u0438\u0442\u0435\u043b\u044e, \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u043c\u0443 \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e, \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u0434\n\nBDU:2026-06725\nCVE-2026-45321\n\n\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0430\u0446\u0438\u0439 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044f:\nhttps://github.com/TanStack/router/security/advisories/GHSA-g7cv-rxg3-hmpx\nhttps://github.com/TanStack/router/issues/7383", "creation_timestamp": "2026-05-15T12:57:17.000000Z"}, {"uuid": "5dc5c7a2-839b-461c-b86e-803591d73254", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "GHSA-G7CV-RXG3-HMPX", "type": "seen", "source": "https://t.me/GithubRedTeam/84445", "content": "\ud83d\udea8 GitHub \u76d1\u63a7\u6d88\u606f\u63d0\u9192\n\n\ud83d\udea8 \u53d1\u73b0\u5173\u952e\u8bcd\uff1a #CVE-2026\n\n\ud83d\udce6 \u9879\u76ee\u540d\u79f0\uff1a tanstack-compromise-checker\n\ud83d\udc64 \u9879\u76ee\u4f5c\u8005\uff1a fabriziosalmi\n\ud83d\udee0 \u5f00\u53d1\u8bed\u8a00\uff1a Shell\n\u2b50 Star\u6570\u91cf\uff1a 0  |  \ud83c\udf74 Fork\u6570\u91cf\uff1a 0\n\ud83d\udcc5 \u66f4\u65b0\u65f6\u95f4\uff1a 2026-05-16 11:51:00\n\n\ud83d\udcdd \u9879\u76ee\u63cf\u8ff0\uff1a\nShell script to detect TanStack npm supply chain attack indicators (CVE-2026-45321 / GHSA-g7cv-rxg3-hmpx)\n\n\ud83d\udd17 \u70b9\u51fb\u8bbf\u95ee\u9879\u76ee\u5730\u5740", "creation_timestamp": "2026-05-16T12:00:04.000000Z"}, {"uuid": "a5cb111e-2d5f-44c6-8a29-6ab730248cb4", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "GHSA-g7cv-rxg3-hmpx", "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"}]}