From 4b04442e4b161576152c1b423a1acbc3f5946f8d Mon Sep 17 00:00:00 2001 From: fadwen <110697945+fadwen@users.noreply.github.com> Date: Thu, 15 Oct 2026 07:27:47 -0700 Subject: [PATCH] fix(gate): leave the contents of a Win32 Package folder out of a folder search The gate analyzed every .ps1 under the script folders, including the contents of a Win32 app's Package folder, the files that become the .intunewin. An install.ps1 there runs through the install command as SYSTEM, but nothing in its name tells the analysis that, so it was read as a user-context 32-bit platform script and its machine-wide writes became warnings that failed the build at -FailOn Warning. Files under a Package segment are left out of a folder search, next to *.Tests.ps1. A Packages folder, which holds Win32 apps, stays in, and a file named directly on -Path is still analyzed. --- CHANGELOG.md | 6 ++++++ Examples/Invoke-IntuneScriptGate.ps1 | 21 +++++++++++++++------ Examples/intune-script-gate.yml | 3 ++- README.md | 5 +++-- Tests/Integration/ScriptGate.Tests.ps1 | 20 ++++++++++++++++++++ 5 files changed, 46 insertions(+), 9 deletions(-) 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',