Private/Threat.Defender.ps1

function Get-OIDefenderCommandPath {
    [CmdletBinding()]
    param()

    if (-not (Test-OIIsWindows)) {
        throw 'Microsoft Defender scanning is available only on Windows.'
    }

    $candidates = New-Object 'System.Collections.Generic.List[object]'
    $platformRoot = if ([string]::IsNullOrWhiteSpace($env:ProgramData)) { $null } else { Join-Path $env:ProgramData 'Microsoft\Windows Defender\Platform' }

    if ($platformRoot -and (Test-Path -LiteralPath $platformRoot)) {
        foreach ($directory in (Get-ChildItem -LiteralPath $platformRoot -Directory -ErrorAction SilentlyContinue)) {
            $command = Join-Path $directory.FullName 'MpCmdRun.exe'
            if (-not (Test-Path -LiteralPath $command)) { continue }

            $parsedVersion = $null
            try { $parsedVersion = [version]$directory.Name } catch { Write-Verbose "Ignoring non-version Defender platform directory: $($directory.Name)" }
            $candidates.Add([pscustomobject]@{
                Path    = $command
                Version = $parsedVersion
                Name    = $directory.Name
            })
        }
    }

    $selected = $candidates |
        Sort-Object -Property { if ($null -ne $_.Version) { $_.Version } else { [version]'0.0' } } -Descending |
        Select-Object -First 1

    if ($null -ne $selected) {
        return $selected.Path
    }

    $fallbacks = New-Object 'System.Collections.Generic.List[string]'
    if (-not [string]::IsNullOrWhiteSpace($env:ProgramFiles)) {
        $fallbacks.Add((Join-Path $env:ProgramFiles 'Windows Defender\MpCmdRun.exe'))
    }
    if (-not [string]::IsNullOrWhiteSpace(${env:ProgramFiles(x86)})) {
        $fallbacks.Add((Join-Path ${env:ProgramFiles(x86)} 'Windows Defender\MpCmdRun.exe'))
    }

    foreach ($fallback in $fallbacks) {
        if (Test-Path -LiteralPath $fallback) {
            return $fallback
        }
    }

    throw 'MpCmdRun.exe was not found in the Defender platform or legacy installation directories.'
}

function Get-OIDefenderSignatureName {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [string]$Output
    )

    if ([string]::IsNullOrWhiteSpace($Output)) { return $null }

    $patterns = @(
        '(?im)^\s*Threat(?:\s+Name)?\s*:\s*(?<name>[^\r\n]+)',
        '(?im)^\s*Detected\s+threat\s*:\s*(?<name>[^\r\n]+)',
        '(?im)^\s*Threat\s+\d+\s*:\s*(?<name>[^\r\n]+)'
    )

    foreach ($pattern in $patterns) {
        $match = [regex]::Match($Output, $pattern)
        if ($match.Success) {
            return $match.Groups['name'].Value.Trim()
        }
    }

    return $null
}

function ConvertFrom-OIDefenderOutput {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [string]$Output,

        [Parameter(Mandatory = $true)]
        [int]$ExitCode
    )

    $normalizedOutput = if ($null -eq $Output) { [string]::Empty } else { $Output.Trim() }
    $signatureName = Get-OIDefenderSignatureName -Output $normalizedOutput
    $detectedPattern = '(?im)(threat\s+(?:was\s+)?found|found\s+(?:a\s+)?threat|detected\s+threat|Threat(?:\s+Name)?\s*:|Threat\s+\d+\s*:)'
    $cleanPattern = '(?im)(no\s+threats?\s+(?:(?:was|were)\s+)?found|found\s+no\s+threats?)'
    $hasDetectionMarker = $normalizedOutput -match $detectedPattern
    $hasCleanMarker = $normalizedOutput -match $cleanPattern

    if ($hasDetectionMarker -and $hasCleanMarker) {
        $status = 'Indeterminate'
        $message = 'MpCmdRun.exe output contained both clean and detection markers.'
    }
    elseif ($hasDetectionMarker) {
        $status = 'Detected'
        $message = $null
    }
    elseif ($ExitCode -eq 0 -and $hasCleanMarker) {
        $status = 'Clean'
        $message = $null
    }
    else {
        $status = 'Indeterminate'
        $message = "MpCmdRun.exe returned exit code $ExitCode without an unambiguous clean or detection marker."
    }

    return [pscustomobject]@{
        Status        = $status
        SignatureName = $signatureName
        Message       = $message
    }
}

