Examples/Invoke-IntuneScriptGate.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Runs IntuneScriptLab's analysis as a CI gate: annotations, a job summary and an exit code. .DESCRIPTION Analyzes every PowerShell script under the given paths with Test-IntuneScript, the way Intune will run them (script type, context and bitness from the folder, the file name or a directive), then reports for a build: - one GitHub Actions annotation per finding (::error, ::warning, ::notice with the file, line, column and rule), on by default when GITHUB_ACTIONS is set - a Markdown summary appended to the file GITHUB_STEP_SUMMARY names, or to -SummaryPath - a SARIF 2.1.0 log at -SarifPath, for github/codeql-action/upload-sarif - the findings themselves on the pipeline with -PassThru - exit code 1 when any finding reaches -FailOn (Error by default), 0 otherwise The script ships in the module's Examples folder and is what the workflow template intune-script-gate.yml calls; it works from any runner with the module installed, and from a copy of the module checked into a repository (it imports the module next to itself first, then by name). .PARAMETER Path Folders or scripts to analyze; folders are searched for .ps1 files recursively. Default: the current directory. .PARAMETER FailOn The lowest severity that fails the build: Error (default), Warning, Information, or None to report without failing. .PARAMETER IncludeRule Rule names (wildcards allowed) to run; everything else is skipped. .PARAMETER ExcludeRule Rule names (wildcards allowed) to skip. IslAssumedContext is a common one to exclude once the folder layout is settled. .PARAMETER ScriptType Passed to Test-IntuneScript when every script under -Path is of one type; Auto (default) infers per script. .PARAMETER Annotate Write GitHub Actions annotations. Defaults to on when the GITHUB_ACTIONS variable is set; -Annotate:$false silences them. .PARAMETER SummaryPath The Markdown file to append the summary to. Default: the file named by GITHUB_STEP_SUMMARY, or none. .PARAMETER Root The folder file paths are made relative to in annotations and the summary. Default: GITHUB_WORKSPACE, or the current directory. .PARAMETER Settings A settings file path or hashtable for the analysis. Omitted, the nearest IntuneScriptLab.settings.psd1 above each script applies. .PARAMETER SarifPath Write the findings as a SARIF 2.1.0 log to this file (Export-IntuneFindingSarif), for upload to code scanning. Paths inside it are relative to -Root. .PARAMETER PassThru Also write the IntuneScriptLab.Finding objects to the pipeline. .EXAMPLE .\Invoke-IntuneScriptGate.ps1 -Path .\Intune Analyzes everything under .\Intune and exits 1 if any script has an error-level finding. .EXAMPLE .\Invoke-IntuneScriptGate.ps1 -Path .\Remediations, .\Win32 -FailOn Warning -ExcludeRule IslAssumedContext Fails on warnings too, without the context note. .EXAMPLE .\Invoke-IntuneScriptGate.ps1 -Path . -FailOn None -PassThru | Group-Object RuleName Reports without failing and returns the findings for further processing. .INPUTS None. .OUTPUTS IntuneScriptLab.Finding with -PassThru; otherwise nothing on the pipeline. The exit code carries the verdict. .NOTES Author: Jeffrey Stuhr. Annotations use the workflow-command syntax GitHub Actions parses from stdout; on any other CI they are plain lines and can be switched off. .LINK https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions #> [CmdletBinding()] [OutputType('IntuneScriptLab.Finding')] # GitHub reads workflow commands (::error, ::warning) from plain stdout lines, which is what # Write-Host produces; the findings themselves go to the pipeline only with -PassThru [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] param( [string[]]$Path = @('.'), [ValidateSet('Error', 'Warning', 'Information', 'None')] [string]$FailOn = 'Error', [string[]]$IncludeRule, [string[]]$ExcludeRule, [ValidateSet('Auto', 'Detection', 'Remediation', 'PlatformScript', 'Win32Detection', 'Win32Requirement')] [string]$ScriptType = 'Auto', [switch]$Annotate = [bool]$env:GITHUB_ACTIONS, [string]$SummaryPath = $env:GITHUB_STEP_SUMMARY, [string]$SarifPath, [string]$Root = $(if ($env:GITHUB_WORKSPACE) { $env:GITHUB_WORKSPACE } else { (Get-Location).Path }), $Settings, [switch]$PassThru ) $ErrorActionPreference = 'Stop' # The module next to this script when it runs from the module folder, otherwise the installed one $shipped = Join-Path -Path (Split-Path -Parent $PSScriptRoot) -ChildPath 'IntuneScriptLab.psd1' if (Test-Path -LiteralPath $shipped) { Import-Module $shipped -Force } else { Import-Module IntuneScriptLab } $severityRank = @{ Information = 0; Warning = 1; Error = 2 } $annotationLevel = @{ Information = 'notice'; Warning = 'warning'; Error = 'error' } $rootPath = (Resolve-Path -LiteralPath $Root).ProviderPath.TrimEnd('\', '/') function Get-RelativePath { param([string]$FullPath) if ($FullPath.StartsWith($rootPath, [System.StringComparison]::OrdinalIgnoreCase)) { $FullPath.Substring($rootPath.Length).TrimStart('\', '/').Replace('\', '/') } else { $FullPath } } function ConvertTo-AnnotationText { # Workflow commands take a percent-encoded message on one line param([string]$Text) $Text.Replace('%', '%25').Replace("`r", '%0D').Replace("`n", '%0A') } $testSplat = @{ Path = $Path; ScriptType = $ScriptType } if ($PSBoundParameters.ContainsKey('Settings')) { $testSplat.Settings = $Settings } if ($IncludeRule) { $testSplat.IncludeRule = $IncludeRule } if ($ExcludeRule) { $testSplat.ExcludeRule = $ExcludeRule } $findings = @(Test-IntuneScript @testSplat) $scripts = @($findings | ForEach-Object { $_.ScriptPath } | Sort-Object -Unique) $counts = @{ Error = 0; Warning = 0; Information = 0 } foreach ($finding in $findings) { $counts[$finding.Severity]++ } if ($findings) { $findings | Format-Table | Out-String -Width 200 | Write-Host } Write-Host ("IntuneScriptLab: $($findings.Count) finding(s) in $($scripts.Count) script(s): " + "$($counts.Error) error(s), $($counts.Warning) warning(s), $($counts.Information) note(s)") if ($Annotate) { foreach ($finding in $findings) { $location = "file=$(Get-RelativePath -FullPath $finding.ScriptPath)" if ($finding.Line -gt 0) { $location += ",line=$($finding.Line),col=$([Math]::Max(1, $finding.Column))" } $message = ConvertTo-AnnotationText -Text $finding.Message Write-Host "::$($annotationLevel[$finding.Severity]) $location,title=$($finding.RuleName)::$message" } } if ($SummaryPath) { $lines = [System.Collections.Generic.List[string]]::new() $lines.Add("## IntuneScriptLab: $($findings.Count) finding(s) in $($scripts.Count) script(s)") $lines.Add('') $lines.Add("$($counts.Error) error(s), $($counts.Warning) warning(s), $($counts.Information) note(s); " + "gate: fail on $FailOn.") if ($findings) { $lines.Add('') $lines.Add('| Severity | Rule | Script | Line | Message |') $lines.Add('|---|---|---|---:|---|') foreach ($finding in $findings) { $cell = $finding.Message.Replace('|', '\|') -replace '\r?\n', ' ' $line = if ($finding.Line -gt 0) { $finding.Line } else { '' } $lines.Add("| $($finding.Severity) | $($finding.RuleName) | " + "$(Get-RelativePath -FullPath $finding.ScriptPath) | $line | $cell |") } } $lines.Add('') $summaryText = ($lines -join "`n") + "`n" [System.IO.File]::AppendAllText($SummaryPath, $summaryText, [System.Text.UTF8Encoding]::new($false)) } if ($SarifPath) { $null = Export-IntuneFindingSarif -Finding $findings -Path $SarifPath -Root $rootPath Write-Host "SARIF written to $SarifPath" } if ($PassThru) { $findings } # Assigned from an if statement, a one-item array comes back unwrapped on Windows PowerShell 5.1 $failing = @() if ($FailOn -ne 'None') { $failing = @($findings | Where-Object { $severityRank[$_.Severity] -ge $severityRank[$FailOn] }) } if ($failing.Count) { Write-Host "::group::Gate" Write-Host "$($failing.Count) finding(s) at $FailOn or above; failing the build" Write-Host '::endgroup::' exit 1 } exit 0 |