#!/bin/bash # # Cassandra · malicious extension check · by Ciphero # # What this script does, in order: # 1. Lists the extensions installed in your editors (VS Code, Cursor, # Windsurf and other forks) and browsers (Chrome, Edge, Brave, Arc, # Firefox and others), by folder name: id and version only. # 2. Checks a fixed list of paths (below) for the traces of specific # incidents and records yes / no. It never reads or sends file contents. # 3. Lists AI tool names and the NAMES (never values) of API-key variables. # 4. Shows you every fact it will send and asks before sending. # 5. Sends them to Ciphero over HTTPS. Ciphero matches the extensions # against known-malicious lists and the marketplaces, and prints # your risk level. # # It installs nothing, writes nothing, schedules nothing, needs no sudo. # Run it only on a machine you own or are allowed to check. By sending # the facts you accept Ciphero's terms (https://ciphero.ai/terms#cassandra) # and privacy policy (https://ciphero.ai/privacy#cassandra). # Incidents it looks for traces of: # CSA-2026-0001 · Malicious Claude Code extension 2.1.313 (Cursor marketplace) # https://cursor.com/blog/claude-code-extension-security-notice set -u CASSANDRA_URL="https://ciphero.ai/cassandra/scan/YOUR-CHECK-TOKEN" CASSANDRA_VERSION="2" OUT="" NL=' ' fact() { local IFS="$(printf '\t')"; OUT="${OUT}$*${NL}"; } if [ -t 1 ]; then B="$(printf '\033[1m')"; D="$(printf '\033[2m')"; R="$(printf '\033[0m')" RED="$(printf '\033[38;5;203m')"; AMB="$(printf '\033[38;5;215m')"; YEL="$(printf '\033[38;5;221m')" GRN="$(printf '\033[38;5;114m')"; GLD="$(printf '\033[38;5;179m')" else B=""; D=""; R=""; RED=""; AMB=""; YEL=""; GRN=""; GLD="" fi say() { printf '%s\n' "$*"; } row() { case "$2" in yes) say " ${RED}●${R} $1 ${RED}${B}found${R}" ;; no) say " ${GRN}○${R} ${D}$1${R} ${GRN}clear${R}" ;; na) say " ${D}· $1 n/a${R}" ;; *) say " ${YEL}?${R} $1 ${YEL}could not check${R}" ;; esac } say "" say " ${GLD}${B}◆ CASSANDRA${R} ${D}malicious extension check · by Ciphero${R}" say "" case "$(uname -s)" in Darwin) OS=darwin ;; Linux) OS=linux ;; *) say " This script covers macOS and Linux. On Windows, run the PowerShell line from the Cassandra page."; exit 1 ;; esac fact v "$CASSANDRA_VERSION" fact os "$OS" fact arch "$(uname -m)" fact host "$(hostname -s 2>/dev/null || hostname)" TMPD="${TMPDIR:-/tmp}" # ── Indicator checks: yes / no only ────────────────────────────── contains_any() { # needle path... local needle="$1" st=no; shift for p in "$@"; do [ -e "$p" ] || continue if [ -r "$p" ]; then grep -qF -- "$needle" "$p" 2>/dev/null && { echo yes; return; }; else st=err; fi done echo "$st" } fingerprint_any() { # fingerprint path... local needle="$1" st=no; shift for p in "$@"; do [ -e "$p" ] || continue command -v ssh-keygen >/dev/null 2>&1 || { st=err; continue; } if [ -r "$p" ]; then ssh-keygen -lf "$p" -E sha256 2>/dev/null | grep -qF -- "$needle" && { echo yes; return; }; else st=err; fi done echo "$st" } exists_any() { for p in "$@"; do [ -e "$p" ] && { echo yes; return; }; done; echo no; } launchctl_has() { command -v launchctl >/dev/null 2>&1 || { echo na; return; } launchctl list 2>/dev/null | grep -qF -- "$1" && echo yes || echo no } connected_to() { if command -v lsof >/dev/null 2>&1; then lsof -nP -iTCP 2>/dev/null | grep -qF -- "$1" && echo yes || echo no elif command -v ss >/dev/null 2>&1; then ss -tn 2>/dev/null | grep -qF -- "$1" && echo yes || echo no elif command -v netstat >/dev/null 2>&1; then netstat -an 2>/dev/null | grep -qF -- "$1" && echo yes || echo no else echo err; fi } check() { # id os-list label result case " $2 " in *" $OS "*) ;; *) return ;; esac fact check "$1" "$4"; row "$3" "$4" } # ── Editor extensions: folder names only ───────────────────────── NEEDLES="anthropic.claude-code-2.1.313" EXT_COUNT=0 list_exts() { # editor dir [ -d "$2" ] || return for d in "$2"/*/; do [ -d "$d" ] || continue n="$(basename "$d")" case "$n" in .*) continue ;; esac fact ext "$1" "$n"; EXT_COUNT=$((EXT_COUNT + 1)) done for nd in $NEEDLES; do [ -r "$2/.obsolete" ] && grep -qF -- "$nd" "$2/.obsolete" 2>/dev/null && fact hist "$1" "$nd" obsolete done } sha256_of() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}' elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}' else openssl dgst -sha256 "$1" 2>/dev/null | awk '{print $NF}'; fi } vsix_cache() { # editor dir [ -d "$2" ] || return for f in "$2"/*; do [ -f "$f" ] || continue n="$(basename "$f")" for nd in $NEEDLES; do case "$n" in "$nd"*) fact vsix "$1" "$n" "$(sha256_of "$f")" ;; esac done done } list_exts cursor "$HOME/.cursor/extensions" list_exts cursor "$HOME/.cursor-server/extensions" vsix_cache cursor "$HOME/Library/Application Support/Cursor/CachedExtensionVSIXs" vsix_cache cursor "$HOME/.config/Cursor/CachedExtensionVSIXs" list_exts vscode "$HOME/.vscode/extensions" list_exts vscode "$HOME/.vscode-server/extensions" vsix_cache vscode "$HOME/Library/Application Support/Code/CachedExtensionVSIXs" vsix_cache vscode "$HOME/.config/Code/CachedExtensionVSIXs" list_exts vscode_insiders "$HOME/.vscode-insiders/extensions" list_exts windsurf "$HOME/.windsurf/extensions" vsix_cache windsurf "$HOME/Library/Application Support/Windsurf/CachedExtensionVSIXs" list_exts vscodium "$HOME/.vscode-oss/extensions" list_exts kiro "$HOME/.kiro/extensions" list_exts trae "$HOME/.trae/extensions" say " ${B}Editor extensions${R} ${D}${EXT_COUNT} found, ids and versions only${R}" # ── Browser extensions: ids only ───────────────────────────────── BEXT_COUNT=0 BSEEN=" " BNAMES="" add_bext() { # browser id version case "$BSEEN" in *" $1:$2 "*) return ;; esac BSEEN="$BSEEN$1:$2 " case " $BNAMES " in *" $1 "*) ;; *) BNAMES="${BNAMES:+$BNAMES }$1" ;; esac fact bext "$1" "$2" "$3"; BEXT_COUNT=$((BEXT_COUNT + 1)) } list_chromium() { # browser root: //Extensions//_0 [ -d "$2" ] || return for ed in "$2"/*/Extensions; do [ -d "$ed" ] || continue for idd in "$ed"/*/; do id="$(basename "$idd")" [ "${#id}" -eq 32 ] || continue case "$id" in *[!a-p]*) continue ;; esac ver="" for vd in "$idd"*/; do [ -d "$vd" ] && ver="$(basename "$vd")"; done add_bext "$1" "$id" "${ver%_*}" done done } list_firefox() { # browser root: //extensions/.xpi [ -d "$2" ] || return for x in "$2"/*/extensions/*.xpi; do [ -f "$x" ] || continue add_bext "$1" "$(basename "$x" .xpi)" "" done } list_chromium chrome "$HOME/Library/Application Support/Google/Chrome" list_chromium chrome "$HOME/.config/google-chrome" list_chromium chrome "$HOME/.var/app/com.google.Chrome/config/google-chrome" list_chromium edge "$HOME/Library/Application Support/Microsoft Edge" list_chromium edge "$HOME/.config/microsoft-edge" list_chromium brave "$HOME/Library/Application Support/BraveSoftware/Brave-Browser" list_chromium brave "$HOME/.config/BraveSoftware/Brave-Browser" list_chromium arc "$HOME/Library/Application Support/Arc/User Data" list_chromium dia "$HOME/Library/Application Support/Dia/User Data" list_chromium comet "$HOME/Library/Application Support/Comet" list_chromium vivaldi "$HOME/Library/Application Support/Vivaldi" list_chromium vivaldi "$HOME/.config/vivaldi" list_chromium chromium "$HOME/Library/Application Support/Chromium" list_chromium chromium "$HOME/.config/chromium" list_chromium chromium "$HOME/snap/chromium/common/chromium" list_firefox firefox "$HOME/Library/Application Support/Firefox/Profiles" list_firefox firefox "$HOME/.mozilla/firefox" list_firefox firefox "$HOME/snap/firefox/common/.mozilla/firefox" say " ${B}Browser extensions${R} ${D}${BEXT_COUNT} found${BNAMES:+ in $BNAMES}, ids and versions only${R}" say "" # ── Incident traces: yes / no only ─────────────────────────────── say " ${B}Claude Code 2.1.313${R} ${D}incident traces${R}" check ssh_key_comment "darwin linux" "Attacker SSH key svc-telemetry in authorized_keys" "$(contains_any "svc-telemetry" "$HOME/.ssh/authorized_keys" "$HOME/.ssh/authorized_keys2")" check ssh_key_fingerprint "darwin linux windows" "Attacker SSH key fingerprint in authorized_keys" "$(fingerprint_any "SHA256:0Z6R+DF64Do2nwQic8a7kkfDLNNk4IiK+2U2Rci4htk" "$HOME/.ssh/authorized_keys" "$HOME/.ssh/authorized_keys2")" check mac_launch_agent "darwin" "Fake Apple LaunchAgent com.apple.sysmond.plist" "$(exists_any "$HOME/Library/LaunchAgents/com.apple.sysmond.plist")" check mac_launchctl "darwin" "Reverse-tunnel service com.apple.sysmond loaded" "$(launchctl_has "com.apple.sysmond")" check unix_tunnel_files "darwin linux" "Tunnel files ~/.config/.smt and ~/.config/.smtk" "$(exists_any "$HOME/.config/.smt" "$HOME/.config/.smtk")" check unix_stage_files "darwin linux" "Staging files .stk_* in the temp directory" "$(exists_any "$TMPD"/.stk_* "/tmp"/.stk_*)" check c2_connection "darwin linux windows" "Live connection to the attacker server 134.209.82.225" "$(connected_to "134.209.82.225")" say "" # ── AI tools and API-key variable NAMES (never values) ─────────── TOOLS="" for b in claude codex cursor-agent gemini aider goose opencode amp copilot ollama lms; do command -v "$b" >/dev/null 2>&1 && { fact tool "$b"; TOOLS="$TOOLS $b"; } done if [ "$OS" = darwin ]; then for a in "Cursor" "Windsurf" "Claude" "ChatGPT" "Visual Studio Code" "Kiro" "Trae" "Zed" "Ollama" "LM Studio"; do if [ -d "/Applications/$a.app" ] || [ -d "$HOME/Applications/$a.app" ]; then fact tool "app:$a"; TOOLS="$TOOLS app:$a"; fi done fi ENV_RE='^(OPENAI|ANTHROPIC|GEMINI|GOOGLE|MISTRAL|GROQ|OPENROUTER|TOGETHER|FIREWORKS|DEEPSEEK|XAI|COHERE|HF|HUGGINGFACE|AWS|AZURE|GITHUB|GH|GITLAB|NPM|PYPI|STRIPE|SLACK|DATABRICKS|SNOWFLAKE)[A-Z0-9_]*(_API_KEY|_API_TOKEN|_ACCESS_TOKEN|_SECRET_KEY)$' ENVS="$( { env | cut -d= -f1; grep -hoE '^[[:space:]]*export[[:space:]]+[A-Z][A-Z0-9_]*=' "$HOME/.zshrc" "$HOME/.zprofile" "$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.profile" 2>/dev/null | sed -E 's/^[[:space:]]*export[[:space:]]+//; s/=$//'; } | grep -E "$ENV_RE" | sort -u)" for e in $ENVS; do fact env "$e"; done say " ${B}AI tools${R} ${D}${TOOLS:- none found}${R}" KEYN="$(echo $ENVS)" say " ${B}Key variable names${R} ${D}${KEYN:-none found}${R}" say "" # ── Consent ────────────────────────────────────────────────────── NFACTS="$(printf '%s' "$OUT" | grep -c .)" say " ${B}Ready to send ${NFACTS} facts to Ciphero.${R} ${D}No file contents, no key values.${R}" say " ${D}Sending accepts Ciphero's terms and privacy policy: https://ciphero.ai/terms#cassandra${R}" ANSWER="" while :; do if [ "${CASSANDRA_YES:-}" = 1 ]; then ANSWER=y elif (exec < /dev/tty) 2>/dev/null; then printf ' Send them? [Y]es / [n]o / [s]how every line: ' > /dev/tty read -r ANSWER < /dev/tty || ANSWER=n else say " No terminal to ask on. Re-run with CASSANDRA_YES=1 to send without asking."; exit 1; fi case "$ANSWER" in s|S) printf '%s' "$OUT" | sed 's/^/ /' ;; n|N|no) say " Nothing sent."; exit 0 ;; *) break ;; esac done RESP="$(printf '%s' "$OUT" | curl -fsS --max-time 40 -X POST -H 'Content-Type: text/plain' --data-binary @- "$CASSANDRA_URL" 2>&1)" || { say " ${RED}Upload failed:${R} $RESP"; exit 1; } say "" printf '%s\n' "$RESP" | while IFS='|' read -r level text; do case "$level" in critical) say " ${RED}${B}■ CRITICAL${R} ${B}$text${R}" ;; high) say " ${AMB}${B}■ HIGH${R} ${B}$text${R}" ;; caution) say " ${YEL}${B}■ CAUTION${R} ${B}$text${R}" ;; clear) say " ${GRN}${B}■ CLEAR${R} ${B}$text${R}" ;; step) say " ${GLD}→${R} $text" ;; error) say " ${RED}${B}■${R} $text" ;; *) say " ${D}$text${R}" ;; esac done say ""