Merged PR 63189: Add copilot usage and vscode plugin script

- Add copilot usage check script #124960
- Add VSCode Plugin Scan poc script #124961

Related work items: #124960, #124961
This commit is contained in:
Jurjen Ladenius Effectory
2025-10-17 13:05:51 +00:00
2 changed files with 882 additions and 0 deletions

View File

@@ -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

View File

@@ -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 = @"
<!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