Private/Get-IslScriptContext.ps1

function Get-IslScriptContext {
    <#
    .SYNOPSIS
        Parses a script and works out how Intune would run it.
 
    .DESCRIPTION
        Produces the object every rule receives: the AST and tokens, the raw bytes (for the
        encoding rule), and the effective ScriptType, Context and Architecture. Explicit
        parameters win, then a directive comment inside the script, then the settings file
        (Get-IslSetting), then inference from the file name, then the defaults Intune itself
        applies.
 
        Directive comment, anywhere in the script:
            # IntuneScriptLab: ScriptType=Detection Context=User Architecture=x86
            # IntuneScriptLab: ScriptType=Win32Detection EnforceSignatureCheck=true
    #>

    [CmdletBinding()]
    [OutputType('IntuneScriptLab.ScriptContext')]
    param(
        [Parameter(Mandatory)]
        [string]$Path,

        [ValidateSet('Auto', 'Detection', 'Remediation', 'PlatformScript', 'Win32Detection', 'Win32Requirement')]
        [string]$ScriptType = 'Auto',

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

        # The host the script runs in: x86 (SysWOW64), x64 (System32 on an x64 device) or arm64
        # (System32 on an ARM64 device, native ARM64 PowerShell; there is no x64 host there)
        [ValidateSet('Auto', 'x86', 'x64', 'arm64')]
        [string]$Architecture = 'Auto',

        # The Win32 rule's "Enforce script signature check"; Auto reads the directive, else off
        [ValidateSet('Auto', 'True', 'False')]
        [string]$EnforceSignatureCheck = 'Auto',

        # The IntuneScriptLab.Settings object for the script; its type, context, architecture and
        # signature entries apply when neither a parameter nor a directive sets them
        [pscustomobject]$Settings
    )

    $resolved = (Resolve-Path -LiteralPath $Path).ProviderPath
    $bytes = [System.IO.File]::ReadAllBytes($resolved)
    $tokens = $null
    $errors = $null
    $ast = [System.Management.Automation.Language.Parser]::ParseFile($resolved, [ref]$tokens, [ref]$errors)

    # Directive comment overrides inference but not explicit parameters
    $directive = @{}
    foreach ($token in $tokens) {
        if ($token.Kind -ne 'Comment') { continue }
        if ($token.Text -match '^#\s*IntuneScriptLab\s*:\s*(.+)$') {
            foreach ($pair in ($Matches[1] -split '[\s;,]+')) {
                if ($pair -match ('^(ScriptType|Context|' +
                    'Architecture|EnforceSignatureCheck)=(\w+)$')) { $directive[$Matches[1]] = $Matches[2] }
            }
        }
    }

    # Words are matched at a boundary so 'prefix-check.ps1' is not a fix and 'undetectable' is
    # not a detection. The nearest two folder names settle the ambiguous case: a detection script
    # under a Win32, apps or packages folder is a Win32 detection rule and one under a
    # remediations folder is a remediation detection. Without a folder hint, a detection named
    # after an app, package or installer is taken as Win32.
    $name = [System.IO.Path]::GetFileNameWithoutExtension($resolved)
    $folderHint = ''
    $folder = Split-Path -Path $resolved -Parent
    for ($depth = 0; $depth -lt 2 -and $folder -and -not $folderHint; $depth++) {
        $leaf = Split-Path -Path $folder -Leaf
        if ($leaf -match '(?i)(^|[^a-z])(win32|apps?|packages?|installers?|intunewin)([^a-z]|$)') {
            $folderHint = 'Win32'
        }
        elseif ($leaf -match '(?i)(^|[^a-z])(remediations?|healthscripts?|proactive)([^a-z]|$)') {
            $folderHint = 'Remediation'
        }
        $folder = Split-Path -Path $folder -Parent
    }
    $win32Words = '(?i)(^|[^a-z])(app|apps|win32|package|software|install|installed|msi|exe)([^a-z]|$)'
    $inferredSource = 'inferred from file name'
    $inferredType = switch -Regex ($name) {
        '(?i)(^|[^a-z])requirement' { 'Win32Requirement'; break }
        '(?i)(^|[^a-z])detect' {
            if ($folderHint) { $inferredSource = 'inferred from folder and file name' }
            if ($folderHint -eq 'Win32') { 'Win32Detection' }
            elseif ($folderHint -eq 'Remediation') { 'Detection' }
            elseif ($name -match $win32Words) { 'Win32Detection' }
            else { 'Detection' }
            break
        }
        '(?i)(^|[^a-z])(remediat\w*|fix)([^a-z]|$)' { 'Remediation'; break }
        default { 'PlatformScript' }
    }
    $typeSource = 'parameter'
    if ($ScriptType -eq 'Auto') {
        if ($directive.ScriptType) { $ScriptType = $directive.ScriptType; $typeSource = 'directive' }
        elseif ($Settings.ScriptType) { $ScriptType = $Settings.ScriptType; $typeSource = 'settings' }
        else { $ScriptType = $inferredType; $typeSource = $inferredSource }
    }

    # Intune's own defaults when nothing says otherwise. Portal defaults differ from API defaults
    # (Findings.md, "Graph API defaults"); these are the portal ones because that is what most
    # scripts get, and they are the more restrictive assumption for the rules.
    if ($Context -eq 'Auto') {
        $Context = if ($directive.Context) { $directive.Context }
        elseif ($Settings.Context) { $Settings.Context }
        elseif ($ScriptType -eq 'PlatformScript') { 'User' }   # portal default for platform scripts
        else { 'System' }
    }
    if ($Architecture -eq 'Auto') {
        $Architecture = if ($directive.Architecture) { $directive.Architecture }
        elseif ($Settings.Architecture) { $Settings.Architecture }
        elseif ($ScriptType -in 'Win32Detection', 'Win32Requirement') { 'x64' }  # 64-bit by default
        else { 'x86' }                                                          # scripts/remediations: 32-bit
    }

    $signatureCheck = if ($EnforceSignatureCheck -ne 'Auto') { $EnforceSignatureCheck -eq 'True' }
    elseif ($directive.ContainsKey('EnforceSignatureCheck')) {
        "$($directive.EnforceSignatureCheck)" -in 'true', '1', 'yes'
    }
    elseif ($null -ne $Settings.EnforceSignatureCheck) { [bool]$Settings.EnforceSignatureCheck }
    else { $false }

    [pscustomobject]@{
        PSTypeName            = 'IntuneScriptLab.ScriptContext'
        Path                  = $resolved
        Ast                   = $ast
        Tokens                = $tokens
        ParseErrors           = @($errors)
        Bytes                 = $bytes
        ScriptType            = $ScriptType
        TypeSource            = $typeSource
        Context               = $Context
        Architecture          = $Architecture
        EnforceSignatureCheck = $signatureCheck
    }
}