functions/Invoke-XdrEndpointDeviceLiveResponseCommand.ps1
|
function Invoke-XdrEndpointDeviceLiveResponseCommand { <# .SYNOPSIS Sends a command to an active Live Response session in Microsoft Defender XDR. .DESCRIPTION Submits a command to an active Live Response session and polls for the result. Parses the raw command line to extract the command definition ID and parameters, then sends the command via the Live Response API and waits for completion. Supports the full Live Response command syntax including: - Positional parameters mapped by order from the command definition - Named parameters using -paramName value syntax (e.g. -output json, -name notepad.exe) - Boolean flags using -flagName syntax (e.g. -full_path, -upload, -overwrite, -keep) - Alias resolution (ls -> dir, process -> processes, download -> getfile, etc.) This cmdlet can be used programmatically or is called automatically by Connect-XdrEndpointDeviceLiveResponse during interactive sessions. .PARAMETER SessionId The Live Response session ID (starts with CLR prefix). .PARAMETER Command The raw command line to execute (e.g., "dir /Applications", "processes", "getfile /etc/hosts"). Supports all Live Response command aliases (ls, process, download, etc.). Values containing spaces must be quoted: getfile "/Applications/Utilities/Activity Monitor.app/Contents/Info.plist" .PARAMETER CurrentDirectory The current working directory on the remote device. Defaults to "C:\" for Windows sessions. For macOS and Linux sessions, use '/' or the session's reported current directory. .PARAMETER BackgroundMode Run the command in background mode if supported. .PARAMETER TimeoutSeconds Maximum time to wait for command completion. Defaults to 300 seconds (5 minutes). Automatically extended to 600s for analyze commands. .PARAMETER PollIntervalSeconds How often to check for command completion. Defaults to 2 seconds. .PARAMETER CommandDefinitions Array of command definition objects from the Live Response API's get_command_definitions endpoint. Used to resolve aliases and correctly classify -name tokens as flags or named parameters. When not provided, falls back to heuristic parsing with 'path' as the default param_id. .PARAMETER DeviceName Optional device name to stamp onto the returned command object. .PARAMETER DeviceId Optional device ID to stamp onto the returned command object. .PARAMETER ExpandTableOutput When set, emits PowerShell-native row objects for table outputs and stamps each row with Timestamp, DeviceName, DeviceId, command, and status metadata. .PARAMETER IncludeCommandResult When used with -ExpandTableOutput, also emits the original command result object before the flattened table rows. .PARAMETER RawCommandResult Returns the original command result object without default structured table expansion. Useful for callers such as the interactive Live Response shell that need the raw outputs, context, and error collections from the API response. .EXAMPLE Invoke-XdrEndpointDeviceLiveResponseCommand -SessionId "CLR0c33ce1c-1665-4e00-9059-8fa39da9e2cb" -Command "processes" Lists running processes on the remote device. .EXAMPLE Invoke-XdrEndpointDeviceLiveResponseCommand -SessionId "CLR0c33ce1c-1665-4e00-9059-8fa39da9e2cb" -Command "dir /Applications" -CurrentDirectory "/" Lists the contents of /Applications on a macOS device. .EXAMPLE Invoke-XdrEndpointDeviceLiveResponseCommand -SessionId "CLR0c33ce1c-1665-4e00-9059-8fa39da9e2cb" -Command "dir -full_path" Lists all files with full paths. The -full_path flag is correctly sent in the flags[] array. .EXAMPLE Invoke-XdrEndpointDeviceLiveResponseCommand -SessionId "CLR0c33ce1c-1665-4e00-9059-8fa39da9e2cb" -Command "process -name launchd" Filters processes by name using the 'process' alias and a named -name parameter on macOS. .EXAMPLE Invoke-XdrEndpointDeviceLiveResponseCommand -SessionId "CLR0c33ce1c-1665-4e00-9059-8fa39da9e2cb" -Command "getfile /etc/hosts" -TimeoutSeconds 120 Downloads a file from a macOS device with a 2-minute timeout. .EXAMPLE Invoke-XdrEndpointDeviceLiveResponseCommand -SessionId "CLR0c33ce1c-1665-4e00-9059-8fa39da9e2cb" -Command "ls" Lists files using the 'ls' alias for 'dir'. Alias is preserved in raw_command_line. .EXAMPLE $sessions | Invoke-XdrEndpointDeviceLiveResponseCommand -Command "processes" -ExpandTableOutput Returns one PowerShell object per process row, stamped with device and execution metadata. .EXAMPLE $sessions | Invoke-XdrEndpointDeviceLiveResponseCommand -Command "processes" -ExpandTableOutput -IncludeCommandResult Returns the original command result object followed by flattened process rows. .EXAMPLE Invoke-XdrEndpointDeviceLiveResponseCommand -SessionId "CLR0c33ce1c-1665-4e00-9059-8fa39da9e2cb" -Command "drivers" -RawCommandResult Returns the original command result object without expanding structured table rows. .NOTES macOS validation baseline: February 24, 2026. Use POSIX-style paths for macOS sessions (for example: /, /Applications, /etc/hosts, /tmp). Some Live Response commands are platform-restricted or tenant-policy restricted and can return errors such as "Not allowed to run this command". These responses should be recorded as capability limitations instead of parser failures. .OUTPUTS PSCustomObject Returns the command result object including output, status, context, and errors. With -ExpandTableOutput, returns flattened table row objects when table outputs are present. #> [OutputType([PSCustomObject])] [OutputType([PSCustomObject[]])] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'Pipeline-bound parameters are buffered for batched execution.')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Variables are consumed inside a deferred worker scriptblock for batch execution.')] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string]$SessionId, [Parameter(Mandatory = $true)] [string]$Command, [Parameter(ValueFromPipelineByPropertyName = $true)] [string]$CurrentDirectory = 'C:\', [Parameter()] [switch]$BackgroundMode, [Parameter()] [int]$TimeoutSeconds = 300, [Parameter()] [int]$PollIntervalSeconds = 2, [Parameter(ValueFromPipelineByPropertyName = $true)] [array]$CommandDefinitions , [Parameter(ValueFromPipelineByPropertyName = $true)] [string]$DeviceName , [Parameter(ValueFromPipelineByPropertyName = $true)] [string]$DeviceId , [Parameter()] [switch]$ExpandTableOutput , [Parameter()] [switch]$IncludeCommandResult , [Parameter()] [switch]$RawCommandResult ) begin { Update-XdrConnectionSettings function Get-XdrLiveResponseStatusText { param( [Parameter(Mandatory = $false)] [object]$Status ) switch ($Status) { 0 { 'Pending' } 1 { 'Completed' } 2 { 'Failed' } 3 { 'Cancelled' } 4 { 'Expired' } 5 { 'Rejected' } 6 { 'Interrupted' } 7 { 'Created' } 130 { 'Downloading' } $null { '' } default { "Status $Status" } } } function Get-XdrLiveResponseCommandDefinitionList { param( [Parameter(Mandatory = $false)] [object]$CommandDefinitions ) if ($null -eq $CommandDefinitions) { return @() } if ($CommandDefinitions -is [System.Array] -and $CommandDefinitions.Count -eq 1) { $firstEntry = $CommandDefinitions[0] if ($firstEntry -is [System.Array]) { return @($firstEntry) } } @($CommandDefinitions) } function Add-XdrLiveResponseCommandContext { param( [Parameter(Mandatory = $true)] [object]$CommandResult, [Parameter(Mandatory = $true)] [object]$Request ) $timestamp = $null foreach ($candidateTimestamp in @($CommandResult.completed_on, $CommandResult.created_on, $CommandResult.created_time, $CommandResult.started_on)) { if ([string]::IsNullOrWhiteSpace("$candidateTimestamp")) { continue } try { $timestamp = [datetime]$candidateTimestamp } catch { $timestamp = $candidateTimestamp } break } Add-Member -InputObject $CommandResult -NotePropertyName 'Timestamp' -NotePropertyValue $timestamp -Force Add-Member -InputObject $CommandResult -NotePropertyName 'DeviceName' -NotePropertyValue $Request.DeviceName -Force Add-Member -InputObject $CommandResult -NotePropertyName 'DeviceId' -NotePropertyValue $Request.DeviceId -Force $statusValue = if ($CommandResult.PSObject.Properties['status']) { $CommandResult.status } elseif ($CommandResult.PSObject.Properties['Status']) { $CommandResult.Status } else { $null } $sessionIdentifier = if ($CommandResult.PSObject.Properties['session_id']) { $CommandResult.session_id } elseif ($CommandResult.PSObject.Properties['SessionId']) { $CommandResult.SessionId } else { $Request.SessionId } $commandText = if ($CommandResult.PSObject.Properties['raw_command_line']) { $CommandResult.raw_command_line } else { $Request.Command } $durationValue = if ($CommandResult.PSObject.Properties['duration_seconds']) { $CommandResult.duration_seconds } elseif ($CommandResult.PSObject.Properties['DurationSeconds']) { $CommandResult.DurationSeconds } else { $null } Add-Member -InputObject $CommandResult -NotePropertyName 'ShortDeviceId' -NotePropertyValue $(if ([string]::IsNullOrWhiteSpace($Request.DeviceId)) { $null } elseif ($Request.DeviceId.Length -le 12) { $Request.DeviceId } else { '{0}...' -f $Request.DeviceId.Substring(0, 12) }) -Force Add-Member -InputObject $CommandResult -NotePropertyName 'StatusText' -NotePropertyValue (Get-XdrLiveResponseStatusText -Status $statusValue) -Force Add-Member -InputObject $CommandResult -NotePropertyName 'SessionId' -NotePropertyValue $sessionIdentifier -Force Add-Member -InputObject $CommandResult -NotePropertyName 'Command' -NotePropertyValue $commandText -Force Add-Member -InputObject $CommandResult -NotePropertyName 'DurationSeconds' -NotePropertyValue $durationValue -Force if ($CommandResult.PSObject.TypeNames[0] -ne 'XdrEndpointDeviceLiveResponseCommand') { $CommandResult.PSObject.TypeNames.Insert(0, 'XdrEndpointDeviceLiveResponseCommand') } $CommandResult } function Get-XdrLiveResponseCommandId { param( [Parameter(Mandatory = $true)] [object]$CommandResult ) if ($CommandResult.PSObject.Properties['command_definition_id'] -and -not [string]::IsNullOrWhiteSpace("$($CommandResult.command_definition_id)")) { return "$($CommandResult.command_definition_id)".ToLower() } $commandText = if ($CommandResult.PSObject.Properties['raw_command_line']) { "$($CommandResult.raw_command_line)" } elseif ($CommandResult.PSObject.Properties['Command']) { "$($CommandResult.Command)" } else { '' } if ([string]::IsNullOrWhiteSpace($commandText)) { return '' } @($commandText.Trim().Split(' ', [System.StringSplitOptions]::RemoveEmptyEntries) | Select-Object -First 1)[0].ToLower() } function Test-XdrLiveResponseStructuredDefaultOutput { param( [Parameter(Mandatory = $true)] [string]$CommandId ) $CommandId -in @( 'processes', 'services', 'drivers', 'connections', 'scheduledtasks', 'startupfolders', 'dir', 'persistence' ) } function Get-XdrLiveResponseRowBase { param( [Parameter(Mandatory = $true)] [object]$CommandResult, [Parameter(Mandatory = $true)] [int]$OutputIndex ) [ordered]@{ Timestamp = $CommandResult.Timestamp DeviceName = $CommandResult.DeviceName DeviceId = $CommandResult.DeviceId ShortDeviceId = $CommandResult.ShortDeviceId Command = $CommandResult.Command CommandId = (Get-XdrLiveResponseCommandId -CommandResult $CommandResult) Status = $CommandResult.status StatusText = $CommandResult.StatusText DurationSeconds = $CommandResult.DurationSeconds SessionId = $CommandResult.SessionId OutputIndex = $OutputIndex } } function ConvertTo-XdrLiveResponseRowObject { param( [Parameter(Mandatory = $true)] [hashtable]$BaseProperties, [Parameter(Mandatory = $true)] [hashtable]$RowProperties, [Parameter(Mandatory = $true)] [string]$PrimaryTypeName ) $rowData = [ordered]@{} foreach ($key in $BaseProperties.Keys) { $rowData[$key] = $BaseProperties[$key] } foreach ($key in $RowProperties.Keys) { $propertyName = if ($rowData.Contains($key)) { "Table_$key" } else { $key } $rowData[$propertyName] = $RowProperties[$key] } $rowObject = [PSCustomObject]$rowData $rowObject.PSObject.TypeNames.Insert(0, $PrimaryTypeName) if ($rowObject.PSObject.TypeNames[1] -ne 'XdrEndpointDeviceLiveResponseTableRow') { $rowObject.PSObject.TypeNames.Insert(1, 'XdrEndpointDeviceLiveResponseTableRow') } $rowObject } function ConvertTo-XdrLiveResponsePersistenceRow { param( [Parameter(Mandatory = $true)] [object]$CommandResult, [Parameter(Mandatory = $true)] [object]$OutputItem, [Parameter(Mandatory = $true)] [int]$OutputIndex ) $baseProperties = Get-XdrLiveResponseRowBase -CommandResult $CommandResult -OutputIndex $OutputIndex $flattenedRows = [System.Collections.Generic.List[object]]::new() $autoruns = $OutputItem.data.autoruns if ($null -eq $autoruns) { return @($flattenedRows) } foreach ($categoryProperty in $autoruns.PSObject.Properties) { $categoryName = $categoryProperty.Name foreach ($entry in @($categoryProperty.Value)) { switch ($categoryName) { 'startup_folders' { $flattenedRows.Add((ConvertTo-XdrLiveResponseRowObject -BaseProperties $baseProperties -PrimaryTypeName 'XdrEndpointDeviceLiveResponsePersistenceRow' -RowProperties ([ordered]@{ Category = 'StartupFolder' Name = $entry.filePath Path = $entry.filePath Target = $entry.executablePath EntryType = $entry.category IsEnabled = $null ValueName = $null ValueType = $null Value = $null CommandLine = $null Principal = $null }))) } 'registry' { $flattenedRows.Add((ConvertTo-XdrLiveResponseRowObject -BaseProperties $baseProperties -PrimaryTypeName 'XdrEndpointDeviceLiveResponsePersistenceRow' -RowProperties ([ordered]@{ Category = 'Registry' Name = $entry.display_name Path = $entry.reg_path Target = $null EntryType = $entry.value_name IsEnabled = $null ValueName = $entry.value_name ValueType = $entry.value_type Value = $entry.value CommandLine = $null Principal = $null }))) } 'schedule_tasks' { $execAction = @($entry.task.actions.exec | Select-Object -First 1)[0] $principal = $entry.task.principals.principal $flattenedRows.Add((ConvertTo-XdrLiveResponseRowObject -BaseProperties $baseProperties -PrimaryTypeName 'XdrEndpointDeviceLiveResponsePersistenceRow' -RowProperties ([ordered]@{ Category = 'ScheduledTask' Name = $entry.id Path = $entry.task.registrationInfo.uri Target = $execAction.command EntryType = 'Task' IsEnabled = $entry.is_enabled ValueName = $null ValueType = $null Value = $null CommandLine = $execAction.arguments Principal = $(if ($principal.userId) { $principal.userId } else { $principal.id }) }))) } } } } @($flattenedRows) } function ConvertTo-XdrLiveResponseTableRow { param( [Parameter(Mandatory = $true)] [object]$CommandResult ) $flattenedRows = [System.Collections.Generic.List[object]]::new() $outputIndex = 0 $commandId = Get-XdrLiveResponseCommandId -CommandResult $CommandResult foreach ($outputItem in @($CommandResult.outputs)) { if ($commandId -eq 'persistence' -and $outputItem.data_type -eq 'object' -and $null -ne $outputItem.data) { foreach ($row in @(ConvertTo-XdrLiveResponsePersistenceRow -CommandResult $CommandResult -OutputItem $outputItem -OutputIndex $outputIndex)) { $flattenedRows.Add($row) } $outputIndex++ continue } if ($outputItem.data_type -ne 'table' -or $null -eq $outputItem.data) { $outputIndex++ continue } foreach ($row in @($outputItem.data)) { $baseProperties = Get-XdrLiveResponseRowBase -CommandResult $CommandResult -OutputIndex $outputIndex switch ($commandId) { 'processes' { $rowObject = ConvertTo-XdrLiveResponseRowObject -BaseProperties $baseProperties -PrimaryTypeName 'XdrEndpointDeviceLiveResponseProcessRow' -RowProperties ([ordered]@{ Name = $row.name Pid = $row.pid ParentId = $row.parent_id UserName = $row.user_name ProcessStatus = $row.status CreatedTime = $row.creation_time CpuCyclesK = $row.'cpu_cycles (K)' MemoryKB = $row.'memory (K)' WorkingSetBytes = $row.memory_usage.working_set PrivateBytes = $row.memory_usage.private_bytes ProcessSessionId = $row.session_id }) } 'services' { $rowObject = ConvertTo-XdrLiveResponseRowObject -BaseProperties $baseProperties -PrimaryTypeName 'XdrEndpointDeviceLiveResponseServiceRow' -RowProperties ([ordered]@{ ServiceName = $row.service_name DisplayName = $row.display_name CurrentState = $row.current_state StartType = $row.start_type ServiceType = $row.service_type StartName = $row.service_start_name BinaryPath = $row.binary_path Path = $row.path Arguments = $row.args ProcessId = $row.process_id Dependencies = @($row.dependencies) -join ', ' }) } 'drivers' { $rowObject = ConvertTo-XdrLiveResponseRowObject -BaseProperties $baseProperties -PrimaryTypeName 'XdrEndpointDeviceLiveResponseDriverRow' -RowProperties ([ordered]@{ DriverName = $row.driver_name ServiceName = $row.service_name ServiceState = $row.service_state ServiceType = $row.service_type DriverLoaded = $row.driver_loaded Path = $row.path Description = $row.description }) } 'connections' { $rowObject = ConvertTo-XdrLiveResponseRowObject -BaseProperties $baseProperties -PrimaryTypeName 'XdrEndpointDeviceLiveResponseConnectionRow' -RowProperties ([ordered]@{ ProcessName = $row.name Pid = $row.pid ConnectionState = $row.status_display LocalIp = $row.local_ip LocalPort = $row.local_port LocalEndpoint = '{0}:{1}' -f $row.local_ip, $row.local_port RemoteIp = $row.remote_ip RemotePort = $row.remote_port RemoteEndpoint = '{0}:{1}' -f $row.remote_ip, $row.remote_port }) } 'scheduledtasks' { $execAction = @($row.task.actions.exec | Select-Object -First 1)[0] $principal = $row.task.principals.principal $rowObject = ConvertTo-XdrLiveResponseRowObject -BaseProperties $baseProperties -PrimaryTypeName 'XdrEndpointDeviceLiveResponseScheduledTaskRow' -RowProperties ([ordered]@{ TaskId = $row.id IsEnabled = $row.is_enabled Author = $row.task.registrationInfo.author Principal = $(if ($principal.userId) { $principal.userId } else { $principal.id }) ActionType = $(if ($row.task.actions.exec) { 'Exec' } elseif ($row.task.actions.comHandler) { 'ComHandler' } else { $null }) ActionPath = $execAction.command Arguments = $execAction.arguments Context = $row.task.actions.context }) } 'startupfolders' { $rowObject = ConvertTo-XdrLiveResponseRowObject -BaseProperties $baseProperties -PrimaryTypeName 'XdrEndpointDeviceLiveResponseStartupFolderRow' -RowProperties ([ordered]@{ FilePath = $row.filePath ExecutablePath = $row.executablePath Category = $row.category }) } 'dir' { $rowObject = ConvertTo-XdrLiveResponseRowObject -BaseProperties $baseProperties -PrimaryTypeName 'XdrEndpointDeviceLiveResponseDirectoryRow' -RowProperties ([ordered]@{ Path = $row.path ItemType = $(if ($row.isDirectory) { 'Directory' } else { 'File' }) Size = $row.size Created = $row.created Modified = $row.modified IsCompressed = $row.isCompressed Hidden = $row.hidden ReadOnly = $row.readOnly }) } default { $rowData = [ordered]@{} if ($row -is [System.Collections.IDictionary]) { foreach ($key in $row.Keys) { $rowData["$key"] = $row[$key] } } elseif ($null -ne $row -and $row.PSObject.Properties.Count -gt 0) { foreach ($property in $row.PSObject.Properties) { $rowData[$property.Name] = $property.Value } } else { $rowData['Value'] = $row } $rowObject = ConvertTo-XdrLiveResponseRowObject -BaseProperties $baseProperties -PrimaryTypeName 'XdrEndpointDeviceLiveResponseTableRow' -RowProperties $rowData } } $flattenedRows.Add($rowObject) } $outputIndex++ } switch ($commandId) { 'processes' { @($flattenedRows | Sort-Object DeviceName, @{ Expression = { if ($null -eq $_.MemoryKB) { -1 } else { [double]$_.MemoryKB } }; Descending = $true }, Name, Pid) } 'services' { @($flattenedRows | Sort-Object DeviceName, DisplayName, ServiceName) } 'drivers' { @($flattenedRows | Sort-Object DeviceName, DriverName, ServiceName) } 'connections' { @($flattenedRows | Sort-Object DeviceName, ProcessName, LocalPort, RemotePort) } 'scheduledtasks' { @($flattenedRows | Sort-Object DeviceName, TaskId) } 'startupfolders' { @($flattenedRows | Sort-Object DeviceName, FilePath) } 'dir' { @($flattenedRows | Sort-Object DeviceName, @{ Expression = { if ($_.ItemType -eq 'Directory') { 0 } else { 1 } } }, Path) } 'persistence' { @($flattenedRows | Sort-Object DeviceName, Category, Name) } default { @($flattenedRows | Sort-Object DeviceName, OutputIndex) } } } function Write-XdrLiveResponseCommandOutput { param( [Parameter(Mandatory = $true)] [object]$CommandResult, [Parameter(Mandatory = $true)] [object]$Request ) $commandResultWithContext = Add-XdrLiveResponseCommandContext -CommandResult $CommandResult -Request $Request $commandId = Get-XdrLiveResponseCommandId -CommandResult $commandResultWithContext $shouldExpand = -not $RawCommandResult.IsPresent -and ($ExpandTableOutput.IsPresent -or (Test-XdrLiveResponseStructuredDefaultOutput -CommandId $commandId)) if (-not $shouldExpand) { $commandResultWithContext return } $expandedRows = @(ConvertTo-XdrLiveResponseTableRow -CommandResult $commandResultWithContext) if ($IncludeCommandResult) { $commandResultWithContext } if ($expandedRows.Count -gt 0) { foreach ($expandedRow in $expandedRows) { $expandedRow } return } if (-not $IncludeCommandResult) { $commandResultWithContext } } $pendingRequests = [System.Collections.Generic.List[object]]::new() $invokeCommandScript = { param($Item, $SharedParameters) $sessionId = "$($Item.SessionId)" $commandLine = "$($Item.Command)" $currentDirectory = if ([string]::IsNullOrWhiteSpace("$($Item.CurrentDirectory)")) { 'C:\' } else { "$($Item.CurrentDirectory)" } $commandDefinitions = @(Get-XdrLiveResponseCommandDefinitionList -CommandDefinitions $Item.CommandDefinitions) $timeoutSeconds = [int]$Item.TimeoutSeconds $pollIntervalSeconds = [int]$Item.PollIntervalSeconds $backgroundMode = [bool]$Item.BackgroundMode $headers = $SharedParameters.HeadersData $webSession = $SharedParameters.WebSession if (-not $webSession) { $webSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new() foreach ($cookieInfo in $SharedParameters.CookieData) { $cookie = [System.Net.Cookie]::new($cookieInfo.Name, $cookieInfo.Value, $cookieInfo.Path, $cookieInfo.Domain) $webSession.Cookies.Add($cookie) } } $tokenList = [System.Collections.Generic.List[string]]::new() $tokenIsQuoted = [System.Collections.Generic.List[bool]]::new() $pos = 0 $line = $commandLine.Trim() while ($pos -lt $line.Length) { while ($pos -lt $line.Length -and $line[$pos] -eq ' ') { $pos++ } if ($pos -ge $line.Length) { break } $tokenBuf = [System.Text.StringBuilder]::new() $wasQuoted = $false while ($pos -lt $line.Length -and $line[$pos] -ne ' ') { $ch = $line[$pos] if ($ch -eq '"' -or $ch -eq "'") { $wasQuoted = $true $quoteChar = $ch $pos++ while ($pos -lt $line.Length -and $line[$pos] -ne $quoteChar) { $null = $tokenBuf.Append($line[$pos]) $pos++ } if ($pos -lt $line.Length) { $pos++ } } else { $null = $tokenBuf.Append($ch) $pos++ } } if ($tokenBuf.Length -gt 0) { $tokenList.Add($tokenBuf.ToString()) $tokenIsQuoted.Add($wasQuoted) } } if ($tokenList.Count -eq 0) { throw 'Empty command' } $rawFirstToken = $tokenList[0] $commandId = $rawFirstToken.ToLower() $cmdDef = $null if ($commandDefinitions.Count -gt 0) { $cmdDef = $commandDefinitions | Where-Object { $_.command_definition_id -eq $commandId } | Select-Object -First 1 if (-not $cmdDef) { foreach ($def in $commandDefinitions) { if ($def.aliases) { $aliasLower = @($def.aliases | ForEach-Object { "$_".ToLower() }) if ($commandId -in $aliasLower) { $cid = $def.command_definition_id $commandId = if ($cid -is [System.Collections.IEnumerable] -and $cid -isnot [string]) { "$($cid | Select-Object -First 1)" } else { "$cid" } $cmdDef = $def break } } } } } $knownFlagIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $knownParamIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) if ($cmdDef) { if ($cmdDef.flags) { foreach ($flag in $cmdDef.flags) { $flagId = if ($flag -is [string]) { $flag } elseif ($null -ne $flag.flag_id) { $flag.flag_id } elseif ($null -ne $flag.id) { $flag.id } else { $flag.name } if ($flagId) { $null = $knownFlagIds.Add($flagId) } } } if ($cmdDef.params) { foreach ($paramDef in $cmdDef.params) { if ($paramDef.param_id) { $null = $knownParamIds.Add($paramDef.param_id) } } } } $params = [System.Collections.Generic.List[hashtable]]::new() $flags = [System.Collections.Generic.List[string]]::new() $positional = [System.Collections.Generic.List[string]]::new() $namedParamSpecs = [System.Collections.Generic.List[hashtable]]::new() $i = 1 while ($i -lt $tokenList.Count) { $token = $tokenList[$i] if ($token -match '^-(.+)$') { $nameWithoutDash = $Matches[1].ToLower() $nextIdx = $i + 1 $hasNext = $nextIdx -lt $tokenList.Count $nextToken = if ($hasNext) { $tokenList[$nextIdx] } else { $null } $nextIsFlag = $nextToken -and $nextToken -match '^-' -and -not $tokenIsQuoted[$nextIdx] $isKnownFlag = $knownFlagIds.Contains($nameWithoutDash) $isKnownParam = $knownParamIds.Contains($nameWithoutDash) if ($isKnownFlag) { $flags.Add($nameWithoutDash) $i++ } elseif ($isKnownParam -and $hasNext -and -not $nextIsFlag) { $params.Add(@{ param_id = $nameWithoutDash; value = $nextToken }) $namedParamSpecs.Add(@{ param_id = $nameWithoutDash; value = $nextToken }) $i += 2 } elseif (-not $isKnownFlag -and -not $isKnownParam -and $hasNext -and -not $nextIsFlag) { $params.Add(@{ param_id = $nameWithoutDash; value = $nextToken }) $namedParamSpecs.Add(@{ param_id = $nameWithoutDash; value = $nextToken }) $i += 2 } else { $flags.Add($nameWithoutDash) $i++ } } else { $positional.Add($token) $i++ } } if ($positional.Count -gt 0) { if ($cmdDef -and $cmdDef.params) { $namedParamIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($namedParam in $namedParamSpecs) { $null = $namedParamIds.Add($namedParam.param_id) } $remainingParamDefs = @($cmdDef.params | Where-Object { $null -ne $_ -and $_.param_id -and -not $_.isHidden -and -not $namedParamIds.Contains($_.param_id) }) for ($j = 0; $j -lt [Math]::Min($positional.Count, $remainingParamDefs.Count); $j++) { $params.Add(@{ param_id = $remainingParamDefs[$j].param_id; value = $positional[$j] }) } } elseif ($positional.Count -eq 1) { $params.Add(@{ param_id = 'path'; value = $positional[0] }) } } $rawCommandLine = $commandLine.Trim() $needsRebuild = $false foreach ($paramSpec in $params) { if ($paramSpec.value -match '\s') { $doubleQuotedValue = '"' + $paramSpec.value + '"' $singleQuotedValue = "'$($paramSpec.value)'" if (-not ($rawCommandLine -match [regex]::Escape($doubleQuotedValue)) -and -not ($rawCommandLine -match [regex]::Escape($singleQuotedValue))) { $needsRebuild = $true break } } } if ($needsRebuild) { $parts = [System.Collections.Generic.List[string]]::new() $parts.Add($rawFirstToken) foreach ($positionalValue in $positional) { $quotedPositionalValue = if ($positionalValue -match '\s') { '"' + $positionalValue + '"' } else { $positionalValue } $parts.Add($quotedPositionalValue) } foreach ($namedParam in $namedParamSpecs) { $namedPart = if ($namedParam.value -match '\s') { '-{0} "{1}"' -f $namedParam.param_id, $namedParam.value } else { '-{0} {1}' -f $namedParam.param_id, $namedParam.value } $parts.Add($namedPart) } foreach ($flag in $flags) { $parts.Add("-$flag") } $rawCommandLine = $parts -join ' ' } $effectiveTimeout = switch ($commandId) { 'analyze' { [Math]::Max($timeoutSeconds, 600) } 'findfile' { [Math]::Max($timeoutSeconds, 300) } default { $timeoutSeconds } } $body = @{ session_id = $sessionId command_definition_id = $commandId params = @($params) flags = @($flags) raw_command_line = $rawCommandLine current_directory = $currentDirectory background_mode = $backgroundMode } | ConvertTo-Json -Depth 10 $createUri = "https://security.microsoft.com/apiproxy/mtp/liveResponseApi/create_command?session_id=$sessionId&useV3Api=true" $createResult = Invoke-RestMethod -Uri $createUri -Method Post -ContentType 'application/json' -Body $body -WebSession $webSession -Headers $headers $commandGuid = $createResult.command_id if (-not $commandGuid) { return $createResult } $pollUri = "https://security.microsoft.com/apiproxy/mtp/liveResponseApi/commands/${commandGuid}?session_id=$sessionId&useV2Api=false&useV3Api=true" $elapsed = 0 $commandResult = $null while ($elapsed -lt $effectiveTimeout) { Start-Sleep -Seconds $pollIntervalSeconds $elapsed += $pollIntervalSeconds $commandResult = Invoke-RestMethod -Uri $pollUri -Method Get -ContentType 'application/json' -WebSession $webSession -Headers $headers if ($commandResult.completed_on) { $commandResult.PSObject.TypeNames.Insert(0, 'XdrEndpointDeviceLiveResponseCommand') return $commandResult } } if ($commandResult) { $commandResult.PSObject.TypeNames.Insert(0, 'XdrEndpointDeviceLiveResponseCommand') return $commandResult } } } process { $pendingRequests.Add([PSCustomObject]@{ SessionId = $SessionId Command = $Command CurrentDirectory = $CurrentDirectory DeviceName = $DeviceName DeviceId = $DeviceId BackgroundMode = [bool]$BackgroundMode TimeoutSeconds = $TimeoutSeconds PollIntervalSeconds = $PollIntervalSeconds CommandDefinitions = $CommandDefinitions }) } end { if ($pendingRequests.Count -eq 0) { return } if ($pendingRequests.Count -eq 1) { try { $singleResult = & $invokeCommandScript -Item $pendingRequests[0] -SharedParameters @{ WebSession = $script:session HeadersData = $script:headers } if ($singleResult) { Write-XdrLiveResponseCommandOutput -CommandResult $singleResult -Request $pendingRequests[0] } } catch { Write-Error "Failed to execute Live Response command: $_" } return } $requestContext = Get-XdrRequestContextSnapshot $batchResults = Invoke-XdrRateLimitedBatch -Items $pendingRequests.ToArray() -OperationName 'Invoke-XdrEndpointDeviceLiveResponseCommand' -ItemScript $invokeCommandScript -SharedParameters @{ BaseUrl = $requestContext.BaseUrl CookieData = $requestContext.CookieData HeadersData = $requestContext.HeadersData } foreach ($batchResult in @($batchResults | Sort-Object @{ Expression = { if ([string]::IsNullOrWhiteSpace($_.Item.DeviceName)) { '~' } else { $_.Item.DeviceName } } }, @{ Expression = { $_.Item.SessionId } })) { if ($batchResult.Success) { if ($batchResult.Result) { Write-XdrLiveResponseCommandOutput -CommandResult $batchResult.Result -Request $batchResult.Item } } else { Write-Error "Failed to execute Live Response command for session '$($batchResult.Item.SessionId)': $($batchResult.ErrorText)" } } } } |