# Cassandra - malicious extension check - by Ciphero # # Lists the extensions installed in your editors (VS Code, Cursor, # Windsurf and other forks) and browsers (Chrome, Edge, Brave, Firefox # and others) by folder name: id and version only. If a version an # incident below names is installed, hashes that extension's main file # and checks it for the published attacker strings (the hash and a # yes / no go, never the file). Checks a fixed list of places (named # files and folders, your Run keys, the scheduled tasks, the running # processes and the code of your other extensions) for the traces of # specific incidents: yes / no plus the name of what matched, never # file contents. Lists AI tool names and the NAMES (never values) of # API-key variables, shows you every fact, asks before sending, then # prints your risk level. Ciphero matches the extensions against # known-malicious lists and the marketplaces. Installs nothing. # 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 extensions 2.1.312 and 2.1.313 (Cursor marketplace) # https://cursor.com/blog/claude-code-extension-security-notice # # The readable copy at https://ciphero.ai/cassandra.ps1 is this same file with # YOUR-CHECK-TOKEN in place of a check token. Run as is, it does every # check, prints its own summary of the incident traces and stops before # asking to send: it has no token, so there is nothing it can send to. # # Cmdlets only, no .NET calls beyond strings and hashtables, so it also # runs under Constrained Language Mode on managed machines. Its state # lives in one hashtable ($cx) rather than in your session's variables. & { $ErrorActionPreference = 'SilentlyContinue' $ProgressPreference = 'SilentlyContinue' $url = 'https://ciphero.ai/cassandra/scan/YOUR-CHECK-TOKEN' $token = 'YOUR-CHECK-TOKEN' $cx = @{ facts = @(); checkId = ''; extDirs = @(); extCount = 0; bextCount = 0; bseen = @{}; bnames = @(); checks = 0; found = 0; backdoored = @(); history = @() } $psNoise = @('PSPath', 'PSParentPath', 'PSChildName', 'PSDrive', 'PSProvider') function Add-Fact { $cx.facts += ($args -join "`t") } function Show-Row($label, $st) { switch ($st) { 'yes' { Write-Host " * $label " -NoNewline; Write-Host 'found' -ForegroundColor Red } 'no' { Write-Host " o $label " -NoNewline -ForegroundColor DarkGray; Write-Host 'clear' -ForegroundColor Green } 'na' { Write-Host " . $label n/a" -ForegroundColor DarkGray } default { Write-Host " ? $label " -NoNewline; Write-Host 'could not check' -ForegroundColor Yellow } } } # A file under ProgramData that this user can't open (OpenSSH Server's # administrators_authorized_keys) is one that code running as this user # couldn't have written either: not applicable, not a failed check. function Test-AdminOnly($p) { if (-not $env:ProgramData) { return $false } if (-not $p.ToLowerInvariant().StartsWith($env:ProgramData.ToLowerInvariant())) { return $false } try { $null = Get-Content -LiteralPath $p -TotalCount 1 -ErrorAction Stop; return $false } catch { return $true } } # Each probe returns yes / no / na / err and records the name of # what matched (a file, a key, an extension folder): never a path # under the profile, never contents. A wildcard sits only in the last # segment of a path, so it expands as a filter inside its literal # parent folder; brackets elsewhere in the path stay literal. function Add-Hit($name) { Add-Fact 'hit' $cx.checkId $name } function Expand-Paths($paths) { $found = @() foreach ($p in $paths) { if ($p -match '[*?]') { $dir = Split-Path $p -Parent; $leaf = Split-Path $p -Leaf if ($dir -and (Test-Path -LiteralPath $dir)) { $found += @(Get-ChildItem -LiteralPath $dir -Filter $leaf -File -Force -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName }) } } elseif (Test-Path -LiteralPath $p -PathType Leaf) { $found += $p } } return $found } function Test-Contains($needle, $paths) { $st = 'no'; $read = $false; $skipped = $false foreach ($p in (Expand-Paths $paths)) { if (Test-AdminOnly $p) { $skipped = $true; continue } try { if (Select-String -LiteralPath $p -SimpleMatch -Pattern $needle -Quiet -ErrorAction Stop) { $st = 'yes'; Add-Hit (Split-Path $p -Leaf) } $read = $true } catch { if ($st -ne 'yes') { $st = 'err' } } } if ($st -eq 'no' -and $skipped -and -not $read) { return 'na' } return $st } function Test-Fingerprint($needle, $paths) { $st = 'no'; $read = $false; $skipped = $false $keygen = Get-Command ssh-keygen -ErrorAction SilentlyContinue foreach ($p in (Expand-Paths $paths)) { if (Test-AdminOnly $p) { $skipped = $true; continue } if (-not $keygen) { $st = 'err'; continue } # Empty input on stdin: ssh-keygen -l asks nothing, and if a build # ever did, it gets an answer instead of waiting on the terminal. $fp = '' | & ssh-keygen -lf $p -E sha256 2>$null if (-not $fp) { continue } if (($fp -join ' ').Contains($needle)) { $st = 'yes'; Add-Hit (Split-Path $p -Leaf) } $read = $true } if ($st -eq 'no' -and $skipped -and -not $read) { return 'na' } return $st } function Test-Any($paths) { $st = 'no' foreach ($p in $paths) { if ($p -match '[*?]') { $dir = Split-Path $p -Parent; $leaf = Split-Path $p -Leaf if ($dir -and (Test-Path -LiteralPath $dir)) { foreach ($f in @(Get-ChildItem -LiteralPath $dir -Filter $leaf -Force -ErrorAction SilentlyContinue)) { $st = 'yes'; Add-Hit $f.Name } } } elseif (Test-Path -LiteralPath $p) { $st = 'yes'; Add-Hit (Split-Path $p -Leaf) } } return $st } # The user's own Run keys: persistence the notice did not list. function Test-RunKey($needle) { $st = 'no' foreach ($k in 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run', 'HKCU:\Software\Microsoft\Windows\CurrentVersion\RunOnce') { $props = Get-ItemProperty -Path $k -ErrorAction SilentlyContinue if (-not $props) { continue } foreach ($p in $props.PSObject.Properties) { if ($psNoise -contains $p.Name) { continue } if ("$($p.Value)".Contains($needle)) { $st = 'yes'; Add-Hit $p.Name } } } return $st } # Any scheduled task whose action runs the needle, whatever the task is called. function Test-TaskAction($needle) { if (-not (Get-Command Get-ScheduledTask -ErrorAction SilentlyContinue)) { return 'err' } $st = 'no' foreach ($t in @(Get-ScheduledTask -ErrorAction SilentlyContinue)) { foreach ($a in @($t.Actions)) { if ("$($a.Execute) $($a.Arguments)".Contains($needle)) { $st = 'yes'; Add-Hit $t.TaskName; break } } } return $st } # The needle inside the code of any installed extension other than # the versions an incident already names. Top-level, dist and out # scripts only (a few MB at most), so it stays fast. function Test-ExtCode($needle) { $st = 'no' foreach ($pair in $cx.extDirs) { $editor, $dir = $pair foreach ($f in @(Get-ChildItem -Path "$dir\*\*.js", "$dir\*\dist\*.js", "$dir\*\out\*.js" -File -ErrorAction SilentlyContinue)) { if ($f.Length -gt 64MB) { continue } $folder = (($f.FullName.Substring($dir.Length) -replace '^[\\/]+', '') -split '[\\/]')[0] $named = $false foreach ($n in $needles) { if ($folder.StartsWith($n)) { $named = $true } } if ($named) { continue } if (Select-String -LiteralPath $f.FullName -SimpleMatch -Pattern $needle -Quiet -ErrorAction SilentlyContinue) { $st = 'yes'; Add-Hit "${editor}:$folder" } } } return $st } function Test-Task($name) { if (Get-Command Get-ScheduledTask -ErrorAction SilentlyContinue) { if (Get-ScheduledTask | Where-Object { $_.TaskName -eq $name }) { return 'yes' } else { return 'no' } } schtasks /Query /TN $name *> $null if ($LASTEXITCODE -eq 0) { return 'yes' } else { return 'no' } } function Test-Connection2($ip) { if (Get-Command Get-NetTCPConnection -ErrorAction SilentlyContinue) { if (Get-NetTCPConnection -RemoteAddress $ip -ErrorAction SilentlyContinue) { return 'yes' } else { return 'no' } } if (netstat -an | Select-String -SimpleMatch $ip) { return 'yes' } else { return 'no' } } function Test-Process($needle) { $procs = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue if (-not $procs) { return 'err' } foreach ($p in $procs) { if ($p.CommandLine -and $p.CommandLine.Contains($needle)) { return 'yes' } } return 'no' } # A check's status is the last yes / no / na / err its probe # returned; anything else a cmdlet let slip into the pipeline is # ignored, and a probe that throws is 'could not check'. function Run-Check($id, $label, $probe) { $cx.checkId = $id $st = 'err' try { $seen = @(& $probe) | Where-Object { "$_" -in @('yes', 'no', 'na', 'err') } | Select-Object -Last 1 if ($seen) { $st = "$seen" } } catch { $st = 'err' } Add-Fact 'check' $id $st; Show-Row $label $st $cx.checks += 1; if ($st -eq 'yes') { $cx.found += 1 } } Write-Host '' Write-Host ' CASSANDRA' -ForegroundColor DarkYellow -NoNewline; Write-Host ' malicious extension check by Ciphero' -ForegroundColor DarkGray Write-Host '' $mode = "$($ExecutionContext.SessionState.LanguageMode)" if ($mode -and $mode -ne 'FullLanguage') { Write-Host " PowerShell runs in $mode here, a policy on this machine. Cassandra is written for it; anything it cannot check will say so." -ForegroundColor DarkGray Write-Host '' } Add-Fact 'v' '3' Add-Fact 'os' 'windows' Add-Fact 'arch' $env:PROCESSOR_ARCHITECTURE Add-Fact 'host' $env:COMPUTERNAME $needles = @('anthropic.claude-code-2.1.312', 'anthropic.claude-code-2.1.313') function List-Exts($editor, $dir) { if (-not (Test-Path -LiteralPath $dir)) { return } $cx.extDirs += ,@($editor, (Get-Item -LiteralPath $dir).FullName) Get-ChildItem -LiteralPath $dir -Directory -Force | Where-Object { -not $_.Name.StartsWith('.') } | ForEach-Object { Add-Fact 'ext' $editor $_.Name; $cx.extCount += 1 } $obs = Join-Path $dir '.obsolete' if (Test-Path -LiteralPath $obs) { $text = Get-Content -LiteralPath $obs -Raw foreach ($n in $needles) { if ($text -and $text.Contains($n)) { Add-Fact 'hist' $editor $n 'obsolete'; Note-Hist $editor $n } } } } # editor:version pairs the install history names, for the readable # copy's own summary (the full check's verdict gets the facts). function Note-Hist($editor, $n) { if ($cx.history -notcontains "${editor}:$n") { $cx.history += "${editor}:$n" } } # The editor's own install log names the versions it installed and # removed: a second history source for a version an update replaced. function Log-History($editor, $dir) { if (-not (Test-Path -LiteralPath $dir)) { return } $logs = @(Get-ChildItem -Path "$dir\*\main.log", "$dir\*\sharedprocess.log" -File -ErrorAction SilentlyContinue) foreach ($n in $needles) { foreach ($f in $logs) { if (Select-String -LiteralPath $f.FullName -SimpleMatch -Pattern $n -Quiet) { Add-Fact 'hist' $editor $n 'logs'; Note-Hist $editor $n; break } } } } function Vsix-Cache($editor, $dir) { if (-not (Test-Path -LiteralPath $dir)) { return } Get-ChildItem -LiteralPath $dir -File | ForEach-Object { $f = $_ foreach ($n in $needles) { if ($f.Name.StartsWith($n)) { Add-Fact 'vsix' $editor $f.Name ((Get-FileHash -LiteralPath $f.FullName -Algorithm SHA256).Hash.ToLower()) } } } } # The main file of an installed copy of a version an incident names: # its SHA-256, and whether the published attacker strings are in it. function Payload-Scan($editor, $dir, $file, $nds, $strings) { if (-not (Test-Path -LiteralPath $dir)) { return } Get-ChildItem -LiteralPath $dir -Directory -Force | ForEach-Object { $folder = $_ foreach ($n in $nds) { if (-not $folder.Name.StartsWith($n)) { continue } $f = Join-Path $folder.FullName $file if (-not (Test-Path -LiteralPath $f -PathType Leaf)) { continue } $hit = 'no' foreach ($s in $strings) { if (Select-String -LiteralPath $f -SimpleMatch -Pattern $s -Quiet) { $hit = 'yes'; break } } if ($hit -eq 'yes') { $cx.backdoored += $folder.Name } Add-Fact 'payload' $editor $folder.Name $file ((Get-FileHash -LiteralPath $f -Algorithm SHA256).Hash.ToLower()) $hit Write-Host " $($folder.Name) $file hashed: the hash goes, not the file" -ForegroundColor DarkGray break } } } List-Exts 'cursor' "$env:USERPROFILE\.cursor\extensions" Vsix-Cache 'cursor' "$env:APPDATA\Cursor\CachedExtensionVSIXs" Log-History 'cursor' "$env:APPDATA\Cursor\logs" Payload-Scan 'cursor' "$env:USERPROFILE\.cursor\extensions" 'extension.js' @('anthropic.claude-code-2.1.312', 'anthropic.claude-code-2.1.313') @('134.209.82.225', 'svc-telemetry') List-Exts 'vscode' "$env:USERPROFILE\.vscode\extensions" Vsix-Cache 'vscode' "$env:APPDATA\Code\CachedExtensionVSIXs" Log-History 'vscode' "$env:APPDATA\Code\logs" Payload-Scan 'vscode' "$env:USERPROFILE\.vscode\extensions" 'extension.js' @('anthropic.claude-code-2.1.312', 'anthropic.claude-code-2.1.313') @('134.209.82.225', 'svc-telemetry') List-Exts 'vscode_insiders' "$env:USERPROFILE\.vscode-insiders\extensions" Payload-Scan 'vscode_insiders' "$env:USERPROFILE\.vscode-insiders\extensions" 'extension.js' @('anthropic.claude-code-2.1.312', 'anthropic.claude-code-2.1.313') @('134.209.82.225', 'svc-telemetry') List-Exts 'windsurf' "$env:USERPROFILE\.windsurf\extensions" Vsix-Cache 'windsurf' "$env:APPDATA\Windsurf\CachedExtensionVSIXs" Log-History 'windsurf' "$env:APPDATA\Windsurf\logs" Payload-Scan 'windsurf' "$env:USERPROFILE\.windsurf\extensions" 'extension.js' @('anthropic.claude-code-2.1.312', 'anthropic.claude-code-2.1.313') @('134.209.82.225', 'svc-telemetry') List-Exts 'vscodium' "$env:USERPROFILE\.vscode-oss\extensions" Payload-Scan 'vscodium' "$env:USERPROFILE\.vscode-oss\extensions" 'extension.js' @('anthropic.claude-code-2.1.312', 'anthropic.claude-code-2.1.313') @('134.209.82.225', 'svc-telemetry') List-Exts 'kiro' "$env:USERPROFILE\.kiro\extensions" Payload-Scan 'kiro' "$env:USERPROFILE\.kiro\extensions" 'extension.js' @('anthropic.claude-code-2.1.312', 'anthropic.claude-code-2.1.313') @('134.209.82.225', 'svc-telemetry') List-Exts 'trae' "$env:USERPROFILE\.trae\extensions" Payload-Scan 'trae' "$env:USERPROFILE\.trae\extensions" 'extension.js' @('anthropic.claude-code-2.1.312', 'anthropic.claude-code-2.1.313') @('134.209.82.225', 'svc-telemetry') Write-Host " Editor extensions $($cx.extCount) found, ids and versions only" function Add-Bext($browser, $id, $ver) { $k = "$browser|$id" if ($cx.bseen.ContainsKey($k)) { return } $cx.bseen[$k] = $true if ($cx.bnames -notcontains $browser) { $cx.bnames += $browser } Add-Fact 'bext' $browser $id $ver $cx.bextCount += 1 } function List-Chromium($browser, $root) { if (-not (Test-Path -LiteralPath $root)) { return } Get-ChildItem -LiteralPath $root -Directory -Force | ForEach-Object { $ed = Join-Path $_.FullName 'Extensions' if (Test-Path -LiteralPath $ed) { Get-ChildItem -LiteralPath $ed -Directory | Where-Object { $_.Name -cmatch '^[a-p]{32}$' } | ForEach-Object { $v = Get-ChildItem -LiteralPath $_.FullName -Directory | Select-Object -Last 1 $ver = if ($v) { $v.Name -replace '_\d+$', '' } else { '' } Add-Bext $browser $_.Name $ver } } } } function List-Firefox($browser, $root) { if (-not (Test-Path -LiteralPath $root)) { return } Get-ChildItem -LiteralPath $root -Directory | ForEach-Object { $ed = Join-Path $_.FullName 'extensions' if (Test-Path -LiteralPath $ed) { Get-ChildItem -LiteralPath $ed -Filter '*.xpi' -File | ForEach-Object { Add-Bext $browser $_.BaseName '' } } } } List-Chromium 'chrome' "$env:LOCALAPPDATA\Google\Chrome\User Data" List-Chromium 'edge' "$env:LOCALAPPDATA\Microsoft\Edge\User Data" List-Chromium 'brave' "$env:LOCALAPPDATA\BraveSoftware\Brave-Browser\User Data" List-Chromium 'comet' "$env:LOCALAPPDATA\Perplexity\Comet\User Data" List-Chromium 'vivaldi' "$env:LOCALAPPDATA\Vivaldi\User Data" List-Chromium 'chromium' "$env:LOCALAPPDATA\Chromium\User Data" List-Firefox 'firefox' "$env:APPDATA\Mozilla\Firefox\Profiles" $where = if ($cx.bnames) { ' in ' + ($cx.bnames -join ' ') } else { '' } Write-Host " Browser extensions $($cx.bextCount) found$where, ids and versions only" Write-Host '' Write-Host ' Claude Code 2.1.312 and 2.1.313' -NoNewline; Write-Host ' incident traces' -ForegroundColor DarkGray Run-Check 'ssh_key_fingerprint' 'Attacker SSH key fingerprint in authorized_keys' { Test-Fingerprint 'SHA256:0Z6R+DF64Do2nwQic8a7kkfDLNNk4IiK+2U2Rci4htk' @("$env:USERPROFILE\.ssh\authorized_keys", "$env:PROGRAMDATA\ssh\administrators_authorized_keys") } Run-Check 'win_ssh_key_comment' 'Attacker SSH key svc-telemetry in authorized_keys' { Test-Contains 'svc-telemetry' @("$env:USERPROFILE\.ssh\authorized_keys", "$env:PROGRAMDATA\ssh\administrators_authorized_keys") } Run-Check 'tunnel_key_anywhere' 'The reverse-tunnel key by fingerprint among your SSH keys' { Test-Fingerprint 'SHA256:KOAU57+ZG6u3MG4WwzPzPX2O5YEe9qdOC1Bl+6MVyDQ' @("$env:USERPROFILE\.ssh\*", "$env:USERPROFILE\.config\smtk") } Run-Check 'win_task' 'Scheduled task SysMon' { Test-Task 'SysMon' } Run-Check 'win_tunnel_files' 'Tunnel files smt.cmd and smtk in .config' { Test-Any @("$env:USERPROFILE\.config\smt.cmd", "$env:USERPROFILE\.config\smtk") } Run-Check 'win_startup' 'Startup script sysmon.cmd' { Test-Any @("$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup\sysmon.cmd") } Run-Check 'win_stage_files' 'Staging files .stk_* in %TEMP%' { Test-Any @("$env:TEMP\.stk_*") } Run-Check 'win_run_key' 'Run key starting the tunnel script smt.cmd' { Test-RunKey 'smt.cmd' } Run-Check 'win_task_action' 'A scheduled task running the tunnel script under another name' { Test-TaskAction 'smt.cmd' } Run-Check 'tunnel_process' 'Reverse SSH tunnel process to the attacker server' { Test-Process 'tunnel@134.209.82.225' } Run-Check 'c2_connection' 'Live connection to the attacker server 134.209.82.225' { Test-Connection2 '134.209.82.225' } Run-Check 'ext_code_ioc' 'The attacker server address inside the code of another installed extension' { Test-ExtCode '134.209.82.225' } Write-Host '' $tools = @() foreach ($b in @('claude', 'codex', 'cursor-agent', 'gemini', 'aider', 'goose', 'opencode', 'amp', 'copilot', 'ollama', 'lms')) { if (Get-Command $b -ErrorAction SilentlyContinue) { Add-Fact 'tool' $b; $tools += $b } } foreach ($a in @(@('Cursor', "$env:LOCALAPPDATA\Programs\cursor"), @('Windsurf', "$env:LOCALAPPDATA\Programs\Windsurf"), @('Visual Studio Code', "$env:LOCALAPPDATA\Programs\Microsoft VS Code"), @('Claude', "$env:LOCALAPPDATA\AnthropicClaude"), @('Kiro', "$env:LOCALAPPDATA\Programs\Kiro"), @('Trae', "$env:LOCALAPPDATA\Programs\Trae"), @('Ollama', "$env:LOCALAPPDATA\Programs\Ollama"), @('LM Studio', "$env:LOCALAPPDATA\Programs\LM Studio"))) { if (Test-Path -LiteralPath $a[1]) { Add-Fact 'tool' ('app:' + $a[0]); $tools += ('app:' + $a[0]) } } $envRe = '^(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)$' # Names only: this session's variables, plus the user's own persistent # ones from the registry (the ones a new terminal would also have). $names = @(Get-ChildItem env: | ForEach-Object { $_.Name }) $userEnv = Get-ItemProperty -Path 'HKCU:\Environment' -ErrorAction SilentlyContinue if ($userEnv) { $names += @($userEnv.PSObject.Properties | Where-Object { $psNoise -notcontains $_.Name } | ForEach-Object { $_.Name }) } $names = @($names | Where-Object { $_ -cmatch $envRe } | Sort-Object -Unique) foreach ($e in $names) { Add-Fact 'env' $e } Write-Host " AI tools $($tools -join ', ')" -ForegroundColor DarkGray Write-Host " Key variable names $($names -join ', ')" -ForegroundColor DarkGray Write-Host '' # The readable copy: no token, so nothing to send to. Its own summary # of the incident traces, then out, before the consent prompt. if ($token -eq 'YOUR-CHECK-TOKEN') { if ($cx.backdoored.Count -gt 0) { Write-Host ' BACKDOORED BUILD ' -ForegroundColor Red -NoNewline; Write-Host "Installed, and its main file contains the attacker strings: $($cx.backdoored -join ', ')" } if ($cx.history.Count -gt 0) { Write-Host ' BAD VERSION WAS INSTALLED ' -ForegroundColor Red -NoNewline; Write-Host "In the editor's install history: $($cx.history -join ', '). An update may have removed it since; what it did while installed is not undone." } if ($cx.found -gt 0) { Write-Host ' TRACES FOUND ' -ForegroundColor Red -NoNewline; Write-Host "$($cx.found) of $($cx.checks) published indicators were found on this machine." } elseif ($cx.backdoored.Count -eq 0 -and $cx.history.Count -eq 0) { Write-Host ' NO TRACES ' -ForegroundColor Green -NoNewline; Write-Host "None of the $($cx.checks) published indicators were found on this machine." } else { Write-Host " None of the $($cx.checks) other published indicators were found." } if ($cx.found -gt 0 -or $cx.backdoored.Count -gt 0) { Write-Host " -> Treat it as compromised. Steps, per incident: https://ciphero.ai/advisories" } elseif ($cx.history.Count -gt 0) { Write-Host " -> Treat the machine as exposed: rotate the keys and tokens it used, from another machine. Steps: https://ciphero.ai/advisories" } Write-Host '' Write-Host ' This is the readable copy of the script. It carries no check token, so nothing was sent' -ForegroundColor DarkGray Write-Host " and everything above ran here. It did not match your $($cx.extCount) editor and $($cx.bextCount) browser" -ForegroundColor DarkGray Write-Host ' extensions against the known-malicious lists or the marketplaces. For that, and a' -ForegroundColor DarkGray Write-Host " written report: https://ciphero.ai/cassandra" -ForegroundColor DarkGray Write-Host '' return } # Consent: nothing goes until you type yes. CASSANDRA_YES=1 answers # for you (automation); a host that cannot ask gives up after a few tries. Write-Host " Ready to send $($cx.facts.Count) facts to Ciphero. No file contents, no key values." Write-Host " Sending accepts Ciphero's terms and privacy policy: https://ciphero.ai/terms#cassandra" -ForegroundColor DarkGray $asked = 0 while ($true) { if ($env:CASSANDRA_YES -eq '1') { $answer = 'y' } else { if ($asked -ge 8) { Write-Host ' No answer. Nothing sent.'; return } $asked += 1 $answer = Read-Host ' Send them? [y]es / [n]o / [s]how every line' } if ($answer -match '^(s|show)$') { $cx.facts | ForEach-Object { Write-Host " $_" }; continue } if ($answer -match '^(n|no)$') { Write-Host ' Nothing sent.'; return } if ($answer -match '^(y|yes)$') { break } Write-Host ' Please answer y, n or s.' -ForegroundColor DarkGray } try { $resp = Invoke-RestMethod -Uri $url -Method Post -Body ($cx.facts -join "`n") -ContentType 'text/plain; charset=utf-8' -TimeoutSec 40 } catch { Write-Host " Upload failed: $($_.Exception.Message)" -ForegroundColor Red; return } Write-Host '' foreach ($line in ($resp -split "`n")) { $level, $text = $line -split '\|', 2 switch ($level) { 'critical' { Write-Host ' CRITICAL ' -ForegroundColor Red -NoNewline; Write-Host $text } 'high' { Write-Host ' HIGH ' -ForegroundColor DarkYellow -NoNewline; Write-Host $text } 'caution' { Write-Host ' CAUTION ' -ForegroundColor Yellow -NoNewline; Write-Host $text } 'clear' { Write-Host ' CLEAR ' -ForegroundColor Green -NoNewline; Write-Host $text } 'step' { Write-Host " -> $text" } 'error' { Write-Host " $text" -ForegroundColor Red } default { if ($text) { Write-Host " $text" -ForegroundColor DarkGray } } } } Write-Host '' }