Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ release notes.

### Added

- `Invoke-IntuneDetectionTest`, `Invoke-IntunePlatformScriptTest` and `Invoke-IntuneRequirementTest` take script
paths from the pipeline, by value or from a `FullName` or `PSPath` property, so `Get-ChildItem .\Detections |
Invoke-IntuneDetectionTest` runs each one. `Test-IntuneWin32Rule`, `Test-IntuneWin32Requirement` and
`Export-IntuneAgentDiagnostic` results have a format view, like every other result type; the module contract
test now requires one for every output type an exported command declares.
- `Repair-IntuneScript` applies eight more edits, each the one the finding's message asks for: `-Force` on
`Install-Module`, `Install-PackageProvider`, `Install-Package`, `Update-Module` and `Uninstall-Module`,
`-Confirm:$false` on `Register-PSRepository`, `-ErrorAction SilentlyContinue` on a probing cmdlet in a Win32
Expand Down
59 changes: 59 additions & 0 deletions IntuneScriptLab.Format.ps1xml
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,65 @@
</ListEntries>
</ListControl>
</View>
<View>
<Name>IntuneScriptLab.RuleResult</Name>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List views, like the other per-run results, because each carries a Reason sentence that a table would truncate; the Rule line folds Kind, Operation, Operator and Value into the shape the portal shows the rule in.

<ViewSelectedBy>
<TypeName>IntuneScriptLab.RuleResult</TypeName>
</ViewSelectedBy>
<ListControl>
<ListEntries>
<ListEntry>
<ListItems>
<ListItem><PropertyName>Met</PropertyName></ListItem>
<ListItem><Label>Rule</Label><ScriptBlock>"$($_.Kind) $($_.Operation)$(if ($_.Operator) { " $($_.Operator) '$($_.Value)'" })"</ScriptBlock></ListItem>
<ListItem><PropertyName>Target</PropertyName></ListItem>
<ListItem><PropertyName>Actual</PropertyName></ListItem>
<ListItem><PropertyName>Reason</PropertyName></ListItem>
<ListItem><PropertyName>Check32BitOn64System</PropertyName></ListItem>
</ListItems>
</ListEntry>
</ListEntries>
</ListControl>
</View>
<View>
<Name>IntuneScriptLab.ApplicabilityResult</Name>
<ViewSelectedBy>
<TypeName>IntuneScriptLab.ApplicabilityResult</TypeName>
</ViewSelectedBy>
<ListControl>
<ListEntries>
<ListEntry>
<ListItems>
<ListItem><PropertyName>Applicable</PropertyName></ListItem>
<ListItem><PropertyName>Reason</PropertyName></ListItem>
<ListItem><PropertyName>Applicability</PropertyName></ListItem>
<ListItem><PropertyName>Details</PropertyName></ListItem>
<ListItem><Label>Checks</Label><ScriptBlock>($_.Checks | ForEach-Object { "$($_.Requirement): $(if ($_.Met) { 'met' } else { 'not met' })" }) -join "; "</ScriptBlock></ListItem>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Checks array is summarised to one line per requirement so the default output stays a screen; the full objects are still on the property for anyone who wants them.

