Private/Rules/Find-IslPowerShell7Syntax.ps1

function Find-IslPowerShell7Syntax {
    <#
    .SYNOPSIS
        Flags syntax, cmdlets and parameters that only exist in PowerShell 7.
 
    .DESCRIPTION
        The Intune Management Extension runs every script with Windows PowerShell 5.1
        (observed: PSVersion 5.1.26100, Desktop edition, for remediations, platform scripts and
        Win32 detection). A PowerShell 7-only construct is a parse error there, which means the
        script never runs: a detection script exits 1 and triggers the remediation, a Win32
        detection reports "not detected".
 
    .PARAMETER Context
        The IntuneScriptLab.ScriptContext from Get-IslScriptContext: AST, tokens, bytes and the
        effective ScriptType, Context and Architecture.
 
    .EXAMPLE
        Find-IslPowerShell7Syntax -Context (Get-IslScriptContext -Path .\Detect.ps1)
 
        The findings this rule produces for one script, as IntuneScriptLab.Finding objects.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [pscustomobject]$Context
    )

    $rule = 'IslPowerShell7Syntax'
    $evidence = ('Scripts run under Windows PowerShell 5.1; a parse error made the detection exit 1 and run the ' +
        'remediation (REM-PS7-SYNTAX)')
    $ast = $Context.Ast

    foreach ($parseError in $Context.ParseErrors) {
        $findingSplat = @{
            RuleName = $rule
            Severity = 'Error'
            Context  = $Context
            Extent   = $parseError.Extent
            Message  = "Parse error: $($parseError.Message)"
            Evidence = $evidence
        }
        New-IslFinding @findingSplat
    }

    $requires = $ast.ScriptRequirements
    if ($requires -and $requires.RequiredPSVersion -and $requires.RequiredPSVersion.Major -ge 6) {
        $findingSplat = @{
            RuleName = $rule
            Severity = 'Error'
            Context  = $Context
            Extent   = $ast.Extent
            Message  = ("'#Requires -Version $($requires.RequiredPSVersion)' cannot be satisfied: Intune runs " +
                'Windows PowerShell 5.1')
            Evidence = $evidence
        }
        New-IslFinding @findingSplat
    }

    $syntaxNodes = Find-IslAstNode -Ast $ast -TypeName TernaryExpressionAst, PipelineChainAst
    foreach ($node in $syntaxNodes) {
        $what = if ($node.GetType().Name -eq 'TernaryExpressionAst') { 'Ternary operator (? :)' }
        else { 'Pipeline chain operator (&& / ||)' }
        $findingSplat = @{
            RuleName = $rule
            Severity = 'Error'
            Context  = $Context
            Extent   = $node.Extent
            Message  = "$what is PowerShell 7 only; Windows PowerShell 5.1 fails to parse the whole script"
            Evidence = $evidence
        }
        New-IslFinding @findingSplat
    }

    $coalesce = Find-IslAstNode -Ast $ast -TypeName BinaryExpressionAst, AssignmentStatementAst -Where {
        param($node) "$($node.Operator)" -in 'QuestionQuestion', 'QuestionQuestionEquals'
    }
    foreach ($node in $coalesce) {
        $findingSplat = @{
            RuleName = $rule
            Severity = 'Error'
            Context  = $Context
            Extent   = $node.Extent
            Message  = ('Null-coalescing operator (?? / ??=) is PowerShell 7 only; Windows PowerShell 5.1 ' +
                'fails to parse the whole script')
            Evidence = $evidence
        }
        New-IslFinding @findingSplat
    }

    $memberTypes = 'MemberExpressionAst', 'InvokeMemberExpressionAst', 'IndexExpressionAst'
    $nullConditional = Find-IslAstNode -Ast $ast -TypeName $memberTypes -Where {
        param($node)
        $property = $node.PSObject.Properties['NullConditional']
        $property -and $property.Value
    }
    foreach ($node in $nullConditional) {
        $findingSplat = @{
            RuleName = $rule
            Severity = 'Error'
            Context  = $Context
            Extent   = $node.Extent
            Message  = ('Null-conditional member access (?. / ?[]) is PowerShell 7 only; Windows PowerShell ' +
                '5.1 fails to parse the whole script')
            Evidence = $evidence
        }
        New-IslFinding @findingSplat
    }

    foreach ($command in (Find-IslCommand -Ast $ast -Name 'ForEach-Object', '%', 'foreach')) {
        if (Test-IslCommandParameter -Command $command -ParameterName 'Parallel') {
            $findingSplat = @{
                RuleName = $rule
                Severity = 'Error'
                Context  = $Context
                Extent   = $command.Extent
                Message  = 'ForEach-Object -Parallel is PowerShell 7 only'
                Evidence = $evidence
            }
            New-IslFinding @findingSplat
        }
    }

    # Cmdlets and parameters that do not exist in 5.1: a runtime error, not a parse error
    $coreOnlyCommands = 'Get-Error', 'Join-String', 'Test-Json', 'ConvertFrom-Markdown', 'Get-Uptime',
    'Remove-Alias', 'Get-ExperimentalFeature', 'Get-MarkdownOption', 'Show-Markdown', 'Switch-Process',
    'ConvertTo-CliXml', 'ConvertFrom-CliXml'
    foreach ($command in (Find-IslCommand -Ast $ast -Name $coreOnlyCommands)) {
        $findingSplat = @{
            RuleName = $rule
            Severity = 'Error'
            Context  = $Context
            Extent   = $command.Extent
            Message  = "$($command.GetCommandName()) does not exist in Windows PowerShell 5.1"
            Evidence = $evidence
        }
        New-IslFinding @findingSplat
    }

    $coreOnlyParameters = @{
        'ConvertFrom-Json'   = 'AsHashtable', 'Depth', 'NoEnumerate', 'DateKind'
        'ConvertTo-Json'     = 'AsArray', 'EnumsAsStrings', 'EscapeHandling'
        'Split-Path'         = 'LeafBase', 'Extension'
        'Get-ChildItem'      = 'FollowSymlink'
        'Invoke-WebRequest'  = 'SkipCertificateCheck', 'SkipHttpErrorCheck', 'Form', 'Resume',
            'StatusCodeVariable', 'Authentication', 'Token', 'AllowInsecureRedirect', 'RetryIntervalSec'
        'Invoke-RestMethod'  = 'SkipCertificateCheck', 'SkipHttpErrorCheck', 'Form', 'Resume',
            'StatusCodeVariable', 'Authentication', 'Token', 'AllowInsecureRedirect', 'ResponseHeadersVariable'
        'Select-String'      = 'Raw', 'Culture', 'NoEmphasis'
        'Test-Connection'    = 'TargetName', 'TcpPort', 'Ping', 'Traceroute', 'IPv4', 'IPv6', 'Repeat'
        'Get-Content'        = 'AsByteStream'
        'Set-Content'        = 'AsByteStream'
        'Add-Content'        = 'AsByteStream'
        'Out-File'           = 'Encoding utf8NoBOM'
        'Start-Process'      = 'Environment'
        'Get-Process'        = 'CommandLine'
        'Compress-Archive'   = 'PassThru'
        'Import-Module'      = 'UseWindowsPowerShell', 'SkipEditionCheck'
        'New-TemporaryFile'  = 'Extension'
        'Rename-Item'        = ''
    }
    foreach ($commandName in $coreOnlyParameters.Keys) {
        foreach ($command in (Find-IslCommand -Ast $ast -Name $commandName)) {
            foreach ($parameter in ($coreOnlyParameters[$commandName] |
                Where-Object { $_ -and $_ -notmatch ' ' })) {
                if (Test-IslCommandParameter -Command $command -ParameterName $parameter) {
                    $findingSplat = @{
                        RuleName = $rule
                        Severity = 'Error'
                        Context  = $Context
                        Extent   = $command.Extent
                        Message  = "$commandName -$parameter does not exist in Windows PowerShell 5.1"
                        Evidence = $evidence
                    }
                    New-IslFinding @findingSplat
                }
            }
        }
    }
}