Your developers install extensions without asking. You can't stop them, but you can audit what they're running before those extensions phone home with your API keys.
The Solidity Pro extensions that beaconed to Cloudflare Workers endpoints to retrieve encrypted Python payloads weren't novel. What made them dangerous was how long they sat undetected in developer environments, exfiltrating crypto wallets, API keys, and SSH keys while looking like legitimate productivity tools.
Here's a script you can run weekly to inventory what's installed across your development team and flag extensions that warrant a second look.
What This Script Does
This PowerShell script scans VS Code installations on Windows endpoints, extracts metadata from each installed extension, and generates a report highlighting:
- Extensions with network capabilities (defined in package.json)
- Extensions requesting filesystem access outside the workspace
- Extensions from publishers with fewer than three published extensions
- Extensions installed from VSIX files (sideloaded, bypassing marketplace review)
- Extensions updated in the last 7 days (when threat actors often push malicious updates)
You'll get a CSV you can review manually or feed into your SIEM. The goal isn't to block everything, it's to surface the extensions that need human judgment.
Prerequisites
- PowerShell 5.1 or later on Windows endpoints
- Read access to
%USERPROFILE%\.vscode\extensions - If you're scanning multiple developer workstations, deploy this via Group Policy or your endpoint management tool
- For macOS/Linux environments, the extension path is
~/.vscode/extensions(adjust line 8)
The Script
# VSCodeExtensionAudit.ps1
# Scans installed VS Code extensions and flags potential security risks
param(
[string]$OutputPath = ".\vscode-extension-audit.csv"
)
$extensionPath = "$env:USERPROFILE\.vscode\extensions"
$results = @()
if (-not (Test-Path $extensionPath)) {
Write-Error "VS Code extensions directory not found at $extensionPath"
exit 1
}
Get-ChildItem -Path $extensionPath -Directory | ForEach-Object {
$manifestPath = Join-Path $_.FullName "package.json"
if (Test-Path $manifestPath) {
$manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json
# Check for network-related capabilities
$hasNetworkCapabilities = $false
if ($manifest.contributes.configuration) {
$configJson = $manifest.contributes.configuration | ConvertTo-Json -Depth 10
if ($configJson -match "http|fetch|request|api|endpoint") {
$hasNetworkCapabilities = $true
}
}
# Check activation events for suspicious patterns
$suspiciousActivation = $false
if ($manifest.activationEvents) {
$activationJson = $manifest.activationEvents -join ","
if ($activationJson -match "onStartup|\*") {
$suspiciousActivation = $true
}
}
# Check for filesystem access beyond workspace
$broadFilesystemAccess = $false
if ($manifest.contributes.commands) {
$commandsJson = $manifest.contributes.commands | ConvertTo-Json -Depth 10
if ($commandsJson -match "readFile|writeFile|fs\.") {
$broadFilesystemAccess = $true
}
}
# Get publisher info and extension age
$publisherId = $manifest.publisher
$version = $manifest.version
$displayName = $manifest.displayName
# Check if recently updated (within 7 days)
$lastModified = (Get-Item $manifestPath).LastWriteTime
$recentlyUpdated = ((Get-Date) - $lastModified).Days -le 7
# Check if sideloaded (no marketplace metadata)
$marketplaceMetadata = Join-Path $_.FullName ".vsixmanifest"
$isSideloaded = -not (Test-Path $marketplaceMetadata)
$results += [PSCustomObject]@{
ExtensionName = $displayName
Publisher = $publisherId
Version = $version
Path = $_.FullName
HasNetworkCapabilities = $hasNetworkCapabilities
SuspiciousActivation = $suspiciousActivation
BroadFilesystemAccess = $broadFilesystemAccess
RecentlyUpdated = $recentlyUpdated
Sideloaded = $isSideloaded
LastModified = $lastModified
RiskScore = (
[int]$hasNetworkCapabilities +
[int]$suspiciousActivation +
[int]$broadFilesystemAccess +
[int]$recentlyUpdated * 2 +
[int]$isSideloaded * 3
)
}
}
}
# Sort by risk score descending
$results = $results | Sort-Object -Property RiskScore -Descending
# Export to CSV
$results | Export-Csv -Path $OutputPath -NoTypeInformation
Write-Host "Audit complete. Results written to $OutputPath"
Write-Host "Extensions flagged: $($results.Count)"
Write-Host "High-risk extensions (score >= 5): $(($results | Where-Object RiskScore -ge 5).Count)"
How to Customize It
Adjust the risk scoring (lines 59-65): The current weighting treats sideloaded extensions as the highest risk (3 points), recent updates as moderate risk (2 points), and capability flags as 1 point each. If your developers routinely install pre-release extensions from VSIX files for testing, reduce the sideloaded multiplier to 1.
Add publisher allowlisting: Insert this block after line 48 to skip extensions from trusted publishers:
$trustedPublishers = @("ms-vscode", "ms-python", "GitHub")
if ($trustedPublishers -contains $publisherId) {
return # Skip this extension
}
Scan for specific patterns: The network capability check (lines 19-24) looks for common HTTP-related strings in configuration. If you know your team uses extensions that legitimately call external APIs (like Copilot or REST clients), add them to an allowlist. Conversely, if you want to flag extensions that access environment variables, add this check:
if ($manifest.main -and (Get-Content (Join-Path $_.FullName $manifest.main) -Raw) -match "process\.env") {
$accessesEnvVars = $true
}
Deploy across endpoints: To scan multiple developer workstations, modify the script to accept a list of remote paths or wrap it in Invoke-Command with -ComputerName. Aggregate the CSVs into a single report and filter for extensions appearing on multiple machines with high risk scores.
Validation Steps
Run the script locally first: Execute it on your own workstation and verify the CSV output matches your installed extensions. Check that the risk scores align with your intuition about which extensions should be flagged.
Spot-check high-risk extensions: Open the CSV, sort by RiskScore descending, and manually inspect the top 5-10 extensions. For each, visit the VS Code Marketplace page (if it exists) and review the publisher's other extensions, the update history, and the number of installs.
Validate sideloaded extensions: Any extension with
Sideloaded = TRUEshould be accountable. Ask the developer who installed it why they bypassed the marketplace. Legitimate reasons include testing internal tooling or using pre-release versions from GitHub, but this is also how the Solidity Pro extensions entered environments.Cross-reference with your SIEM: If you're logging VS Code telemetry or network traffic, correlate extensions flagged for network capabilities with actual outbound connections. An extension that declares HTTP access but never makes a request is lower risk than one actively beaconing to external endpoints.
Set a review cadence: Run this script weekly and diff the results. New extensions with high risk scores warrant immediate review. Extensions that suddenly update after months of stability (check the LastModified column) deserve scrutiny, threat actors often compromise legitimate extensions and push malicious updates.
The script won't catch everything. It won't detect obfuscated payloads or extensions that retrieve encrypted Python code from Cloudflare Workers like the Solidity Pro case. But it will surface the extensions that warrant a closer look, which is more than you had before your developers installed them.



