param( [string]$EvidenceDate = (Get-Date -Format 'yyyy-MM-dd') ) $ErrorActionPreference = 'Stop' $projectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path $frontendRoot = Join-Path $projectRoot 'frontend\admin' $evidenceRoot = Join-Path $projectRoot "docs\evidence\ops\$EvidenceDate\sca" $goCacheRoot = Join-Path $projectRoot '.cache' $goBuildCache = Join-Path $goCacheRoot 'go-build' $goModCache = Join-Path $goCacheRoot 'gomod' $goPath = Join-Path $goCacheRoot 'gopath' New-Item -ItemType Directory -Force $evidenceRoot, $goBuildCache, $goModCache, $goPath | Out-Null function Invoke-CapturedCommand { param( [Parameter(Mandatory = $true)][string]$FilePath, [string[]]$ArgumentList = @(), [Parameter(Mandatory = $true)][string]$WorkingDirectory, [Parameter(Mandatory = $true)][string]$StdOutPath, [Parameter(Mandatory = $true)][string]$StdErrPath ) Remove-Item $StdOutPath, $StdErrPath -Force -ErrorAction SilentlyContinue $process = Start-Process ` -FilePath $FilePath ` -ArgumentList $ArgumentList ` -WorkingDirectory $WorkingDirectory ` -PassThru ` -WindowStyle Hidden ` -RedirectStandardOutput $StdOutPath ` -RedirectStandardError $StdErrPath ` -Wait return $process.ExitCode } function Get-NpmAuditCounts { param( [Parameter(Mandatory = $true)][string]$JsonPath ) if (-not (Test-Path $JsonPath)) { return $null } $raw = Get-Content $JsonPath -Raw if ([string]::IsNullOrWhiteSpace($raw)) { return $null } $payload = $raw | ConvertFrom-Json if (-not $payload.metadata -or -not $payload.metadata.vulnerabilities) { return $null } return $payload.metadata.vulnerabilities } function Get-GovulnFindingCount { param( [Parameter(Mandatory = $true)][string]$JsonPath ) if (-not (Test-Path $JsonPath)) { return [pscustomobject]@{ Count = 0 IDs = @() } } $count = 0 $ids = New-Object System.Collections.Generic.HashSet[string] $insideFinding = $false foreach ($line in Get-Content $JsonPath) { if ($line -match '"finding"') { $insideFinding = $true $count++ continue } if ($insideFinding -and $line -match '"osv":\s*"([^"]+)"') { [void]$ids.Add($Matches[1]) $insideFinding = $false } } return [pscustomobject]@{ Count = $count IDs = @($ids) } } $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' $prodAuditJson = Join-Path $evidenceRoot "npm-audit-prod-$timestamp.json" $prodAuditErr = Join-Path $evidenceRoot "npm-audit-prod-$timestamp.stderr.txt" $fullAuditJson = Join-Path $evidenceRoot "npm-audit-full-$timestamp.json" $fullAuditErr = Join-Path $evidenceRoot "npm-audit-full-$timestamp.stderr.txt" $govulnJson = Join-Path $evidenceRoot "govulncheck-$timestamp.jsonl" $govulnErr = Join-Path $evidenceRoot "govulncheck-$timestamp.stderr.txt" $summaryPath = Join-Path $evidenceRoot "SCA_SUMMARY_$timestamp.md" $prodAuditExit = Invoke-CapturedCommand ` -FilePath 'npm.cmd' ` -ArgumentList @('audit', '--omit=dev', '--json', '--registry=https://registry.npmjs.org/') ` -WorkingDirectory $frontendRoot ` -StdOutPath $prodAuditJson ` -StdErrPath $prodAuditErr $fullAuditExit = Invoke-CapturedCommand ` -FilePath 'npm.cmd' ` -ArgumentList @('audit', '--json', '--registry=https://registry.npmjs.org/') ` -WorkingDirectory $frontendRoot ` -StdOutPath $fullAuditJson ` -StdErrPath $fullAuditErr Push-Location $projectRoot try { $env:GOCACHE = $goBuildCache $env:GOMODCACHE = $goModCache $env:GOPATH = $goPath $govulnExit = Invoke-CapturedCommand ` -FilePath 'go' ` -ArgumentList @('run', 'golang.org/x/vuln/cmd/govulncheck@latest', '-json', './...') ` -WorkingDirectory $projectRoot ` -StdOutPath $govulnJson ` -StdErrPath $govulnErr } finally { Pop-Location Remove-Item Env:GOCACHE, Env:GOMODCACHE, Env:GOPATH -ErrorAction SilentlyContinue } $prodCounts = Get-NpmAuditCounts -JsonPath $prodAuditJson $fullCounts = Get-NpmAuditCounts -JsonPath $fullAuditJson $govulnFindings = Get-GovulnFindingCount -JsonPath $govulnJson $prodFindingSummary = if ($prodCounts) { "info=$($prodCounts.info) low=$($prodCounts.low) moderate=$($prodCounts.moderate) high=$($prodCounts.high) critical=$($prodCounts.critical) total=$($prodCounts.total)" } else { 'unavailable' } $fullFindingSummary = if ($fullCounts) { "info=$($fullCounts.info) low=$($fullCounts.low) moderate=$($fullCounts.moderate) high=$($fullCounts.high) critical=$($fullCounts.critical) total=$($fullCounts.total)" } else { 'unavailable' } $govulnIDsSummary = if ($govulnFindings.IDs.Count -gt 0) { ($govulnFindings.IDs | Sort-Object) -join ', ' } else { 'none' } $prodAuditJsonName = Split-Path $prodAuditJson -Leaf $prodAuditErrName = Split-Path $prodAuditErr -Leaf $fullAuditJsonName = Split-Path $fullAuditJson -Leaf $fullAuditErrName = Split-Path $fullAuditErr -Leaf $govulnJsonName = Split-Path $govulnJson -Leaf $govulnErrName = Split-Path $govulnErr -Leaf $summaryLines = @( '# SCA Summary', '', "- Generated at: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss zzz')", "- Project root: $projectRoot", '', '## Commands', '', '- `cd frontend/admin && npm.cmd audit --omit=dev --json --registry=https://registry.npmjs.org/`', '- `cd frontend/admin && npm.cmd audit --json --registry=https://registry.npmjs.org/`', '- `go run golang.org/x/vuln/cmd/govulncheck@latest -json ./...`', '', '## Exit Codes', '', "- npm audit production: $prodAuditExit", "- npm audit full: $fullAuditExit", "- govulncheck: $govulnExit", '', '## Findings', '', "- npm audit production: $prodFindingSummary", "- npm audit full: $fullFindingSummary", "- govulncheck reachable findings: $($govulnFindings.Count)", "- govulncheck reachable IDs: $govulnIDsSummary", '', '## Evidence Files', '', "- $prodAuditJsonName", "- $prodAuditErrName", "- $fullAuditJsonName", "- $fullAuditErrName", "- $govulnJsonName", "- $govulnErrName", '' ) Set-Content -Path $summaryPath -Value ($summaryLines -join [Environment]::NewLine) -Encoding UTF8 Get-Content $summaryPath