</ListItems>
</ListEntry>
</ListEntries>
</ListControl>
</View>
<View>
<Name>IntuneScriptLab.Diagnostic</Name>
<ViewSelectedBy>
<TypeName>IntuneScriptLab.Diagnostic</TypeName>
</ViewSelectedBy>
<ListControl>
<ListEntries>
<ListEntry>
<ListItems>
<ListItem><PropertyName>Path</PropertyName></ListItem>
<ListItem><Label>Size</Label><ScriptBlock>"{0:N1} MB" -f ($_.SizeBytes / 1MB)</ScriptBlock></ListItem>
<ListItem><PropertyName>Files</PropertyName></ListItem>
<ListItem><PropertyName>Logs</PropertyName></ListItem>
<ListItem><PropertyName>Registry</PropertyName></ListItem>
<ListItem><PropertyName>Timeline</PropertyName></ListItem>
</ListItems>
</ListEntry>
</ListEntries>
</ListControl>
</View>
<View>
<Name>IntuneScriptLab.RequirementResult</Name>
<ViewSelectedBy>
Expand Down
113 changes: 61 additions & 52 deletions Public/Invoke-IntuneDetectionTest.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ function Invoke-IntuneDetectionTest {
[CmdletBinding()]
[OutputType('IntuneScriptLab.DetectionResult')]
param(
[Parameter(Mandatory, Position = 0)]
[Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[Alias('FullName', 'PSPath')]
[string]$Path,

[ValidateSet('x86', 'x64', 'arm64')]
Expand All @@ -28,62 +29,70 @@ function Invoke-IntuneDetectionTest {

[switch]$EnforceSignatureCheck
)
if ($Credential -and $Context -eq 'System') {
throw '-Credential applies to -Context User; System runs as NT AUTHORITY\SYSTEM'
}
Write-Verbose "Starting $($MyInvocation.MyCommand.Name) for $($PSBoundParameters.Keys -join ', ')"

$reasons = [System.Collections.Generic.List[string]]::new()
$signatureStatus = ''
$run = $null
if ($EnforceSignatureCheck) {
# With the check on, AgentExecutor returns exit 1 for an unsigned script without running it:
# no probe record, "EnforceSignatureCheck: 1 ... applicationDetected: False" (W32-DET-SIGCHECK)
$signature = Get-AuthenticodeSignature -FilePath (Resolve-Path -LiteralPath $Path).ProviderPath
$signatureStatus = "$($signature.Status)"
if ($signature.Status -ne 'Valid') {
$reasons.Add("Signature status ${signatureStatus}: with the signature check enforced the agent " +
'does not run the script and reports not detected (exit 1 from AgentExecutor)')
process {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A function body outside begin/process/end runs once, as end, with -Path bound to the last piped object only. The process block is what makes a piped list run each script; the body inside it is unchanged apart from indentation and three wrapped lines.

if ($Credential -and $Context -eq 'System') {
throw '-Credential applies to -Context User; System runs as NT AUTHORITY\SYSTEM'
}
}
Write-Verbose "Starting $($MyInvocation.MyCommand.Name) for $($PSBoundParameters.Keys -join ', ')"

if ($reasons.Count -eq 0) {
$scriptRunSplat = @{
Path = $Path
Architecture = $Architecture
Context = $Context
Phase = 'detect'
TimeoutSeconds = $TimeoutSeconds
$reasons = [System.Collections.Generic.List[string]]::new()
$signatureStatus = ''
$run = $null
if ($EnforceSignatureCheck) {
# With the check on, AgentExecutor returns exit 1 for an unsigned script without running it:
# no probe record, "EnforceSignatureCheck: 1 ... applicationDetected: False" (W32-DET-SIGCHECK)
$signature = Get-AuthenticodeSignature -FilePath (Resolve-Path -LiteralPath $Path).ProviderPath
$signatureStatus = "$($signature.Status)"
if ($signature.Status -ne 'Valid') {
$reasons.Add("Signature status ${signatureStatus}: with the signature check enforced the agent " +
'does not run the script and reports not detected (exit 1 from AgentExecutor)')
}
}
if ($Credential) { $scriptRunSplat.Credential = $Credential }
$run = Invoke-IslScriptRun @scriptRunSplat
$hasStdOut = -not [string]::IsNullOrWhiteSpace($run.StdOut)
$hasStdErr = -not [string]::IsNullOrWhiteSpace($run.StdErr)

if ($run.TimedOut) { $reasons.Add("Timed out after $TimeoutSeconds s; Intune kills the script at its " +
"60-minute timeout and reports not detected") }
elseif ($run.ExitCode -ne 0) { $reasons.Add("Exit code $($run.ExitCode): only exit 0 can mean installed") }
if (-not $hasStdOut) { $reasons.Add('Nothing on stdout: exit 0 alone is "not detected"') }
if ($hasStdErr) { $reasons.Add('Output on stderr: any error output means "not detected" even with exit ' +
'0 and stdout') }
}
if ($reasons.Count -eq 0) {
$scriptRunSplat = @{
Path = $Path
Architecture = $Architecture
Context = $Context
Phase = 'detect'
TimeoutSeconds = $TimeoutSeconds
}
if ($Credential) { $scriptRunSplat.Credential = $Credential }
$run = Invoke-IslScriptRun @scriptRunSplat
$hasStdOut = -not [string]::IsNullOrWhiteSpace($run.StdOut)
$hasStdErr = -not [string]::IsNullOrWhiteSpace($run.StdErr)

Write-Verbose "Completed $($MyInvocation.MyCommand.Name)"
[pscustomobject]@{
PSTypeName = 'IntuneScriptLab.DetectionResult'
Detected = ($reasons.Count -eq 0)
Reason = if ($reasons.Count -eq 0) { ('Exit 0 with stdout and no ' +
'stderr') } else { $reasons -join '; ' }
ExitCode = if ($run) { $run.ExitCode } else { 1 }
StdOut = if ($run) { $run.StdOut } else { '' }
StdErr = if ($run) { $run.StdErr } else { '' }
TimedOut = if ($run) { $run.TimedOut } else { $false }
Duration = if ($run) { $run.Duration } else { [timespan]::Zero }
SignatureStatus = $signatureStatus
Architecture = $Architecture
Context = $Context
RunAs = if ($run) { $run.RunAs } else { '' }
Host = if ($run) { $run.Host } else { '' }
ScriptPath = if ($run) { $run.ScriptPath } else { (Resolve-Path -LiteralPath $Path).ProviderPath }
if ($run.TimedOut) {
$reasons.Add("Timed out after $TimeoutSeconds s; Intune kills the script at its " +
'60-minute timeout and reports not detected')
}
elseif ($run.ExitCode -ne 0) {
$reasons.Add("Exit code $($run.ExitCode): only exit 0 can mean installed")
}
if (-not $hasStdOut) { $reasons.Add('Nothing on stdout: exit 0 alone is "not detected"') }
if ($hasStdErr) {
$reasons.Add('Output on stderr: any error output means "not detected" even with exit 0 and stdout')
}
}

Write-Verbose "Completed $($MyInvocation.MyCommand.Name)"
[pscustomobject]@{
PSTypeName = 'IntuneScriptLab.DetectionResult'
Detected = ($reasons.Count -eq 0)
Reason = if ($reasons.Count -eq 0) { ('Exit 0 with stdout and no ' +
'stderr') } else { $reasons -join '; ' }
ExitCode = if ($run) { $run.ExitCode } else { 1 }
StdOut = if ($run) { $run.StdOut } else { '' }
StdErr = if ($run) { $run.StdErr } else { '' }
TimedOut = if ($run) { $run.TimedOut } else { $false }
Duration = if ($run) { $run.Duration } else { [timespan]::Zero }
SignatureStatus = $signatureStatus
Architecture = $Architecture
Context = $Context
RunAs = if ($run) { $run.RunAs } else { '' }
Host = if ($run) { $run.Host } else { '' }
ScriptPath = if ($run) { $run.ScriptPath } else { (Resolve-Path -LiteralPath $Path).ProviderPath }
}
}
}
88 changes: 46 additions & 42 deletions Public/Invoke-IntunePlatformScriptTest.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ function Invoke-IntunePlatformScriptTest {
[CmdletBinding()]
[OutputType('IntuneScriptLab.PlatformScriptResult')]
param(
[Parameter(Mandatory, Position = 0)]
[Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[Alias('FullName', 'PSPath')]
[string]$Path,

[ValidateSet('x86', 'x64', 'arm64')]
Expand All @@ -26,50 +27,53 @@ function Invoke-IntunePlatformScriptTest {
[ValidateRange(1, 86400)]
[int]$TimeoutSeconds = 300
)
Write-Verbose "Starting $($MyInvocation.MyCommand.Name) for $($PSBoundParameters.Keys -join ', ')"

$scriptRunSplat = @{
Path = $Path
Architecture = $Architecture
Context = $Context
Phase = 'script'
TimeoutSeconds = $TimeoutSeconds
}
if ($Credential) {
if ($Context -eq 'System') {
throw '-Credential applies to -Context User; System runs as NT AUTHORITY\SYSTEM'
process {
Write-Verbose "Starting $($MyInvocation.MyCommand.Name) for $($PSBoundParameters.Keys -join ', ')"

$scriptRunSplat = @{
Path = $Path
Architecture = $Architecture
Context = $Context
Phase = 'script'
TimeoutSeconds = $TimeoutSeconds
}
$scriptRunSplat.Credential = $Credential
}
$run = Invoke-IslScriptRun @scriptRunSplat
$runState = if ($run.TimedOut) { 'TimedOut' } elseif ($run.ExitCode -eq 0) { 'Success' } else { 'Failed' }
if ($Credential) {
if ($Context -eq 'System') {
throw '-Credential applies to -Context User; System runs as NT AUTHORITY\SYSTEM'
}
$scriptRunSplat.Credential = $Credential
}
$run = Invoke-IslScriptRun @scriptRunSplat
$runState = if ($run.TimedOut) { 'TimedOut' } elseif ($run.ExitCode -eq 0) { 'Success' } else { 'Failed' }

# What happens next under Intune, from the PS-FAIL experiments: a failed script is fetched and
# run again at the agent's next script policy fetch, which happens at a service start or
# restart and otherwise every 8 hours (the hourly Win32 check-ins fetch no script policy),
# three runs in all, then never again
$warnings = [System.Collections.Generic.List[string]]::new()
if ($runState -ne 'Success') {
$warnings.Add(('Intune runs a failed platform script again at its next script policy fetch (an agent ' +
'start or restart, otherwise every 8 hours), three runs in total (initial plus two retries), ' +
'then reports Failed for good'))
}
# What happens next under Intune, from the PS-FAIL experiments: a failed script is fetched and
# run again at the agent's next script policy fetch, which happens at a service start or
# restart and otherwise every 8 hours (the hourly Win32 check-ins fetch no script policy),
# three runs in all, then never again
$warnings = [System.Collections.Generic.List[string]]::new()
if ($runState -ne 'Success') {
$warnings.Add(('Intune runs a failed platform script again at its next script policy fetch (an ' +
'agent start or restart, otherwise every 8 hours), three runs in total (initial plus two ' +
'retries), then reports Failed for good'))
}

Write-Verbose "Completed $($MyInvocation.MyCommand.Name)"
[pscustomobject]@{
PSTypeName = 'IntuneScriptLab.PlatformScriptResult'
RunState = $runState
ExitCode = $run.ExitCode
ResultMessage = ($run.StdOut + $run.StdErr).TrimEnd("`r", "`n")
StdOut = $run.StdOut
StdErr = $run.StdErr
TimedOut = $run.TimedOut
Duration = $run.Duration
Warnings = $warnings.ToArray()
Architecture = $Architecture
Context = $Context
RunAs = $run.RunAs
Host = $run.Host
ScriptPath = $run.ScriptPath
Write-Verbose "Completed $($MyInvocation.MyCommand.Name)"
[pscustomobject]@{
PSTypeName = 'IntuneScriptLab.PlatformScriptResult'
RunState = $runState
ExitCode = $run.ExitCode
ResultMessage = ($run.StdOut + $run.StdErr).TrimEnd("`r", "`n")
StdOut = $run.StdOut
StdErr = $run.StdErr
TimedOut = $run.TimedOut
Duration = $run.Duration
Warnings = $warnings.ToArray()
Architecture = $Architecture
Context = $Context
RunAs = $run.RunAs
Host = $run.Host
ScriptPath = $run.ScriptPath
}
}
}
Loading
Loading