mirror of
https://dev.azure.com/effectory/Survey%20Software/_git/Cloud%20Engineering
synced 2026-02-27 18:52:18 +01:00
353 lines
14 KiB
PowerShell
353 lines
14 KiB
PowerShell
<#
|
||
.SYNOPSIS
|
||
Scans VS Code extensions for security vulnerabilities using npm audit and Snyk CLI.
|
||
|
||
.DESCRIPTION
|
||
This script lists installed VS Code extensions, downloads their NPM packages,
|
||
and runs security audits using npm audit and/or Snyk CLI to identify vulnerabilities.
|
||
|
||
.PARAMETER OutputPath
|
||
Directory where extension packages will be downloaded. Defaults to ./vscode-extensions-audit
|
||
|
||
.PARAMETER UseSnyk
|
||
Switch to enable Snyk CLI scanning in addition to npm audit
|
||
|
||
.EXAMPLE
|
||
.\vscodepluginscan.ps1
|
||
|
||
.EXAMPLE
|
||
.\vscodepluginscan.ps1 -OutputPath "C:\temp\extensions" -UseSnyk
|
||
#>
|
||
[CmdletBinding()]
|
||
param(
|
||
[string]$OutputPath = "./vscode-extensions-audit",
|
||
[switch]$UseSnyk
|
||
)
|
||
|
||
# Check if VS Code is installed
|
||
if (-not (Get-Command code -ErrorAction SilentlyContinue)) {
|
||
Write-Error "VS Code CLI 'code' not found. Please ensure VS Code is installed and added to PATH."
|
||
exit 1
|
||
}
|
||
|
||
# Check if npm is installed
|
||
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
|
||
Write-Error "npm not found. Please ensure Node.js and npm are installed."
|
||
exit 1
|
||
}
|
||
|
||
# Check if Snyk is installed when requested
|
||
if ($UseSnyk -and -not (Get-Command snyk -ErrorAction SilentlyContinue)) {
|
||
Write-Warning "Snyk CLI not found. Install with: npm install -g snyk"
|
||
$UseSnyk = $false
|
||
}
|
||
|
||
# Create output directory
|
||
if (-not (Test-Path $OutputPath)) {
|
||
New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
|
||
}
|
||
|
||
Write-Host "Starting VS Code extension security scan..." -ForegroundColor Green
|
||
Write-Host "Output directory: $OutputPath" -ForegroundColor Cyan
|
||
|
||
# Get list of installed extensions
|
||
Write-Host "`nGetting list of installed VS Code extensions..." -ForegroundColor Yellow
|
||
$extensions = & code --list-extensions --show-versions
|
||
|
||
if ($extensions.Count -eq 0) {
|
||
Write-Warning "No extensions found."
|
||
exit 0
|
||
}
|
||
|
||
Write-Host "Found $($extensions.Count) extensions" -ForegroundColor Green
|
||
|
||
$auditResults = @()
|
||
|
||
foreach ($extension in $extensions) {
|
||
$extensionParts = $extension -split '@'
|
||
$extensionName = $extensionParts[0]
|
||
$extensionVersion = $extensionParts[1]
|
||
|
||
Write-Host "`nProcessing: $extensionName@$extensionVersion" -ForegroundColor Cyan
|
||
|
||
# Get extension installation path
|
||
$vsCodeExtensionsPath = ""
|
||
if ($env:USERPROFILE) {
|
||
$vsCodeExtensionsPath = Join-Path $env:USERPROFILE ".vscode\extensions"
|
||
}
|
||
|
||
# Find the actual extension directory
|
||
$extensionInstallDir = Get-ChildItem -Path $vsCodeExtensionsPath -Directory | Where-Object { $_.Name -like "$extensionName-*" } | Select-Object -First 1
|
||
|
||
if (-not $extensionInstallDir) {
|
||
Write-Warning " Extension directory not found for $extensionName. Skipping."
|
||
continue
|
||
}
|
||
|
||
Write-Host " Found extension at: $($extensionInstallDir.FullName)" -ForegroundColor Gray
|
||
|
||
# Check if extension has package.json with dependencies
|
||
$extensionPackageJson = Join-Path $extensionInstallDir.FullName "package.json"
|
||
if (-not (Test-Path $extensionPackageJson)) {
|
||
Write-Host " No package.json found for $extensionName. Skipping audit." -ForegroundColor Yellow
|
||
continue
|
||
}
|
||
|
||
# Create extension-specific directory for audit
|
||
$extensionDir = Join-Path $OutputPath $extensionName.Replace('.', '-')
|
||
if (-not (Test-Path $extensionDir)) {
|
||
New-Item -ItemType Directory -Path $extensionDir -Force | Out-Null
|
||
}
|
||
|
||
# Copy package.json if it doesn't exist or is different version
|
||
$targetPackageJson = Join-Path $extensionDir "package.json"
|
||
$shouldCopyPackageJson = $true
|
||
|
||
if (Test-Path $targetPackageJson) {
|
||
try {
|
||
$sourceContent = Get-Content $extensionPackageJson | ConvertFrom-Json
|
||
$targetContent = Get-Content $targetPackageJson | ConvertFrom-Json
|
||
if ($sourceContent.version -eq $targetContent.version -and $sourceContent.name -eq $targetContent.name) {
|
||
$shouldCopyPackageJson = $false
|
||
Write-Host " Package.json already exists with same version. Skipping copy." -ForegroundColor Gray
|
||
}
|
||
} catch {
|
||
# If we can't compare, copy anyway
|
||
Write-Host " Could not compare package.json versions. Copying anyway." -ForegroundColor Yellow
|
||
}
|
||
}
|
||
|
||
if ($shouldCopyPackageJson) {
|
||
Copy-Item $extensionPackageJson $extensionDir -Force
|
||
}
|
||
|
||
# Copy package-lock.json if it exists and package.json was copied
|
||
$extensionPackageLock = Join-Path $extensionInstallDir.FullName "package-lock.json"
|
||
if ($shouldCopyPackageJson -and (Test-Path $extensionPackageLock)) {
|
||
Copy-Item $extensionPackageLock $extensionDir -Force
|
||
}
|
||
|
||
# Copy node_modules if it exists and package.json was copied (some extensions have pre-installed dependencies)
|
||
$extensionNodeModules = Join-Path $extensionInstallDir.FullName "node_modules"
|
||
if ($shouldCopyPackageJson -and (Test-Path $extensionNodeModules)) {
|
||
Write-Host " Copying existing node_modules..." -ForegroundColor Gray
|
||
Copy-Item $extensionNodeModules $extensionDir -Recurse -Force
|
||
}
|
||
|
||
Push-Location $extensionDir
|
||
|
||
try {
|
||
# Check if there are any dependencies to audit
|
||
$packageContent = Get-Content "package.json" | ConvertFrom-Json
|
||
$hasDependencies = $packageContent.dependencies -or $packageContent.devDependencies
|
||
|
||
if (-not $hasDependencies) {
|
||
Write-Host " No dependencies found in $extensionName. Skipping audit." -ForegroundColor Yellow
|
||
Pop-Location
|
||
continue
|
||
}
|
||
|
||
# Install dependencies if node_modules doesn't exist
|
||
if (-not (Test-Path "node_modules")) {
|
||
Write-Host " Installing dependencies..." -ForegroundColor Gray
|
||
$installOutput = & npm install --production 2>&1
|
||
if ($LASTEXITCODE -ne 0) {
|
||
Write-Warning " Failed to install dependencies for $extensionName. Attempting audit anyway."
|
||
}
|
||
}
|
||
|
||
# Run npm audit
|
||
Write-Host " Running npm audit..." -ForegroundColor Gray
|
||
$auditOutput = & npm audit --json 2>&1
|
||
$auditResult = @{
|
||
Extension = $extension
|
||
NpmAudit = $auditOutput
|
||
SnykAudit = $null
|
||
}
|
||
|
||
# Parse npm audit results
|
||
try {
|
||
$auditJson = $auditOutput | ConvertFrom-Json -ErrorAction SilentlyContinue
|
||
if ($auditJson.vulnerabilities) {
|
||
$vulnCount = ($auditJson.vulnerabilities.PSObject.Properties | Measure-Object).Count
|
||
if ($vulnCount -gt 0) {
|
||
Write-Host " ⚠️ Found $vulnCount vulnerabilities in $extensionName" -ForegroundColor Red
|
||
} else {
|
||
Write-Host " ✅ No vulnerabilities found in $extensionName" -ForegroundColor Green
|
||
}
|
||
}
|
||
} catch {
|
||
Write-Host " ℹ️ Audit completed for $extensionName" -ForegroundColor Blue
|
||
}
|
||
|
||
# Run Snyk if requested and available
|
||
if ($UseSnyk) {
|
||
Write-Host " Running Snyk scan..." -ForegroundColor Gray
|
||
$snykOutput = & snyk test --json 2>&1
|
||
$auditResult.SnykAudit = $snykOutput
|
||
|
||
try {
|
||
$snykJson = $snykOutput | ConvertFrom-Json -ErrorAction SilentlyContinue
|
||
if ($snykJson.vulnerabilities) {
|
||
$snykVulnCount = $snykJson.vulnerabilities.Count
|
||
if ($snykVulnCount -gt 0) {
|
||
Write-Host " ⚠️ Snyk found $snykVulnCount vulnerabilities in $extensionName" -ForegroundColor Red
|
||
} else {
|
||
Write-Host " ✅ Snyk found no vulnerabilities in $extensionName" -ForegroundColor Green
|
||
}
|
||
} else {
|
||
Write-Host " ✅ Snyk found no vulnerabilities in $extensionName" -ForegroundColor Green
|
||
}
|
||
} catch {
|
||
Write-Host " ℹ️ Snyk scan completed for $extensionName" -ForegroundColor Blue
|
||
}
|
||
}
|
||
|
||
$auditResults += $auditResult
|
||
|
||
} catch {
|
||
Write-Error " Error processing $extensionName`: $_"
|
||
} finally {
|
||
Pop-Location
|
||
}
|
||
}
|
||
|
||
# Generate summary report
|
||
Write-Host "`n" + "="*60 -ForegroundColor Green
|
||
Write-Host "AUDIT SUMMARY REPORT" -ForegroundColor Green
|
||
Write-Host "="*60 -ForegroundColor Green
|
||
|
||
$reportPath = Join-Path $OutputPath "audit-report.json"
|
||
$auditResults | ConvertTo-Json -Depth 5 | Out-File -FilePath $reportPath -Encoding UTF8
|
||
# Generate HTML report
|
||
$htmlReportPath = Join-Path $OutputPath "audit-report.html"
|
||
$htmlContent = @"
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>VS Code Extensions Security Audit Report</title>
|
||
<style>
|
||
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 20px; background-color: #f5f5f5; }
|
||
.container { max-width: 1200px; margin: 0 auto; background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||
h1 { color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 10px; }
|
||
h2 { color: #34495e; margin-top: 30px; }
|
||
.summary { background-color: #ecf0f1; padding: 15px; border-radius: 5px; margin: 20px 0; }
|
||
.extension { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
|
||
.extension-header { font-weight: bold; font-size: 1.1em; margin-bottom: 10px; }
|
||
.vulnerability { margin: 10px 0; padding: 10px; border-left: 4px solid #e74c3c; background-color: #fdf2f2; }
|
||
.no-vulns { color: #27ae60; font-weight: bold; }
|
||
.has-vulns { color: #e74c3c; font-weight: bold; }
|
||
.audit-source { font-size: 0.9em; color: #7f8c8d; margin-top: 5px; }
|
||
.timestamp { color: #95a5a6; font-size: 0.9em; }
|
||
.stats { display: flex; gap: 20px; margin: 20px 0; }
|
||
.stat-item { background-color: #3498db; color: white; padding: 10px 15px; border-radius: 5px; text-align: center; }
|
||
.stat-value { font-size: 1.5em; font-weight: bold; }
|
||
.stat-label { font-size: 0.9em; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>VS Code Extensions Security Audit Report</h1>
|
||
<div class="timestamp">Generated on: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")</div>
|
||
|
||
<div class="summary">
|
||
<h2>Summary</h2>
|
||
<div class="stats">
|
||
<div class="stat-item">
|
||
<div class="stat-value">$($auditResults.Count)</div>
|
||
<div class="stat-label">Extensions Scanned</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value">$extensionsWithVulns</div>
|
||
<div class="stat-label">With Vulnerabilities</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<h2>Extension Details</h2>
|
||
"@
|
||
|
||
foreach ($result in $auditResults) {
|
||
$extensionName = $result.Extension
|
||
$hasVulnerabilities = $false
|
||
$vulnerabilityDetails = ""
|
||
|
||
# Process npm audit results
|
||
try {
|
||
$npmAudit = $result.NpmAudit | ConvertFrom-Json -ErrorAction SilentlyContinue
|
||
if ($npmAudit.vulnerabilities) {
|
||
$vulnCount = ($npmAudit.vulnerabilities.PSObject.Properties | Measure-Object).Count
|
||
if ($vulnCount -gt 0) {
|
||
$hasVulnerabilities = $true
|
||
$vulnerabilityDetails += "<div class='audit-source'>npm audit found $vulnCount vulnerabilities:</div>"
|
||
foreach ($vulnProperty in $npmAudit.vulnerabilities.PSObject.Properties) {
|
||
$vuln = $vulnProperty.Value
|
||
$vulnerabilityDetails += "<div class='vulnerability'><strong>$($vulnProperty.Name)</strong> - Severity: $($vuln.severity)</div>"
|
||
}
|
||
}
|
||
}
|
||
} catch {
|
||
# Continue processing
|
||
}
|
||
|
||
# Process Snyk results if available
|
||
if ($result.SnykAudit) {
|
||
try {
|
||
$snykAudit = $result.SnykAudit | ConvertFrom-Json -ErrorAction SilentlyContinue
|
||
if ($snykAudit.vulnerabilities -and $snykAudit.vulnerabilities.Count -gt 0) {
|
||
$hasVulnerabilities = $true
|
||
$vulnerabilityDetails += "<div class='audit-source'>Snyk found $($snykAudit.vulnerabilities.Count) vulnerabilities:</div>"
|
||
foreach ($vuln in $snykAudit.vulnerabilities) {
|
||
$vulnerabilityDetails += "<div class='vulnerability'><strong>$($vuln.title)</strong> - Severity: $($vuln.severity)</div>"
|
||
}
|
||
}
|
||
} catch {
|
||
# Continue processing
|
||
}
|
||
}
|
||
|
||
$statusClass = if ($hasVulnerabilities) { "has-vulns" } else { "no-vulns" }
|
||
$statusText = if ($hasVulnerabilities) { "⚠️ Has Vulnerabilities" } else { "✅ No Vulnerabilities" }
|
||
|
||
$htmlContent += @"
|
||
<div class="extension">
|
||
<div class="extension-header">$extensionName</div>
|
||
<div class="$statusClass">$statusText</div>
|
||
$vulnerabilityDetails
|
||
</div>
|
||
"@
|
||
}
|
||
|
||
$htmlContent += @"
|
||
</div>
|
||
</body>
|
||
</html>
|
||
"@
|
||
|
||
$htmlContent | Out-File -FilePath $htmlReportPath -Encoding UTF8
|
||
Write-Host "HTML report saved to: $htmlReportPath" -ForegroundColor Cyan
|
||
Write-Host "Extensions scanned: $($auditResults.Count)" -ForegroundColor Cyan
|
||
Write-Host "Detailed results saved to: $reportPath" -ForegroundColor Cyan
|
||
|
||
# Summary of findings
|
||
$extensionsWithVulns = 0
|
||
foreach ($result in $auditResults) {
|
||
try {
|
||
$npmAudit = $result.NpmAudit | ConvertFrom-Json -ErrorAction SilentlyContinue
|
||
if ($npmAudit.vulnerabilities -and ($npmAudit.vulnerabilities.PSObject.Properties | Measure-Object).Count -gt 0) {
|
||
$extensionsWithVulns++
|
||
}
|
||
} catch {
|
||
# Continue processing
|
||
}
|
||
}
|
||
|
||
if ($extensionsWithVulns -gt 0) {
|
||
Write-Host "⚠️ $extensionsWithVulns extension(s) have vulnerabilities" -ForegroundColor Red
|
||
} else {
|
||
Write-Host "✅ No vulnerabilities found in scanned extensions" -ForegroundColor Green
|
||
}
|
||
|
||
Write-Host "`nScan completed!" -ForegroundColor Green |