function Invoke-OIDefenderFileScan {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$CommandPath,

        [Parameter(Mandatory = $true)]
        [string]$FilePath,

        [Parameter(Mandatory = $true)]
        [ValidateRange(1, 600)]
        [int]$TimeoutSeconds
    )

    if (-not (Test-Path -LiteralPath $FilePath)) {
        return [pscustomobject]@{
            Status         = 'Error'
            ProviderResult = $null
            HResult        = $null
            SignatureName  = $null
            Message        = "Temporary scan file was not found: $FilePath"
            RawOutput      = $null
            ExitCode       = $null
        }
    }

    $process = New-Object System.Diagnostics.Process
    $startInfo = New-Object System.Diagnostics.ProcessStartInfo
    $startInfo.FileName = $CommandPath
    $startInfo.Arguments = '-Scan -ScanType 3 -File "{0}" -DisableRemediation' -f $FilePath
    $startInfo.UseShellExecute = $false
    $startInfo.CreateNoWindow = $true
    $startInfo.RedirectStandardOutput = $true
    $startInfo.RedirectStandardError = $true
    $process.StartInfo = $startInfo

    try {
        if (-not $process.Start()) {
            throw 'The Defender process did not start.'
        }

        $stdoutTask = $process.StandardOutput.ReadToEndAsync()
        $stderrTask = $process.StandardError.ReadToEndAsync()
        $completed = $process.WaitForExit($TimeoutSeconds * 1000)

        if (-not $completed) {
            try {
                $process.Kill()
                $null = $process.WaitForExit(5000)
            }
            catch {
                Write-Verbose "Unable to terminate timed-out Defender process cleanly: $($_.Exception.Message)"
            }
            return [pscustomobject]@{
                Status         = 'Timeout'
                ProviderResult = $null
                HResult        = $null
                SignatureName  = $null
                Message        = "Microsoft Defender exceeded the $TimeoutSeconds-second scan timeout."
                RawOutput      = $null
                ExitCode       = $null
            }
        }

        $process.WaitForExit()
        $stdout = $stdoutTask.Result
        $stderr = $stderrTask.Result
        $combined = (($stdout, $stderr) -join [Environment]::NewLine).Trim()
        $exitCode = $process.ExitCode
        $classification = ConvertFrom-OIDefenderOutput -Output $combined -ExitCode $exitCode

        return [pscustomobject]@{
            Status         = $classification.Status
            ProviderResult = $exitCode
            HResult        = $null
            SignatureName  = $classification.SignatureName
            Message        = $classification.Message
            RawOutput      = $combined
            ExitCode       = $exitCode
        }
    }
    catch {
        return [pscustomobject]@{
            Status         = 'Error'
            ProviderResult = $null
            HResult        = $null
            SignatureName  = $null
            Message        = $_.Exception.Message
            RawOutput      = $null
            ExitCode       = $null
        }
    }
    finally {
        $process.Dispose()
    }
}

function Get-OIDefenderProviderMetadata {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$CommandPath
    )

    $metadata = [ordered]@{
        Provider          = 'Microsoft Defender'
        CommandPath       = $CommandPath
        OutputParser      = 'Conservative English marker parser'
        LocalizationNote  = 'Unrecognized or localized output is returned as Indeterminate rather than Clean.'
    }

    try {
        $metadata.CommandVersion = (Get-Item -LiteralPath $CommandPath).VersionInfo.FileVersion
    }
    catch {
        $metadata.CommandVersionWarning = $_.Exception.Message
    }

    $command = Get-Command Get-MpComputerStatus -ErrorAction SilentlyContinue
    if ($null -ne $command) {
        try {
            $status = Get-MpComputerStatus -ErrorAction Stop
            $metadata.AntivirusEnabled = $status.AntivirusEnabled
            $metadata.RealTimeProtectionEnabled = $status.RealTimeProtectionEnabled
            $metadata.AntivirusSignatureVersion = $status.AntivirusSignatureVersion
            $metadata.AntivirusSignatureLastUpdated = $status.AntivirusSignatureLastUpdated
            $metadata.AMEngineVersion = $status.AMEngineVersion
            $metadata.AMProductVersion = $status.AMProductVersion
        }
        catch {
            $metadata.MetadataWarning = $_.Exception.Message
        }
    }

    return [pscustomobject]$metadata
}