Skip to main content
Extension Vetting Script for VS CodeResearch
5 min readFor Security Engineers

Extension Vetting Script for VS Code

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

  1. 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.

  2. 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.

  3. Validate sideloaded extensions: Any extension with Sideloaded = TRUE should 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.

  4. 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.

  5. 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.

Topics:Research

You Might Also Like