Public/Test-IntuneScript.ps1
|
function Test-IntuneScript { <# .EXTERNALHELP IntuneScriptLab-Help.xml .SYNOPSIS Checks a PowerShell script for the mistakes Intune turns into silent failures. #> [CmdletBinding()] [OutputType('IntuneScriptLab.Finding')] param( [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('FullName', 'PSPath')] [SupportsWildcards()] [string[]]$Path, [ValidateSet('Auto', 'Detection', 'Remediation', 'PlatformScript', 'Win32Detection', 'Win32Requirement')] [string]$ScriptType = 'Auto', [ValidateSet('Auto', 'System', 'User')] [string]$Context = 'Auto', [ValidateSet('Auto', 'x86', 'x64', 'arm64')] [string]$Architecture = 'Auto', [string[]]$IncludeRule, [string[]]$ExcludeRule, [ValidateSet('Information', 'Warning', 'Error')] [string]$MinimumSeverity = 'Information', [switch]$EnforceSignatureCheck, # A settings file path or hashtable; omitted, the nearest IntuneScriptLab.settings.psd1 # above each script applies, and @{} means none $Settings, [switch]$IncludeSuppressed ) begin { $severityRank = @{ Information = 0; Warning = 1; Error = 2 } $settingsCache = @{} function Test-RuleSelected { param([string]$RuleName, [string[]]$Include, [string[]]$Exclude) if ($Include -and -not ($Include | Where-Object { $RuleName -like $_ })) { return $false } if ($Exclude -and ($Exclude | Where-Object { $RuleName -like $_ })) { return $false } $true } } process { $files = foreach ($item in $Path) { foreach ($resolved in (Resolve-Path -Path $item -ErrorAction Stop)) { if (Test-Path -LiteralPath $resolved.ProviderPath -PathType Container) { Get-ChildItem -LiteralPath $resolved.ProviderPath -Recurse -Filter *.ps1 -File | ForEach-Object FullName } else { $resolved.ProviderPath } } } foreach ($file in $files) { # Parameters beat the settings file; the settings file beats inference $settingsSplat = @{ Path = $file; Cache = $settingsCache } if ($PSBoundParameters.ContainsKey('Settings')) { $settingsSplat.Settings = $Settings } $fileSettings = Get-IslSetting @settingsSplat $include = if ($PSBoundParameters.ContainsKey('IncludeRule')) { $IncludeRule } else { $fileSettings.IncludeRule } $exclude = @($ExcludeRule) + @($fileSettings.ExcludeRule) | Where-Object { $_ } $minimum = if ($PSBoundParameters.ContainsKey('MinimumSeverity')) { $MinimumSeverity } elseif ($fileSettings.MinimumSeverity) { $fileSettings.MinimumSeverity } else { $MinimumSeverity } $filters = @{ Include = $include; Exclude = $exclude } $rules = foreach ($rule in $script:RuleOrder) { if (Test-RuleSelected -RuleName ($rule -replace '^Find-', '') @filters) { $rule } } $scriptContextSplat = @{ Path = $file ScriptType = $ScriptType Context = $Context Architecture = $Architecture Settings = $fileSettings } if ($EnforceSignatureCheck) { $scriptContextSplat.EnforceSignatureCheck = 'True' } $scriptContext = Get-IslScriptContext @scriptContextSplat Write-Verbose ("$file : $($scriptContext.ScriptType) ($($scriptContext.TypeSource)), " + "$($scriptContext.Context), $($scriptContext.Architecture)") $findings = @(foreach ($rule in $rules) { & $rule -Context $scriptContext }) # Say what was assumed: the wrong type silently skips whole rule sets. The note obeys # the rule filters, so -ExcludeRule IslAssumedContext silences it $noteWanted = $scriptContext.TypeSource -notin 'parameter', 'settings' -and (Test-RuleSelected -RuleName 'IslAssumedContext' @filters) if ($noteWanted) { $findingSplat = @{ RuleName = 'IslAssumedContext' Severity = 'Information' Context = $scriptContext Message = ("Analyzed as $($scriptContext.ScriptType) ($($scriptContext.TypeSource)), " + "$($scriptContext.Context) context, $($scriptContext.Architecture): the portal " + 'defaults; a deployment through the Graph API or IaC gets 64-bit SYSTEM. Pass ' + "-ScriptType/-Context/-Architecture or add a '# IntuneScriptLab:' " + 'directive if that is wrong') Evidence = ('Portal defaults: platform scripts run as the user in 32-bit, remediations ' + 'as SYSTEM in 32-bit, Win32 detection in 64-bit; Graph stores runAs32Bit=false and ' + 'runAsAccount=system when omitted (Graph API defaults)') } $findings = @(New-IslFinding @findingSplat) + $findings } # Severity overrides from the settings file, then the suppressions in the script foreach ($finding in $findings) { if ($fileSettings.Severity.ContainsKey($finding.RuleName)) { $finding.Severity = $fileSettings.Severity[$finding.RuleName] } } $suppressions = @(Get-IslSuppression -Context $scriptContext) foreach ($finding in $findings) { foreach ($suppression in $suppressions) { if ($finding.RuleName -like $suppression.Rule -and ($suppression.Line -eq 0 -or $suppression.Line -eq $finding.Line)) { $finding.Suppressed = $true break } } } $findings | Where-Object { $severityRank[$_.Severity] -ge $severityRank[$minimum] -and ($IncludeSuppressed -or -not $_.Suppressed) } } } } |