Public/Invoke-IntuneDetectionTest.ps1

function Invoke-IntuneDetectionTest {
    <#
    .EXTERNALHELP IntuneScriptLab-Help.xml
    .SYNOPSIS
        Runs a Win32 custom detection script like Intune and returns the verdict.
    #>

    [CmdletBinding()]
    [OutputType('IntuneScriptLab.DetectionResult')]
    param(
        [Parameter(Mandatory, Position = 0)]
        [string]$Path,

        [ValidateSet('x86', 'x64', 'arm64')]
        [string]$Architecture = 'x64',

        [ValidateSet('User', 'System')]
        [string]$Context = 'User',

        # Run as this account instead of the current user (with -Context User): a scheduled task in
        # the account's own session, the way the agent runs user-context scripts as the signed-in
        # user; needs an elevated session
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        [ValidateRange(1, 86400)]
        [int]$TimeoutSeconds = 300,

        [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)')
        }
    }

    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)

        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 }
    }
}