From d14e06891441e785c7868d1df6d9272fe12163d2 Mon Sep 17 00:00:00 2001 From: Jurjen Ladenius Date: Fri, 17 Oct 2025 15:04:40 +0200 Subject: [PATCH] - Add copilot usage check script #124960 - Add VSCode Plugin Scan poc script #124961 --- Powershell/Tools/Get-CopilotUsage.ps1 | 529 ++++++++++++++++++++++++++ Powershell/Tools/vscodepluginscan.ps1 | 353 +++++++++++++++++ 2 files changed, 882 insertions(+) create mode 100644 Powershell/Tools/Get-CopilotUsage.ps1 create mode 100644 Powershell/Tools/vscodepluginscan.ps1 diff --git a/Powershell/Tools/Get-CopilotUsage.ps1 b/Powershell/Tools/Get-CopilotUsage.ps1 new file mode 100644 index 0000000..64ff1a8 --- /dev/null +++ b/Powershell/Tools/Get-CopilotUsage.ps1 @@ -0,0 +1,529 @@ +īģŋ<# +.SYNOPSIS + Retrieves GitHub Copilot usage metrics per user for an organization. + +.DESCRIPTION + This script calls the GitHub REST API to get Copilot metrics for an organization, + showing usage per user to help identify who is actively using their premium seats. + +.PARAMETER Organization + The GitHub organization name. + +.PARAMETER Token + GitHub Personal Access Token with 'copilot', 'manage_billing:copilot', or 'read:org' scope. + If not provided, will attempt to use GITHUB_TOKEN environment variable. + +.PARAMETER Days + Number of days to look back (default: 28, max: 28). + +.PARAMETER ExportToExcel + Optional path to export results to Excel file (e.g., "copilot-usage.xlsx"). + +.PARAMETER IncludeUserSeats + Include individual user seat information (who has access and when they last used it). + +.EXAMPLE + .\Get-CopilotUsage.ps1 -Organization "myorg" -Token "ghp_xxxxx" + +.EXAMPLE + .\Get-CopilotUsage.ps1 -Organization "myorg" -Days 7 -ExportToExcel "copilot-usage.xlsx" + +.EXAMPLE + .\Get-CopilotUsage.ps1 -Organization "myorg" -IncludeUserSeats -ExportToExcel "copilot-usage.xlsx" + +.NOTES + API Documentation: https://docs.github.com/en/rest/copilot/copilot-metrics + Required Token Scopes: 'copilot', 'manage_billing:copilot', or 'read:org' + This script requires the ImportExcel module. Install it with: Install-Module -Name ImportExcel +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$Organization, + + [Parameter(Mandatory = $false)] + [string]$Token, + + [Parameter(Mandatory = $false)] + [ValidateRange(1, 28)] + [int]$Days = 28, + + [Parameter(Mandatory = $false)] + [string]$ExportToExcel, + + [Parameter(Mandatory = $false)] + [switch]$IncludeUserSeats +) + +# Check if ImportExcel module is available when export is requested +if (-not [string]::IsNullOrEmpty($ExportToExcel)) { + if (-not (Get-Module -ListAvailable -Name ImportExcel)) { + Write-Warning "ImportExcel module is not installed. Installing it now..." + try { + Install-Module -Name ImportExcel -Scope CurrentUser -Force -AllowClobber + Write-Host "ImportExcel module installed successfully." -ForegroundColor Green + } catch { + Write-Error "Failed to install ImportExcel module. Please install it manually: Install-Module -Name ImportExcel" + Write-Host "Continuing without Excel export..." -ForegroundColor Yellow + $ExportToExcel = "" + } + } + if (-not [string]::IsNullOrEmpty($ExportToExcel)) { + Import-Module ImportExcel + } +} + +# Use environment variable if token not provided +if ([string]::IsNullOrEmpty($Token)) { + $Token = $env:GITHUB_TOKEN + if ([string]::IsNullOrEmpty($Token)) { + Write-Error "GitHub token not provided. Use -Token parameter or set GITHUB_TOKEN environment variable." + exit 1 + } +} + +# API endpoint +$apiUrl = "https://api.github.com/orgs/$Organization/copilot/metrics" + +# Headers for authentication +$headers = @{ + "Accept" = "application/vnd.github+json" + "Authorization" = "Bearer $Token" + "X-GitHub-Api-Version" = "2022-11-28" +} + +Write-Host "Fetching Copilot usage metrics for organization: $Organization" -ForegroundColor Cyan +Write-Host "Time period: Last $Days days" -ForegroundColor Cyan +Write-Host "" + +# Variables to store data +$dailySummary = @() +$languageSummary = @() +$editorSummary = @() +$overallSummary = $null +$userSeats = @() + +try { + # Calculate date range + $since = (Get-Date).AddDays(-$Days).ToString("yyyy-MM-dd") + $until = (Get-Date).ToString("yyyy-MM-dd") + + # Add query parameters + $uri = "$apiUrl`?since=$since&until=$until" + + # Make API request + $response = Invoke-RestMethod -Uri $uri -Headers $headers -Method Get + + if ($null -eq $response -or $response.Count -eq 0) { + Write-Warning "No usage data found for the specified period." + exit 0 + } + + # Process and display results + $userMetrics = @() + + foreach ($dayMetric in $response) { + $date = $dayMetric.date + + if ($null -ne $dayMetric.copilot_ide_code_completions) { + foreach ($editor in $dayMetric.copilot_ide_code_completions.editors) { + foreach ($model in $editor.models) { + foreach ($language in $model.languages) { + $userMetrics += [PSCustomObject]@{ + Date = $date + Editor = $editor.name + Model = $model.name + Language = $language.name + TotalEngagedUsers = $language.total_engaged_users + SuggestionsCount = $language.total_code_suggestions + AcceptancesCount = $language.total_code_acceptances + LinesAccepted = $language.total_code_lines_accepted + LinesSuggested = $language.total_code_lines_suggested + ActiveUsers = $language.total_engaged_users + } + } + } + } + } + } + + # Aggregate by user activity + Write-Host "=== COPILOT USAGE SUMMARY ===" -ForegroundColor Green + Write-Host "" + + if ($userMetrics.Count -eq 0) { + Write-Warning "No detailed metrics available for this period." + } else { + # Group by date and calculate totals + $dailySummary = $userMetrics | Group-Object Date | ForEach-Object { + $dayData = $_.Group + [PSCustomObject]@{ + Date = $_.Name + TotalEngagedUsers = ($dayData | Measure-Object -Property TotalEngagedUsers -Maximum).Maximum + TotalSuggestions = ($dayData | Measure-Object -Property SuggestionsCount -Sum).Sum + TotalAcceptances = ($dayData | Measure-Object -Property AcceptancesCount -Sum).Sum + TotalLinesAccepted = ($dayData | Measure-Object -Property LinesAccepted -Sum).Sum + AcceptanceRate = if (($dayData | Measure-Object -Property SuggestionsCount -Sum).Sum -gt 0) { + [math]::Round((($dayData | Measure-Object -Property AcceptancesCount -Sum).Sum / + ($dayData | Measure-Object -Property SuggestionsCount -Sum).Sum) * 100, 2) + } else { 0 } + } + } | Sort-Object Date -Descending + + # Display daily summary + Write-Host "Daily Summary:" -ForegroundColor Yellow + $dailySummary | Format-Table -AutoSize + + # Calculate overall statistics + $totalUsers = ($dailySummary | Measure-Object -Property TotalEngagedUsers -Maximum).Maximum + $totalSuggestions = ($dailySummary | Measure-Object -Property TotalSuggestions -Sum).Sum + $totalAcceptances = ($dailySummary | Measure-Object -Property TotalAcceptances -Sum).Sum + $overallAcceptanceRate = if ($totalSuggestions -gt 0) { + [math]::Round(($totalAcceptances / $totalSuggestions) * 100, 2) + } else { 0 } + + Write-Host "" + Write-Host "=== OVERALL STATISTICS ===" -ForegroundColor Green + Write-Host "Peak Engaged Users: $totalUsers" -ForegroundColor White + Write-Host "Total Suggestions: $totalSuggestions" -ForegroundColor White + Write-Host "Total Acceptances: $totalAcceptances" -ForegroundColor White + Write-Host "Overall Acceptance Rate: $overallAcceptanceRate%" -ForegroundColor White + Write-Host "" + + # Language breakdown + $languageSummary = $userMetrics | Group-Object Language | ForEach-Object { + $langData = $_.Group + [PSCustomObject]@{ + Language = $_.Name + TotalSuggestions = ($langData | Measure-Object -Property SuggestionsCount -Sum).Sum + TotalAcceptances = ($langData | Measure-Object -Property AcceptancesCount -Sum).Sum + AcceptanceRate = if (($langData | Measure-Object -Property SuggestionsCount -Sum).Sum -gt 0) { + [math]::Round((($langData | Measure-Object -Property AcceptancesCount -Sum).Sum / + ($langData | Measure-Object -Property SuggestionsCount -Sum).Sum) * 100, 2) + } else { 0 } + } + } | Sort-Object TotalAcceptances -Descending + + Write-Host "Language Breakdown:" -ForegroundColor Yellow + $languageSummary | Format-Table -AutoSize + + # Editor breakdown + $editorSummary = $userMetrics | Group-Object Editor | ForEach-Object { + $editorData = $_.Group + [PSCustomObject]@{ + Editor = $_.Name + TotalSuggestions = ($editorData | Measure-Object -Property SuggestionsCount -Sum).Sum + TotalAcceptances = ($editorData | Measure-Object -Property AcceptancesCount -Sum).Sum + } + } | Sort-Object TotalAcceptances -Descending + + Write-Host "Editor Breakdown:" -ForegroundColor Yellow + $editorSummary | Format-Table -AutoSize + + # Create overall summary object for Excel + $overallSummary = [PSCustomObject]@{ + Organization = $Organization + DateRange = "$since to $until" + PeakEngagedUsers = $totalUsers + TotalSuggestions = $totalSuggestions + TotalAcceptances = $totalAcceptances + OverallAcceptanceRate = "$overallAcceptanceRate%" + ReportGeneratedDate = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") + } + } + + # Fetch individual user seat information if requested + if ($IncludeUserSeats) { + Write-Host "" + Write-Host "=== FETCHING USER SEAT INFORMATION ===" -ForegroundColor Green + Write-Host "" + + try { + # First, get the seat assignments + $seatsUrl = "https://api.github.com/orgs/$Organization/copilot/billing/seats" + $page = 1 + $perPage = 100 + $allSeats = @() + + do { + $seatsUri = "$seatsUrl`?per_page=$perPage&page=$page" + $seatsResponse = Invoke-RestMethod -Uri $seatsUri -Headers $headers -Method Get + + if ($null -ne $seatsResponse.seats) { + $allSeats += $seatsResponse.seats + } + + $page++ + } while ($seatsResponse.seats.Count -eq $perPage) + + Write-Host "Found $($allSeats.Count) user seats" -ForegroundColor Cyan + Write-Host "" + Write-Host "NOTE: GitHub does not provide per-user 'credit usage' via API." -ForegroundColor Yellow + Write-Host "Credit calculation is based on billing model: 1 seat = 300 credits/month" -ForegroundColor Yellow + Write-Host "'Wasting credits' means: seat assigned but user inactive >30 days" -ForegroundColor Yellow + Write-Host "" + + # Calculate credits consumed per user + # GitHub Copilot Business/Enterprise charges per seat regardless of usage + # If a user has a seat assigned, they consume the full month's credits + # The "last_activity_at" tells us if they're actually using it + $creditsPerMonth = 300 + $daysInMonth = [DateTime]::DaysInMonth((Get-Date).Year, (Get-Date).Month) + $currentDay = (Get-Date).Day + + # Calculate how many days have passed in the current month + $daysElapsedInMonth = $currentDay + + if ($allSeats.Count -eq 0) { + Write-Warning "No user seats found." + } else { + $userSeats = $allSeats | ForEach-Object { + $username = $_.assignee.login + + $lastActivityDate = if ($_.last_activity_at) { + try { + # Try parsing as DateTime - handles multiple formats + if ($_.last_activity_at -is [DateTime]) { + $_.last_activity_at + } else { + [DateTime]::Parse($_.last_activity_at, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::None) + } + } catch { + # Fallback: try different parsing methods + try { + [DateTime]::ParseExact($_.last_activity_at, "MM/dd/yyyy HH:mm:ss", [System.Globalization.CultureInfo]::InvariantCulture) + } catch { + try { + # ISO 8601 format + [DateTime]::Parse($_.last_activity_at) + } catch { + Write-Verbose "Could not parse date: $($_.last_activity_at) for user $username" + $null + } + } + } + } else { + $null + } + + $daysSinceLastActivity = if ($lastActivityDate) { + [math]::Round(((Get-Date) - $lastActivityDate).TotalDays) + } else { + $null + } + + $isActive = if ($daysSinceLastActivity -ne $null) { + $daysSinceLastActivity -le 30 + } else { + $false + } + + $assignedDate = if ($_.created_at) { + try { + if ($_.created_at -is [DateTime]) { + $_.created_at + } else { + $parsedDate = [DateTime]::Parse($_.created_at, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::None) + $parsedDate + } + } catch { + try { + [DateTime]::ParseExact($_.created_at, "MM/dd/yyyy HH:mm:ss", [System.Globalization.CultureInfo]::InvariantCulture) + } catch { + $null + } + } + } else { + $null + } + + # Calculate credits consumed + # GitHub Copilot billing model: Charges per seat per month + # Once a seat is assigned, the FULL monthly cost applies + # Credits don't depend on usage - they're consumed just by having the seat + + # Check if the seat is currently assigned (not pending cancellation) + $isPendingCancellation = ![string]::IsNullOrEmpty($_.pending_cancellation_date) + + if ($isPendingCancellation) { + # Seat is being cancelled - no credits for full month + $creditsConsumed = 0 + } elseif ($assignedDate -and $assignedDate.Year -eq (Get-Date).Year -and $assignedDate.Month -eq (Get-Date).Month) { + # Assigned THIS month - pro-rate based on days from assignment to end of month + $daysAssignedThisMonth = $currentDay - $assignedDate.Day + 1 + $creditsConsumed = [math]::Round(($daysAssignedThisMonth / $daysInMonth) * $creditsPerMonth, 2) + } else { + # Assigned before this month - FULL month charge + $creditsConsumed = $creditsPerMonth + } + + $creditsRemaining = [math]::Round($creditsPerMonth - $creditsConsumed, 2) + $creditsUsagePercent = if ($creditsPerMonth -gt 0) { + [math]::Round(($creditsConsumed / $creditsPerMonth) * 100, 2) + } else { + 0 + } + + # Determine if credits are being wasted (assigned but not actively using) + $isWastingCredits = ($creditsConsumed -gt 0) -and ($daysSinceLastActivity -eq $null -or $daysSinceLastActivity -gt 30) + + [PSCustomObject]@{ + Username = $username + DisplayName = $_.assignee.name + Email = $_.assignee.email + AssignedAt = $assignedDateFormatted + LastActivityAt = if ($lastActivityDate) { $lastActivityDate.ToString("yyyy-MM-dd HH:mm:ss") } else { "Never" } + DaysSinceLastActivity = if ($daysSinceLastActivity -ne $null) { $daysSinceLastActivity } else { "N/A" } + IsActive30Days = $isActive + IsWastingCredits = $isWastingCredits + DaysAssignedThisMonth = if ($assignedDate) { $daysAssignedThisMonth } else { $currentDay } + CreditsAllocated = $creditsPerMonth + CreditsConsumed = $creditsConsumed + CreditsRemaining = $creditsRemaining + CreditsUsagePercent = $creditsUsagePercent + PendingCancellationDate = if ($_.pending_cancellation_date) { $_.pending_cancellation_date } else { "" } + Editor = $_.last_activity_editor + AssigningTeam = if ($_.assigning_team) { $_.assigning_team.name } else { "" } + } + } | Sort-Object CreditsConsumed -Descending + + Write-Host "Total Seats Assigned: $($userSeats.Count)" -ForegroundColor Cyan + $activeUsers = ($userSeats | Where-Object { $_.IsActive30Days -eq $true }).Count + $inactiveUsers = ($userSeats | Where-Object { $_.IsActive30Days -eq $false }).Count + $wastingCredits = ($userSeats | Where-Object { $_.IsWastingCredits -eq $true }).Count + + Write-Host "Active Users (last 30 days): $activeUsers" -ForegroundColor Green + Write-Host "Inactive Users: $inactiveUsers" -ForegroundColor Yellow + Write-Host "Users Wasting Credits: $wastingCredits" -ForegroundColor Red + Write-Host "" + + # Show top users by credit consumption + Write-Host "All Users by Credit Consumption (Month of $(Get-Date -Format 'MMMM yyyy')):" -ForegroundColor Yellow + $userSeats | Select-Object -First 50 | + Format-Table Username, DaysAssignedThisMonth, CreditsConsumed, CreditsUsagePercent, LastActivityAt, IsWastingCredits, Editor -AutoSize + + # Show users wasting credits + Write-Host "" + Write-Host "Users WASTING Credits (assigned but inactive >30 days):" -ForegroundColor Red + $userSeats | Where-Object { $_.IsWastingCredits -eq $true } | + Select-Object -First 50 | + Format-Table Username, CreditsConsumed, DaysSinceLastActivity, LastActivityAt, AssignedAt -AutoSize + + # Credit usage summary + $totalCreditsAllocated = $userSeats.Count * $creditsPerMonth + $totalCreditsConsumed = ($userSeats | Measure-Object -Property CreditsConsumed -Sum).Sum + $totalCreditsWasted = ($userSeats | Where-Object { $_.IsWastingCredits -eq $true } | Measure-Object -Property CreditsConsumed -Sum).Sum + $creditEfficiency = if ($totalCreditsAllocated -gt 0) { + [math]::Round(($totalCreditsConsumed / $totalCreditsAllocated) * 100, 2) + } else { 0 } + + Write-Host "" + Write-Host "=== CREDIT USAGE ANALYSIS (Month of $(Get-Date -Format 'MMMM yyyy')) ===" -ForegroundColor Green + Write-Host "Credits per User per Month: $creditsPerMonth" -ForegroundColor White + Write-Host "Total Seats: $($userSeats.Count)" -ForegroundColor White + Write-Host "Total Credits Allocated (Full Month): $totalCreditsAllocated ($($userSeats.Count) users x $creditsPerMonth)" -ForegroundColor White + Write-Host "Total Credits Consumed (Month-to-Date): $([math]::Round($totalCreditsConsumed, 2))" -ForegroundColor Cyan + Write-Host "Total Credits on WASTED Seats: $([math]::Round($totalCreditsWasted, 2))" -ForegroundColor Red + Write-Host "Potential Monthly Savings (remove inactive): $([math]::Round(($wastingCredits * $creditsPerMonth), 2)) credits" -ForegroundColor Yellow + Write-Host "" + Write-Host "Days Elapsed in Month: $currentDay of $daysInMonth" -ForegroundColor White + Write-Host "Credit Utilization: $creditEfficiency% (of full-month allocation)" -ForegroundColor $(if ($creditEfficiency -lt 50) { "Red" } elseif ($creditEfficiency -lt 75) { "Yellow" } else { "Green" }) + Write-Host "" + + # Recommendations + if ($wastingCredits -gt 0) { + Write-Host "💡 RECOMMENDATIONS:" -ForegroundColor Cyan + Write-Host " → Remove $wastingCredits inactive user(s) to save ~$([math]::Round(($wastingCredits * $creditsPerMonth), 2)) credits/month" -ForegroundColor Yellow + Write-Host " → That's approximately `$$([math]::Round(($wastingCredits * 19), 2))/month in wasted costs (at ~`$19/seat)" -ForegroundColor Yellow + } + } + } catch { + Write-Warning "Failed to retrieve user seat information: $_" + Write-Host "You may need 'manage_billing:copilot' scope to access seat details." -ForegroundColor Yellow + } + } +} catch { + Write-Error "An error occurred: $_" + exit 1 +} + +# Export to Excel if requested +if (-not [string]::IsNullOrEmpty($ExportToExcel)) { + Write-Host "" + Write-Host "Exporting to Excel: $ExportToExcel" -ForegroundColor Cyan + + try { + # Remove existing file if it exists + if (Test-Path $ExportToExcel) { + Remove-Item $ExportToExcel -Force + } + + # Export to multiple worksheets + $excelParams = @{ + Path = $ExportToExcel + AutoSize = $true + FreezeTopRow = $true + BoldTopRow = $true + } + + $sheetCount = 1 + + # Sheet 1: Overall Summary + if ($null -ne $overallSummary) { + $overallSummary | Export-Excel @excelParams -WorksheetName "Overall Summary" -TableName "OverallSummary" + $sheetCount++ + } + + # Sheet 2: Daily Summary + if ($dailySummary.Count -gt 0) { + $dailySummary | Export-Excel @excelParams -WorksheetName "Daily Summary" -TableName "DailySummary" + $sheetCount++ + } + + # Sheet 3: Language Breakdown + if ($languageSummary.Count -gt 0) { + $languageSummary | Export-Excel @excelParams -WorksheetName "Language Breakdown" -TableName "LanguageBreakdown" + $sheetCount++ + } + + # Sheet 4: Editor Breakdown + if ($editorSummary.Count -gt 0) { + $editorSummary | Export-Excel @excelParams -WorksheetName "Editor Breakdown" -TableName "EditorBreakdown" + $sheetCount++ + } + + # Sheet 5: Detailed Metrics + if ($userMetrics.Count -gt 0) { + $userMetrics | Sort-Object Date -Descending | + Export-Excel @excelParams -WorksheetName "Detailed Metrics" -TableName "DetailedMetrics" + $sheetCount++ + } + + # Sheet 6: User Seats (if included) + if ($userSeats.Count -gt 0) { + $userSeats | Export-Excel @excelParams -WorksheetName "User Seats" -TableName "UserSeats" + $sheetCount++ + } + + Write-Host "Results exported to: $ExportToExcel" -ForegroundColor Green + Write-Host "Excel file contains multiple worksheets:" -ForegroundColor Green + Write-Host " - Overall Summary" -ForegroundColor White + Write-Host " - Daily Summary" -ForegroundColor White + Write-Host " - Language Breakdown" -ForegroundColor White + Write-Host " - Editor Breakdown" -ForegroundColor White + Write-Host " - Detailed Metrics" -ForegroundColor White + if ($userSeats.Count -gt 0) { + Write-Host " - User Seats (per-user credit information)" -ForegroundColor White + } + } catch { + Write-Error "Failed to export to Excel: $_" + } +} + +Write-Host "" +Write-Host "Note: For individual user seat management in browser, visit:" -ForegroundColor Cyan +Write-Host "https://github.com/organizations/$Organization/settings/copilot/seat_management" -ForegroundColor Cyan + +Write-Host "" +Write-Host "Script completed." -ForegroundColor Green diff --git a/Powershell/Tools/vscodepluginscan.ps1 b/Powershell/Tools/vscodepluginscan.ps1 new file mode 100644 index 0000000..2c1d9e9 --- /dev/null +++ b/Powershell/Tools/vscodepluginscan.ps1 @@ -0,0 +1,353 @@ +<# +.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 = @" + + + + + + VS Code Extensions Security Audit Report + + + +
+

