Payload/scripts/kaden/security-pre-push.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Polyglot security gate for Python and PowerShell repos (Tier A / B + Trivy + deps). .DESCRIPTION Windows-first alternative to scripts/kaden/security-runner.mjs for non-Node repos. Copy to <repo>/scripts/kaden/security-pre-push.ps1 and customize stack paths below. .PARAMETER Stack python - xml-migrator style (Semgrep p/python, pip-audit) powershell - utilities / hook repos (Semgrep powershell-security.yml, Trivy) .PARAMETER Mode code - Tier A Semgrep on application paths only parity - Tier B Semgrep (registry auto) on full repo secrets - Trivy secret/misconfig (blocking) deps - pip-audit (python only) pre-push - Tier A + deps + secrets (fast push gate) pr - pre-push + parity (pre-PR gate) .EXAMPLE .\scripts\security-pre-push.ps1 -Stack python -Mode pre-push .EXAMPLE .\scripts\security-pre-push.ps1 -Stack powershell -Mode pr #> [CmdletBinding()] param( [Parameter()] [ValidateSet('python', 'powershell')] [string]$Stack = 'python', [Parameter()] [ValidateSet('code', 'parity', 'secrets', 'deps', 'pre-push', 'pr')] [string]$Mode = 'pre-push' ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' # --- Adapt per repo --------------------------------------------------------- $PythonSemgrepTargets = @('xml_extractor', 'run_migrator.py') $PowerShellSemgrepConfig = 'powershell-security.semgrep.yml' # Trivy --skip-dirs matches doublestar glob patterns; a bare name (e.g. "logs") # only matches at the scan root and silently fails to skip nested occurrences # (e.g. some-project/logs), which can turn a multi-second scan into a # multi-minute one on repos with nested log/output/build directories. $TrivySkipDirs = '**/.git,**/node_modules,**/logs,**/.venv,**/__pycache__' # Registry-based Semgrep configs (auto or p/... packs) depend on a network # rule-fetch that can stall indefinitely ("Loading rules from registry"). # Parity mode bounds this with a hard timeout and fails open (warns, does not # throw) rather than hanging a pre-PR gate forever. $SemgrepParityTimeoutMs = 120000 # --------------------------------------------------------------------------- $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot (Join-Path '..' '..'))).Path Push-Location $repoRoot try { function Get-TrivyCommand { if ($env:LOCALAPPDATA) { $wingetTrivy = Join-Path $env:LOCALAPPDATA 'Microsoft\WinGet\Packages\AquaSecurity.Trivy_Microsoft.Winget.Source_8wekyb3d8bbwe\trivy.exe' if (Test-Path $wingetTrivy) { return $wingetTrivy } } return 'trivy' } function Invoke-External { param( [Parameter(Mandatory = $true)][string]$Name, [Parameter(Mandatory = $true)][string]$Exe, [Parameter(Mandatory = $true)][string[]]$Args ) Write-Output "[security-pre-push] $Name" Write-Output "[security-pre-push] $Exe $($Args -join ' ')" & $Exe @Args if ($LASTEXITCODE -ne 0) { throw "$Name failed with exit code $LASTEXITCODE" } } function Invoke-SemgrepCode { if ($Stack -eq 'python') { $missing = @($PythonSemgrepTargets | Where-Object { -not (Test-Path $_) }) if ($missing.Count -gt 0) { throw "Python Semgrep targets not found: $($missing -join ', ')" } Invoke-External -Name 'Semgrep Tier A (p/python)' -Exe 'semgrep' -Args ( @('scan', '--config', 'p/python', '--error') + $PythonSemgrepTargets ) return } if (-not (Test-Path $PowerShellSemgrepConfig)) { throw "PowerShell Semgrep config not found: $PowerShellSemgrepConfig" } Invoke-External -Name 'Semgrep Tier A (powershell-security)' -Exe 'semgrep' -Args @( 'scan', '--config', $PowerShellSemgrepConfig, '--error', '.' ) } function Invoke-SemgrepParity { $semgrepCmd = Get-Command semgrep -ErrorAction SilentlyContinue if (-not $semgrepCmd) { throw 'semgrep not found on PATH.' } Write-Output '[security-pre-push] Semgrep Tier B (parity)' $semgrepArgs = @('scan', '--config', 'auto', '--metrics', 'on', '--error', '.') $semgrepProcess = Start-Process -FilePath $semgrepCmd.Source -ArgumentList $semgrepArgs -NoNewWindow -PassThru $semgrepExited = $semgrepProcess.WaitForExit($SemgrepParityTimeoutMs) if (-not $semgrepExited) { Stop-Process -Id $semgrepProcess.Id -Force -ErrorAction SilentlyContinue Write-Warning "[security-pre-push] Semgrep parity scan did not finish within $($SemgrepParityTimeoutMs / 1000)s; skipping. Run 'semgrep scan --config auto .' manually when convenient." return } if ($semgrepProcess.ExitCode -ne 0) { throw "Semgrep Tier B (parity) failed with exit code $($semgrepProcess.ExitCode)" } } function Invoke-TrivySecret { $trivy = Get-TrivyCommand Invoke-External -Name 'Trivy secrets/misconfig' -Exe $trivy -Args @( 'fs', '--scanners', 'secret,misconfig', '--severity', 'HIGH,CRITICAL', '--skip-dirs', $TrivySkipDirs, '--exit-code', '1', '.' ) } function Invoke-PythonDependency { if (-not (Test-Path 'requirements.txt')) { Write-Output '[security-pre-push] skip pip-audit (no requirements.txt)' return } $pipAudit = Get-Command pip-audit -ErrorAction SilentlyContinue if (-not $pipAudit) { throw 'pip-audit not found. Install with: pip install pip-audit' } Invoke-External -Name 'pip-audit' -Exe 'pip-audit' -Args @('-r', 'requirements.txt') } $steps = switch ($Mode) { 'code' { @('semgrepCode') } 'parity' { @('semgrepParity') } 'secrets' { @('trivySecrets') } 'deps' { @('pythonDeps') } 'pre-push' { @('semgrepCode', 'pythonDeps', 'trivySecrets') } 'pr' { @('semgrepCode', 'pythonDeps', 'trivySecrets', 'semgrepParity') } } if ($Stack -eq 'powershell') { $steps = $steps | Where-Object { $_ -ne 'pythonDeps' } } foreach ($step in $steps) { switch ($step) { 'semgrepCode' { Invoke-SemgrepCode } 'semgrepParity' { Invoke-SemgrepParity } 'trivySecrets' { Invoke-TrivySecret } 'pythonDeps' { Invoke-PythonDependency } default { throw "Unknown step: $step" } } } Write-Output "[security-pre-push] All requested gates passed (stack=$Stack mode=$Mode)." } finally { Pop-Location } |