197 lines
6.0 KiB
PowerShell
197 lines
6.0 KiB
PowerShell
|
|
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
|