Skip to content

Update psscriptanalyzer-sarif-reports.yml #22

Update psscriptanalyzer-sarif-reports.yml

Update psscriptanalyzer-sarif-reports.yml #22

name: PowerShell Corporate Lint (PSScriptAnalyzer + SARIF + Reports) [Report-Only]
on:
push:
branches: [main, develop]
paths:
- "**/*.ps1"
- "**/*.psm1"
- "**/*.psd1"
- ".psscriptanalyzer.psd1"
- ".github/workflows/psscriptanalyzer-sarif-reports.yml"
pull_request:
branches: [main, develop]
paths:
- "**/*.ps1"
- "**/*.psm1"
- "**/*.psd1"
- ".psscriptanalyzer.psd1"
- ".github/workflows/psscriptanalyzer-sarif-reports.yml"
workflow_dispatch:
concurrency:
group: powershell-lint-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
security-events: write
jobs:
psa-analyze:
name: PSScriptAnalyzer Report-Only (SARIF + Artifacts)
runs-on: windows-latest
timeout-minutes: 35
env:
PSA_VERSION: "1.24.0"
SETTINGS_FILE: ".psscriptanalyzer.psd1"
OUT_DIR: "lint-reports"
OUT_JSON: "psa-results.json"
OUT_CSV: "psa-results.csv"
OUT_MD: "psa-results.md"
OUT_SARIF: "psa-results.sarif"
OUT_FAILTXT: "psa-invocation-failed.txt"
OUT_SUMMARY: "psa-summary.txt"
ANALYZE_ROOTS: >-
BlueTeam-Tools
Core-ScriptLibrary
ITSM-Templates-SVR
ITSM-Templates-WKS
ProSuite-Hub
SysAdmin-Tools
EXCLUDE_SUBTREE: "SysAdmin-Tools/GroupPolicyObjects-Templates"
PRUNE_DIR_REGEX: '(?i)[\\/](\.git|node_modules|dist|build|artifacts|\.next)[\\/]'
SARIF_CATEGORY: "powershell/psscriptanalyzer"
steps:
- name: Checkout (sparse)
uses: actions/checkout@v4
with:
fetch-depth: 0
sparse-checkout-cone-mode: false
sparse-checkout: |
BlueTeam-Tools
Core-ScriptLibrary
ITSM-Templates-SVR
ITSM-Templates-WKS
ProSuite-Hub
SysAdmin-Tools
.psscriptanalyzer.psd1
!SysAdmin-Tools/GroupPolicyObjects-Templates
- name: Git long paths
shell: pwsh
run: |
git config --global core.longpaths true
- name: Prepare output folder
shell: pwsh
run: |
$outDir = Join-Path $env:GITHUB_WORKSPACE $env:OUT_DIR
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
- name: Initialize SARIF baseline (always create file)
if: always()
shell: pwsh
run: |
$ErrorActionPreference = 'Continue'
$outDir = Join-Path $env:GITHUB_WORKSPACE $env:OUT_DIR
$sarifOut = Join-Path $outDir $env:OUT_SARIF
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
$baseline = @{
'$schema' = 'https://json.schemastore.org/sarif-2.1.0.json'
version = '2.1.0'
runs = @(@{ tool = @{ driver = @{ name = 'PSScriptAnalyzer'; version = '0' } }; results = @() })
}
$baseline | ConvertTo-Json -Depth 8 | Set-Content -Encoding UTF8 $sarifOut
Write-Host "Baseline SARIF created: $sarifOut"
- name: Ensure module cache directories exist
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path "$HOME\Documents\PowerShell\Modules" | Out-Null
New-Item -ItemType Directory -Force -Path "$HOME\Documents\WindowsPowerShell\Modules" | Out-Null
- name: Cache PowerShell modules
uses: actions/cache@v4
with:
path: |
~\Documents\PowerShell\Modules
~\Documents\WindowsPowerShell\Modules
key: psmodules-${{ runner.os }}-psa-${{ env.PSA_VERSION }}
- name: Install PSScriptAnalyzer
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$v = $env:PSA_VERSION
$have = Get-Module -ListAvailable PSScriptAnalyzer |
Sort-Object Version -Descending |
Select-Object -First 1
if (-not $have -or $have.Version.ToString() -ne $v) {
Install-Module PSScriptAnalyzer -RequiredVersion $v -Force -Scope CurrentUser -AllowClobber
}
Import-Module PSScriptAnalyzer -RequiredVersion $v -Force
Write-Host "PSScriptAnalyzer version loaded: $((Get-Module PSScriptAnalyzer).Version)"
- name: Run PSScriptAnalyzer + Build Reports (REPORT-ONLY)
if: always()
shell: pwsh
run: |
$ErrorActionPreference = 'Continue'
$outDir = Join-Path $env:GITHUB_WORKSPACE $env:OUT_DIR
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
$jsonOut = Join-Path $outDir $env:OUT_JSON
$csvOut = Join-Path $outDir $env:OUT_CSV
$mdOut = Join-Path $outDir $env:OUT_MD
$sarifOut = Join-Path $outDir $env:OUT_SARIF
$failOut = Join-Path $outDir $env:OUT_FAILTXT
$sumOut = Join-Path $outDir $env:OUT_SUMMARY
$root = $env:GITHUB_WORKSPACE
$prune = [regex]::new($env:PRUNE_DIR_REGEX)
$excludeSub = ($env:EXCLUDE_SUBTREE -replace '/', [IO.Path]::DirectorySeparatorChar)
$excludeSubEsc = [regex]::Escape($excludeSub)
$roots = @()
foreach ($token in ($env:ANALYZE_ROOTS -split '\s+')) {
if ([string]::IsNullOrWhiteSpace($token)) { continue }
$p = Join-Path $root $token
if (Test-Path $p) { $roots += $p }
}
$files = @()
if ($roots.Count -gt 0) {
$files = foreach ($r in $roots) {
Get-ChildItem -Path $r -Recurse -File -Include *.ps1,*.psm1,*.psd1 -ErrorAction SilentlyContinue |
Where-Object {
($_.FullName -notmatch $prune) -and
($_.FullName -notmatch $excludeSubEsc)
}
}
}
$files = @($files | Sort-Object FullName -Unique)
$count = @($files).Count
$settingsPath = Join-Path $env:GITHUB_WORKSPACE $env:SETTINGS_FILE
$useSettings = (Test-Path $settingsPath)
$settingsArgs = @{}
if ($useSettings) { $settingsArgs['Settings'] = $settingsPath }
$results = @()
try {
if ($count -gt 0) {
# IMPORTANT: analyze one file at a time to avoid Path binding issues on runners
$paths = @($files | ForEach-Object { [string]$_.FullName })
$all = New-Object System.Collections.Generic.List[object]
foreach ($p in $paths) {
if ([string]::IsNullOrWhiteSpace($p)) { continue }
try {
$r = Invoke-ScriptAnalyzer -Path $p -Recurse:$false @settingsArgs
if ($r) { foreach ($item in @($r)) { $all.Add($item) } }
} catch {
# keep going; record failure details once
$_ | Out-String | Add-Content -Encoding UTF8 $failOut
}
}
$results = @($all)
} else {
"No PowerShell files found to analyze." | Set-Content -Encoding UTF8 $sumOut
}
}
catch {
$_ | Out-String | Set-Content -Encoding UTF8 $failOut
$results = @()
}
if (-not $results) { $results = @() }
# Always write reports (even if empty)
$results | ConvertTo-Json -Depth 8 | Set-Content -Encoding UTF8 $jsonOut
$results |
Select-Object RuleName, Severity, Message, ScriptName, Line, Column |
Export-Csv -Path $csvOut -NoTypeInformation -Encoding UTF8
$bySev = $results | Group-Object Severity | Sort-Object Name
$byRule = $results | Group-Object RuleName | Sort-Object Count -Descending
$md = New-Object System.Collections.Generic.List[string]
$md.Add("# PSScriptAnalyzer Report (Report-Only)")
$md.Add("")
$md.Add("**Files scanned:** $count")
$md.Add("**Settings file used:** $useSettings ($($env:SETTINGS_FILE))")
$md.Add("**Total findings:** $(@($results).Count)")
$md.Add("")
$md.Add("## Findings by Severity")
if ($bySev.Count -eq 0) { $md.Add("- None") }
else { foreach ($g in $bySev) { $md.Add("- **$($g.Name)**: $($g.Count)") } }
$md.Add("")
$md.Add("## Top Rules")
$top = $byRule | Select-Object -First 20
if ($top.Count -eq 0) { $md.Add("- None") }
else { foreach ($g in $top) { $md.Add("- **$($g.Name)**: $($g.Count)") } }
$md.Add("")
$md.Add("> Note: This workflow is report-only. Findings do not fail the job. Artifacts are uploaded when present.")
$md | Set-Content -Encoding UTF8 $mdOut
$summary = New-Object System.Collections.Generic.List[string]
$summary.Add("PSScriptAnalyzer (Report-Only)")
$summary.Add("Files scanned: $count")
$summary.Add("Settings file used: $useSettings ($($env:SETTINGS_FILE))")
$summary.Add("Total findings: $(@($results).Count)")
foreach ($g in $bySev) { $summary.Add(" $($g.Name): $($g.Count)") }
$summary | Set-Content -Encoding UTF8 $sumOut
function Get-SarifLevel([string]$sev) {
switch -Regex ($sev) {
'Error' { 'error' }
'Warning' { 'warning' }
default { 'note' }
}
}
# Update SARIF file (baseline exists already)
try {
$sarif = @{
'$schema' = 'https://json.schemastore.org/sarif-2.1.0.json'
version = '2.1.0'
runs = @(
@{
tool = @{
driver = @{
name = 'PSScriptAnalyzer'
informationUri = 'https://github.com/PowerShell/PSScriptAnalyzer'
version = (Get-Module PSScriptAnalyzer).Version.ToString()
}
}
results = @()
}
)
}
foreach ($r in $results) {
$scriptRel = [string]$r.ScriptName
if (-not [string]::IsNullOrWhiteSpace($scriptRel)) {
$scriptRel = $scriptRel.Replace("$env:GITHUB_WORKSPACE\", '').Replace("$env:GITHUB_WORKSPACE/", '')
}
$sarif.runs[0].results += @{
ruleId = [string]$r.RuleName
level = (Get-SarifLevel ([string]$r.Severity))
message = @{ text = [string]$r.Message }
locations = @(
@{
physicalLocation = @{
artifactLocation = @{ uri = $scriptRel }
region = @{
startLine = [int]$r.Line
startColumn = [int]$r.Column
}
}
}
)
}
}
$sarif | ConvertTo-Json -Depth 12 | Set-Content -Encoding UTF8 $sarifOut
}
catch {
$_ | Out-String | Add-Content -Encoding UTF8 $failOut
}
exit 0
- name: Pre-upload diagnostics (ensure SARIF exists)
if: always()
shell: pwsh
run: |
$ErrorActionPreference = 'Continue'
$outDir = Join-Path $env:GITHUB_WORKSPACE $env:OUT_DIR
$sarifOut = Join-Path $outDir $env:OUT_SARIF
Write-Host "GITHUB_WORKSPACE=$env:GITHUB_WORKSPACE"
Write-Host "OUT_DIR=$outDir"
if (Test-Path $outDir) {
Get-ChildItem -Recurse -Force $outDir | Format-Table FullName, Length
} else {
Write-Host "Output dir missing; creating."
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
}
if (-not (Test-Path $sarifOut)) {
Write-Host "SARIF missing — creating baseline now."
$baseline = @{
'$schema' = 'https://json.schemastore.org/sarif-2.1.0.json'
version = '2.1.0'
runs = @(@{ tool = @{ driver = @{ name = 'PSScriptAnalyzer'; version = '0' } }; results = @() })
}
$baseline | ConvertTo-Json -Depth 8 | Set-Content -Encoding UTF8 $sarifOut
}
- name: Upload lint artifacts (never fail if empty)
if: always()
uses: actions/upload-artifact@v4
with:
name: powershell-lint-reports
path: ${{ env.OUT_DIR }}/**
if-no-files-found: warn
retention-days: 30
- name: Upload SARIF to GitHub Code Scanning (report-only)
if: always() && hashFiles(format('{0}/{1}', env.OUT_DIR, env.OUT_SARIF)) != ''
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: ${{ env.OUT_DIR }}/${{ env.OUT_SARIF }}
category: ${{ env.SARIF_CATEGORY }}