diff --git a/CHANGELOG.md b/CHANGELOG.md index a0caa2b..6bca91f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,12 @@ release notes. IntuneScriptLab` as an `IslModuleDependency` warning, which failed a build at `-FailOn Warning`. Folders are searched by the gate now and `*.Tests.ps1` files are left out; a suite named directly on `-Path` is still analyzed. +- The gate also analyzed the contents of a Win32 app's `Package` folder (`Win32\\Package\`, + what becomes the `.intunewin`), and an `install.ps1` there, which the agent runs through the + install command as SYSTEM, was read as a user-context platform script: every machine-wide write + became an `IslContextIssue` warning, and the build failed at `-FailOn Warning`. Files under a + `Package` folder are left out of a folder search; a `Packages` folder, which holds Win32 apps, + stays in, and a file named directly on `-Path` is still analyzed. - The gate's GitHub annotations percent-encode the `file=` and `title=` values as well as the message, so a script outside `-Root`, whose path stays absolute, no longer ends the property list at its drive letter. diff --git a/Examples/Invoke-IntuneScriptGate.ps1 b/Examples/Invoke-IntuneScriptGate.ps1 index cbf3dd7..0fbe95d 100644 --- a/Examples/Invoke-IntuneScriptGate.ps1 +++ b/Examples/Invoke-IntuneScriptGate.ps1 @@ -23,8 +23,10 @@ .PARAMETER Path Folders or scripts to analyze; folders are searched for .ps1 files recursively, leaving out - Pester suites (*.Tests.ps1), which the suite template places next to the scripts. A suite - named directly is analyzed. Default: the current directory. + Pester suites (*.Tests.ps1), which the suite template places next to the scripts, and the + contents of a Win32 app's Package folder (Win32\\Package\, what becomes the .intunewin), + which the agent runs through the install command rather than as a script. A file named + directly is analyzed. Default: the current directory. .PARAMETER FailOn The lowest severity that fails the build: Error (default), Warning, Information, or None @@ -155,14 +157,21 @@ function ConvertTo-AnnotationProperty { (ConvertTo-AnnotationText -Text $Text).Replace(':', '%3A').Replace(',', '%2C') } -# Folders are searched here rather than by Test-IntuneScript, so that a Pester suite next to the -# scripts (IntuneScripts.Tests.ps1, where the suite template puts it) is not analyzed as an Intune -# script; a suite named directly on -Path still is +# Folders are searched here rather than by Test-IntuneScript, so that two kinds of file the suite +# template puts under the script folders are not analyzed as Intune scripts: a Pester suite next to +# the scripts (IntuneScripts.Tests.ps1) and the contents of a Win32 app's Package folder +# (Win32\\Package\), which become the .intunewin and run through the install command as +# SYSTEM, where an install.ps1 would otherwise be read as a user-context platform script. A file +# named directly on -Path is still analyzed. Only the singular segment 'Package' is left out: a +# 'Packages' folder is one that holds Win32 apps, and their detection scripts stay in. $scriptFiles = @(foreach ($item in $Path) { foreach ($resolved in (Resolve-Path -Path $item)) { if (Test-Path -LiteralPath $resolved.ProviderPath -PathType Container) { Get-ChildItem -LiteralPath $resolved.ProviderPath -Recurse -Filter *.ps1 -File | - Where-Object { $_.Name -notlike '*.Tests.ps1' } | ForEach-Object { $_.FullName } + Where-Object { + $_.Name -notlike '*.Tests.ps1' -and + "$($_.DirectoryName)\" -notmatch '(?i)[\\/]Package[\\/]' + } | ForEach-Object { $_.FullName } } else { $resolved.ProviderPath } } diff --git a/Examples/intune-script-gate.yml b/Examples/intune-script-gate.yml index b4cdb66..2c63e04 100644 --- a/Examples/intune-script-gate.yml +++ b/Examples/intune-script-gate.yml @@ -27,7 +27,8 @@ env: # Folders holding the scripts: remediations, platform scripts, Win32 detection and requirement # scripts. Folder and file names tell the analysis what each script is (Remediations\\ # Detect.ps1, Win32\\Detect.ps1, PlatformScripts\*.ps1); a directive comment overrides. - # Pester suites under them (*.Tests.ps1) are left out of the analysis. + # Pester suites under them (*.Tests.ps1) and the contents of a Win32 app's Package folder + # (Win32\\Package\, what becomes the .intunewin) are left out of the analysis. SCRIPT_PATHS: ./Intune # Error fails the build; Warning is stricter; None reports only FAIL_ON: Error diff --git a/README.md b/README.md index a620613..c272610 100644 --- a/README.md +++ b/README.md @@ -660,8 +660,9 @@ as a Markdown table per kind, Broken first. repository of Intune scripts: on every pull request that touches a script it takes the module from a checked-in copy when `ISL_MODULE_PATH` names its manifest, or installs it from the Gallery, runs [Examples/Invoke-IntuneScriptGate.ps1](./Examples/Invoke-IntuneScriptGate.ps1) over the script -folders (a Pester suite under them, `*.Tests.ps1`, is left out) and fails the build at the chosen -severity. The gate script annotates the changed files +folders (a Pester suite under them, `*.Tests.ps1`, and the contents of a Win32 app's `Package` +folder are left out) and fails the build at the chosen severity. The gate script annotates the +changed files with each finding (file, line, rule, message), appends a Markdown summary to the job, and returns the findings with `-PassThru`; it works on any CI with `-Annotate:$false`. With `-SarifPath` it also writes a SARIF 2.1.0 log (`Export-IntuneFindingSarif`: a rule entry per rule with its help diff --git a/Tests/Integration/ScriptGate.Tests.ps1 b/Tests/Integration/ScriptGate.Tests.ps1 index ed46f78..32d13c1 100644 --- a/Tests/Integration/ScriptGate.Tests.ps1 +++ b/Tests/Integration/ScriptGate.Tests.ps1 @@ -24,6 +24,15 @@ BeforeAll { # A Pester suite next to the scripts, as the suite template places it: not an Intune script [System.IO.File]::WriteAllText((Join-Path $script:Scripts 'IntuneScripts.Tests.ps1'), "BeforeAll { Import-Module IntuneScriptLab }`r`nDescribe 'x' { It 'y' { 1 | Should-Be 1 } }`r`n", $utf8Bom) + # A Win32 app's package content, as the suite template lays it out: the installer runs through + # the install command as SYSTEM, so it is not an Intune script either. Read as one, its + # machine-wide write is a user-context warning. 'Packages' is a folder of Win32 apps and stays in. + $null = New-Item -ItemType Directory -Path (Join-Path $script:Scripts 'Win32\Widget\Package') -Force + $null = New-Item -ItemType Directory -Path (Join-Path $script:Scripts 'Packages\Gadget') -Force + [System.IO.File]::WriteAllText((Join-Path $script:Scripts 'Win32\Widget\Package\install.ps1'), + "New-Item -Path HKLM:\SOFTWARE\Widget -Force | Out-Null`r`nexit 0`r`n", $utf8Bom) + [System.IO.File]::WriteAllText((Join-Path $script:Scripts 'Packages\Gadget\Detect.ps1'), + "Write-Output 'installed'`r`nexit 0`r`n", $utf8Bom) # -File carries the script's exit code out as the process exit code on both hosts; a switch # cannot be negated that way, so the quiet run uses -Command (and expects exit 0 anyway) @@ -110,6 +119,17 @@ Describe 'Invoke-IntuneScriptGate' -Tag 'Integration', 'Gate' { ($direct.Output -join "`n") | Should-BeLikeString '*IslModuleDependency*' } + It 'leaves a Win32 Package folder out, keeps a Packages folder in, and analyzes a named installer' { + $run = Invoke-GateFile -Arguments @('-Path', $script:Scripts, '-Annotate', '-Root', $script:Scripts, + '-FailOn', 'None') + $run.ExitCode | Should-Be 0 + ($run.Output -join "`n") | Should-NotBeLikeString '*Package\install.ps1*' + ($run.Output -join "`n") | Should-BeLikeString '*Packages\Gadget\Detect.ps1*' + $installer = Join-Path $script:Scripts 'Win32\Widget\Package\install.ps1' + $direct = Invoke-GateFile -Arguments @('-Path', $installer, '-FailOn', 'None') + ($direct.Output -join "`n") | Should-BeLikeString '*IslContextIssue*' + } + It 'percent-encodes the colon of an absolute path outside -Root in the file property' { # With -Root below the scripts, every path stays absolute and carries a drive letter $run = Invoke-GateFile -Arguments @('-Path', $script:Scripts, '-Annotate', '-FailOn', 'None',