PSScriptAnalyzer/IntuneScriptLab.Rules.psm1
|
#Requires -Version 5.1 <# PSScriptAnalyzer custom rules that wrap IntuneScriptLab's analysis, one Measure-* function per IntuneScriptLab rule, so the checks run under Invoke-ScriptAnalyzer next to the built-in rules: Invoke-ScriptAnalyzer -Path . -Recurse -CustomRulePath (Get-IntuneAnalyzerRulePath) -IncludeDefaultRules or from a PSScriptAnalyzerSettings.psd1 with CustomRulePath set to this file's path. PSScriptAnalyzer discovers rules by parsing this file for functions with an AST parameter, so the functions are written out rather than generated; the module's contract test keeps them in step with the rule list. PSScriptAnalyzer invokes a rule once for every ScriptBlockAst in a file (the root, each function body, each script block), so every rule runs Test-IntuneScript only on the root and the result is cached per file for the rules that follow. A -ScriptDefinition has no file; its text goes to a temporary .ps1 so the script type can still come from a '# IntuneScriptLab: ScriptType=...' directive. #> Import-Module (Join-Path -Path $PSScriptRoot -ChildPath '..\IntuneScriptLab.psd1') -ErrorAction Stop $script:AnalysisCache = [hashtable]::Synchronized(@{}) function Get-IslAnalysis { <# .SYNOPSIS Runs Test-IntuneScript once per file (or script definition) and caches the findings. #> [CmdletBinding()] param( [Parameter(Mandatory)] [System.Management.Automation.Language.ScriptBlockAst]$ScriptBlockAst ) $file = $ScriptBlockAst.Extent.File if ($file) { $item = Get-Item -LiteralPath $file -ErrorAction Stop $key = "$($item.FullName)|$($item.LastWriteTimeUtc.Ticks)|$($item.Length)" if (-not $script:AnalysisCache.ContainsKey($key)) { $script:AnalysisCache[$key] = @(Test-IntuneScript -Path $item.FullName) } return $script:AnalysisCache[$key] } # No file: the text of the definition, written where the analyzer can read it as a script $text = $ScriptBlockAst.Extent.Text $sha = [System.Security.Cryptography.SHA256]::Create() try { $hash = [System.BitConverter]::ToString($sha.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($text))) } finally { $sha.Dispose() } $key = "definition|$($hash -replace '-', '')" if (-not $script:AnalysisCache.ContainsKey($key)) { $folder = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath 'IntuneScriptLab' $null = New-Item -ItemType Directory -Path $folder -Force $temp = Join-Path -Path $folder -ChildPath "definition-$([guid]::NewGuid().ToString('N')).ps1" try { [System.IO.File]::WriteAllText($temp, $text, [System.Text.UTF8Encoding]::new($true)) $script:AnalysisCache[$key] = @(Test-IntuneScript -Path $temp) } finally { Remove-Item -LiteralPath $temp -Force -ErrorAction SilentlyContinue } } $script:AnalysisCache[$key] } function Get-IslRuleRecord { <# .SYNOPSIS The PSScriptAnalyzer records for one IntuneScriptLab rule on one script block. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Rule, [Parameter(Mandatory)] [System.Management.Automation.Language.ScriptBlockAst]$ScriptBlockAst ) # Nested script blocks are part of the same file; the root carries the analysis if ($null -ne $ScriptBlockAst.Parent) { return } $file = $ScriptBlockAst.Extent.File foreach ($finding in (Get-IslAnalysis -ScriptBlockAst $ScriptBlockAst)) { if ($finding.RuleName -ne $Rule) { continue } $extent = if ($finding.Line -gt 0) { $lines = @("$($finding.Text)" -split "`r?`n") $endLine = $finding.Line + $lines.Count - 1 $endColumn = if ($lines.Count -eq 1) { $finding.Column + $lines[0].Length } else { $lines[-1].Length + 1 } $start = [System.Management.Automation.Language.ScriptPosition]::new($file, $finding.Line, $finding.Column, $lines[0]) $end = [System.Management.Automation.Language.ScriptPosition]::new($file, $endLine, $endColumn, $lines[-1]) [System.Management.Automation.Language.ScriptExtent]::new($start, $end) } else { $ScriptBlockAst.Extent } $message = if ($finding.Evidence) { "$($finding.Message) [Observed: $($finding.Evidence)]" } else { $finding.Message } [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ Message = $message Extent = $extent RuleName = "Measure-$Rule" Severity = $finding.Severity } } } function Measure-IslPowerShell7Syntax { [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst]$ScriptBlockAst ) Get-IslRuleRecord -Rule 'IslPowerShell7Syntax' -ScriptBlockAst $ScriptBlockAst } function Measure-IslEncodingIssue { [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst]$ScriptBlockAst ) Get-IslRuleRecord -Rule 'IslEncodingIssue' -ScriptBlockAst $ScriptBlockAst } function Measure-IslInteractiveCall { [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst]$ScriptBlockAst ) Get-IslRuleRecord -Rule 'IslInteractiveCall' -ScriptBlockAst $ScriptBlockAst } function Measure-IslExitCodeIssue { [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst]$ScriptBlockAst ) Get-IslRuleRecord -Rule 'IslExitCodeIssue' -ScriptBlockAst $ScriptBlockAst } function Measure-IslOutputIssue { [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst]$ScriptBlockAst ) Get-IslRuleRecord -Rule 'IslOutputIssue' -ScriptBlockAst $ScriptBlockAst } function Measure-IslContextIssue { [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst]$ScriptBlockAst ) Get-IslRuleRecord -Rule 'IslContextIssue' -ScriptBlockAst $ScriptBlockAst } function Measure-IslArchitectureIssue { [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst]$ScriptBlockAst ) Get-IslRuleRecord -Rule 'IslArchitectureIssue' -ScriptBlockAst $ScriptBlockAst } function Measure-IslArm64Assumption { [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst]$ScriptBlockAst ) Get-IslRuleRecord -Rule 'IslArm64Assumption' -ScriptBlockAst $ScriptBlockAst } function Measure-IslRebootCommand { [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst]$ScriptBlockAst ) Get-IslRuleRecord -Rule 'IslRebootCommand' -ScriptBlockAst $ScriptBlockAst } function Measure-IslRelativePath { [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst]$ScriptBlockAst ) Get-IslRuleRecord -Rule 'IslRelativePath' -ScriptBlockAst $ScriptBlockAst } function Measure-IslLongSleep { [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst]$ScriptBlockAst ) Get-IslRuleRecord -Rule 'IslLongSleep' -ScriptBlockAst $ScriptBlockAst } function Measure-IslSignatureIssue { [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst]$ScriptBlockAst ) Get-IslRuleRecord -Rule 'IslSignatureIssue' -ScriptBlockAst $ScriptBlockAst } function Measure-IslAssumedContext { [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst]$ScriptBlockAst ) Get-IslRuleRecord -Rule 'IslAssumedContext' -ScriptBlockAst $ScriptBlockAst } function Measure-IslExecutionPolicyCall { [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst]$ScriptBlockAst ) Get-IslRuleRecord -Rule 'IslExecutionPolicyCall' -ScriptBlockAst $ScriptBlockAst } function Measure-IslModuleDependency { [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst]$ScriptBlockAst ) Get-IslRuleRecord -Rule 'IslModuleDependency' -ScriptBlockAst $ScriptBlockAst } function Measure-IslScriptSize { [CmdletBinding()] [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.ScriptBlockAst]$ScriptBlockAst ) Get-IslRuleRecord -Rule 'IslScriptSize' -ScriptBlockAst $ScriptBlockAst } Export-ModuleMember -Function @( 'Measure-IslPowerShell7Syntax' 'Measure-IslEncodingIssue' 'Measure-IslInteractiveCall' 'Measure-IslExitCodeIssue' 'Measure-IslOutputIssue' 'Measure-IslContextIssue' 'Measure-IslArchitectureIssue' 'Measure-IslArm64Assumption' 'Measure-IslRebootCommand' 'Measure-IslRelativePath' 'Measure-IslLongSleep' 'Measure-IslSignatureIssue' 'Measure-IslAssumedContext' 'Measure-IslExecutionPolicyCall' 'Measure-IslModuleDependency' 'Measure-IslScriptSize' ) |