Update psscriptanalyzer-sarif-reports.yml #22
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }} |