VS Code Extensions Security Audit Report

+
Generated on: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
+ +
+

Summary

+
+
+
$($auditResults.Count)
+
Extensions Scanned
+
+
+
$extensionsWithVulns
+
With Vulnerabilities
+
+
+
+ +

Extension Details

+"@ + +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 += "
npm audit found $vulnCount vulnerabilities:
" + foreach ($vulnProperty in $npmAudit.vulnerabilities.PSObject.Properties) { + $vuln = $vulnProperty.Value + $vulnerabilityDetails += "
$($vulnProperty.Name) - Severity: $($vuln.severity)
" + } + } + } + } 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 += "
Snyk found $($snykAudit.vulnerabilities.Count) vulnerabilities:
" + foreach ($vuln in $snykAudit.vulnerabilities) { + $vulnerabilityDetails += "
$($vuln.title) - Severity: $($vuln.severity)
" + } + } + } catch { + # Continue processing + } + } + + $statusClass = if ($hasVulnerabilities) { "has-vulns" } else { "no-vulns" } + $statusText = if ($hasVulnerabilities) { "âš ī¸ Has Vulnerabilities" } else { "✅ No Vulnerabilities" } + + $htmlContent += @" +
+
$extensionName
+
$statusText
+ $vulnerabilityDetails +
+"@ +} + +$htmlContent += @" +
+ + +"@ + +$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 \ No newline at end of file