MDEAutomator.psm1

<#
.SYNOPSIS
    MDEAutomator PowerShell Module - Automates Microsoft Defender for Endpoint (MDE) API operations.
 
.DESCRIPTION
    This module provides a set of functions to automate common Microsoft Defender for Endpoint (MDE) tasks via the MDE API, including device management, live response actions, threat indicator management, and more. It supports robust error handling, retry logic, and integration with Azure Key Vault for secure secret management.
 
.FUNCTIONS
    Connect-MDE
        Authenticates to MDE using a Service Principal, optionally retrieving secrets from Azure Key Vault.
 
    Get-Machines
        Retrieves a list of onboarded and active devices from MDE, with optional filtering.
 
    Get-Actions
        Retrieves recent machine actions performed in MDE.
 
    Undo-Actions
        Cancels all pending machine actions in MDE.
 
    Invoke-MachineIsolation / Undo-MachineIsolation
        Isolates or unisolates specified devices in MDE.
 
    Invoke-ContainDevice / Undo-ContainDevice
        Contains or uncontains specified unmanaged devices in MDE.
 
    Invoke-RestrictAppExecution / Undo-RestrictAppExecution
        Restricts or unrestricts application execution on specified devices.
         
    Invoke-CollectInvestigationPackage
        Collects an investigation package from specified devices.
 
    Invoke-TiFile / Undo-TiFile
        Creates or deletes file hash-based custom threat indicators.
 
    Invoke-TiCert / Undo-TiCert
        Creates or deletes certificate thumbprint-based custom threat indicators.
 
    Invoke-TiIP / Undo-TiIP
        Creates or deletes IP address-based custom threat indicators.
 
    Invoke-TiURL / Undo-TiURL
        Creates or deletes URL/domain-based custom threat indicators.
 
    Invoke-UploadLR
        Uploads a script file to the MDE Live Response library.
 
    Invoke-PutFile
        Pushes a file from the Live Response library to specified devices.
 
    Invoke-GetFile
        Retrieves a file from specified devices using Live Response.
 
    Invoke-LRScript
        Executes a Live Response script on specified devices.
 
.PARAMETERS
    Most functions require an OAuth2 access token (`$token`) obtained via Connect-MDE.
    Device-specific functions require one or more device IDs (`$DeviceIds`).
    Threat indicator functions require indicator values (e.g., `$Sha1s`, `$Sha256s`, `$IPs`, `$URLs`).
 
.NOTES
    - Requires PowerShell 5.1+ and the Az.Accounts/Az.KeyVault modules for Key Vault integration.
    - All API calls are made to the Microsoft Defender for Endpoint API (https://api.securitycenter.microsoft.com).
    - Error handling and retry logic are built-in for robust automation.
    - For more information, see the official Microsoft Defender for Endpoint API documentation.
 
.AUTHOR
    github.com/msdirtbag
 
.VERSION
    1.0.0
 
#>


Function Get-RequestParam {
    param (
        [string]$Name,
        [PSCustomObject]$Request
    )
    $value = $Request.Query.$Name
    if (-not $value) {
        $value = $Request.Body.$Name
    }
    return $value
}

function Get-SecretFromKeyVault {
    param (
        [Parameter(Mandatory = $true)]
        [string] $keyVaultName
    )

    $secretValue = (Get-AzKeyVaultSecret -VaultName $keyVaultName -Name "SPNSECRET" -WarningAction SilentlyContinue).SecretValue

    if ($null -eq $secretValue) {
        throw "[ERROR] Secret not found in Key Vault '$keyVaultName'"
    }

    return $secretValue
}

function Get-AccessToken {
    param(
        [Parameter(Mandatory = $true)]
        [string]$TenantId,
        [Parameter(Mandatory = $true)]
        [string]$SpnId,
        [Parameter(Mandatory = $true)]
        [string]$SpnSecret
    )

    $resourceAppIdUri = 'https://api.securitycenter.microsoft.com'
    $oAuthUri = "https://login.microsoftonline.com/$TenantId/oauth2/token"
    $body = [Ordered]@{
        resource      = $resourceAppIdUri
        client_id     = $SpnId
        client_secret = $SpnSecret
        grant_type    = 'client_credentials'
    }

    try {
        $response = Invoke-RestMethod -Method Post -Uri $oAuthUri -Body $body -ErrorAction Stop
        return $response.access_token
    } catch {
        Write-Error "Failed to acquire access token: $_"
        exit 1
    }
}

Function Connect-MDE {
    param (
        [Parameter(Mandatory=$false)]
        [string] $keyVaultName,
        [Parameter(Mandatory=$true)]
        [string] $SpnId,
        [Parameter(Mandatory=$false)]
        [securestring] $SpnSecret,
        [Parameter(Mandatory=$false)]
        [string] $TenantId
    )
    Write-Host "Connecting to MDE (this may take a few minutes)"

    if (-not $TenantId) {
        $TenantId = (Get-AzContext).Tenant.Id
    }

    if (-not $SpnSecret) {
        if ($keyVaultName) {
            if (-not (Get-Module -ListAvailable -Name Az.Accounts)) {
                Write-Host "Az.Accounts module not found. Installing for first use..."
                Install-Module -Name Az.Accounts -Scope CurrentUser -Force -AllowClobber
            }
            if (-not (Get-Module -ListAvailable -Name Az.KeyVault)) {
                Write-Host "Az.KeyVault module not found. Installing for first use..."
                Install-Module -Name Az.KeyVault -Scope CurrentUser -Force -AllowClobber
            }
            if (-not (Get-Module -Name Az.Accounts)) {
                Import-Module Az.Accounts -ErrorAction Stop
            }
            if (-not (Get-Module -Name Az.KeyVault)) {
                Import-Module Az.KeyVault -ErrorAction Stop
            }
            if (-not (Get-AzContext)) {
                Write-Host "No Azure session detected. Please sign in."
                Connect-AzAccount -TenantId $TenantId -ErrorAction Stop
            }
            $SpnSecret = (Get-AzKeyVaultSecret -VaultName $keyVaultName -Name 'SPNSECRET').SecretValue
        } else {
            Write-Error "SpnSecret must be provided if keyVaultName is not specified."
            throw "SpnSecret must be provided if keyVaultName is not specified."
        }
    }

    if (-not $SpnSecret) {
        Write-Error "Failed to retrieve SPN secret"
        throw "Failed to retrieve SPN secret"
    }

    $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SpnSecret)
    $plainSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)

    try {
        $token = Get-AccessToken -TenantId $TenantId -SpnId $SpnId -SpnSecret $plainSecret
        Write-Host "Successfully retrieved access token for MDE."
    } catch {
        Write-Host "Failed to retrieve access token for MDE. Error: $_"
        exit 1
    }
    return $token
}

function Invoke-WithRetry {
    param(
        [Parameter(Mandatory=$true)]
        [scriptblock]$ScriptBlock,
        [Parameter(Mandatory=$false)]
        [int]$MaxRetryCount = 5,
        [Parameter(Mandatory=$false)]
        [int]$InitialDelaySeconds = 20,
        [Parameter(Mandatory=$false)]
        [bool]$AllowNullResponse = $false,
        [Parameter(Mandatory=$false)]
        [object[]]$ScriptBlockArgs = @()
    )

    $retryCount = 0
    $currentDelaySeconds = $InitialDelaySeconds

    do {
        try {
            $response = & $ScriptBlock @ScriptBlockArgs
            if ($null -eq $response -and -not $AllowNullResponse) {
                Write-Error "Error: Response is null"
                throw "Response is null"
            }
            return $response
        } catch {
            $exception = $_
            $statusCode = $exception.Exception.Response?.StatusCode
            $errorMsg = $exception.Exception.Message
            $errorContent = $exception.Exception.Response?.Error
            if ($errorContent) {
                try {
                    $errorJson = $errorContent | ConvertFrom-Json
                    if ($errorJson.error.code -eq "ActiveRequestAlreadyExists") {
                        Write-Warning "Active request already exists. Skipping. Message: $($errorJson.error.message)"
                        return [PSCustomObject]@{
                            Status = "Skipped"
                            StatusCode = $statusCode
                            ErrorCode = $errorJson.error.code
                            Message = $errorJson.error.message
                        }
                    }
                } catch {
                    Write-Warning "Failed to parse error content: $errorContent"
                }
            }

            if ($statusCode -eq 429) {
                $retryAfter = $exception.Exception.Response.Headers["Retry-After"]
                $currentDelaySeconds = if ($retryAfter -and [int]::TryParse($retryAfter, [ref]$parsedRetryAfter)) {
                    $parsedRetryAfter
                } else {
                    60
                }
                Write-Warning "Rate limit exceeded. Waiting $currentDelaySeconds seconds before retrying..."
            } elseif ($statusCode -ge 400 -and $statusCode -lt 500) {
                if ($statusCode -ne 429) {
                    Write-Warning "MDE says endpoint is unavailable"
                    return [PSCustomObject]@{
                        Status = "Skipped"
                        StatusCode = $statusCode
                        Message = $errorMsg
                    }
                }
            } elseif (($statusCode -ge 500 -and $statusCode -lt 600) -or ($null -eq $statusCode)) {
                Write-Warning "Server error or null response encountered. Retrying..."
            } else {
                Write-Error "Non-HTTP exception encountered: $errorMsg"
            }

            if ($retryCount -ge $MaxRetryCount) {
                Write-Error "Max retry count ($MaxRetryCount) reached. Aborting."
                throw "Max retry attempts reached."
            }

            Start-Sleep -Seconds $currentDelaySeconds
            $retryCount++
            $currentDelaySeconds = [Math]::Min($currentDelaySeconds * 2, 600) + (Get-Random -Minimum 2 -Maximum 5)
        }
    } while ($true)
}

function Invoke-FullDiskScan {
    param (
        [Parameter(Mandatory = $true)]
        [string]$token,
        [Parameter(Mandatory = $true)]
        [string[]]$DeviceIds
    )
    $headers = @{
        "Authorization" = "Bearer $token"
    }
    $body = @{
        "Comment" = "MDEAutomator"
        "ScanType" = "Full"
    }
    $responses = @()

    foreach ($DeviceId in $DeviceIds) {
        $uri = "https://api.securitycenter.microsoft.com/api/machines/$DeviceId/runAntiVirusScan"
        try {
            $response = Invoke-WithRetry -ScriptBlock {
                Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body ($body | ConvertTo-Json) -ContentType "application/json" -ErrorAction Stop
            }

            $actionId = $response.id
            if ([string]::IsNullOrEmpty($actionId)) {
                Write-Host "No machine action ID received for DeviceId: $DeviceId. Marking as failed and continuing."
                continue
            }
            Start-Sleep -Seconds 5
            $statusSucceeded = Get-MachineActionStatus -machineActionId $actionId -token $token

            if ($statusSucceeded) {
                Write-Host "Started Scan on DeviceId: $DeviceId"
            } else {
                Write-Error "Failed to start Full Scan DeviceId: $DeviceId"
            }

            $responses += [PSCustomObject]@{
                DeviceId = $DeviceId
                Response = [PSCustomObject]@{
                    Id = $response.id
                    Type = $response.type
                    Title = $response.title
                    Requestor = $response.requestor
                    RequestorComment = $response.requestorComment
                    Status = if ($statusSucceeded) { "Succeeded" } else { "Failed" }
                    MachineId = $response.machineId
                    ComputerDnsName = $response.computerDnsName
                    CreationDateTimeUtc = $response.creationDateTimeUtc
                    LastUpdateDateTimeUtc = $response.lastUpdateDateTimeUtc
                    CancellationRequestor = $response.cancellationRequestor
                    CancellationComment = $response.cancellationComment
                    CancellationDateTimeUtc = $response.cancellationDateTimeUtc
                    ErrorHResult = $response.errorHResult
                    Scope = $response.scope
                    ExternalId = $response.externalId
                    RequestSource = $response.requestSource
                    RelatedFileInfo = $response.relatedFileInfo
                    Commands = $response.commands
                    TroubleshootInfo = $response.troubleshootInfo
                }
            }
        } catch {
            Write-Error "Failed to initiate Full Scan for DeviceId: $DeviceId $_"
            $responses += [PSCustomObject]@{
                DeviceId = $DeviceId
                Error = $_.Exception.Message
            }
        }
    }
    return $responses
}

function Invoke-UploadLR {
    param (
        [Parameter(Mandatory = $true)]
        [string]$token,

        [Parameter(Mandatory = $true)]
        [string]$filePath
    )

    try {
        $headers = @{ 
            Authorization = "Bearer $token" 
        }
        $fileName = [System.IO.Path]::GetFileName($filePath)
        $fileContent = [System.IO.File]::ReadAllBytes($filePath)
        $boundary = [System.Guid]::NewGuid().ToString() 
        $LF = "`r`n"
        $memoryStream = New-Object System.IO.MemoryStream
        $fileHeader = [System.Text.Encoding]::UTF8.GetBytes("--$boundary$LF" +
            "Content-Disposition: form-data; name=`"file`"; filename=`"$fileName`"$LF" +
            "Content-Type: application/octet-stream$LF$LF")
        $memoryStream.Write($fileHeader, 0, $fileHeader.Length)
        $memoryStream.Write($fileContent, 0, $fileContent.Length)
        $memoryStream.Write([System.Text.Encoding]::UTF8.GetBytes($LF), 0, 2)
        $parametersDescription = [System.Text.Encoding]::UTF8.GetBytes("--$boundary$LF" +
            "Content-Disposition: form-data; name=`"ParametersDescription`"$LF$LF" +
            "test$LF")
        $memoryStream.Write($parametersDescription, 0, $parametersDescription.Length)
        $hasParameters = [System.Text.Encoding]::UTF8.GetBytes("--$boundary$LF" +
            "Content-Disposition: form-data; name=`"HasParameters`"$LF$LF" +
            "false$LF")
        $memoryStream.Write($hasParameters, 0, $hasParameters.Length)
        $overrideIfExists = [System.Text.Encoding]::UTF8.GetBytes("--$boundary$LF" +
            "Content-Disposition: form-data; name=`"OverrideIfExists`"$LF$LF" +
            "true$LF")
        $memoryStream.Write($overrideIfExists, 0, $overrideIfExists.Length)
        $description = [System.Text.Encoding]::UTF8.GetBytes("--$boundary$LF" +
            "Content-Disposition: form-data; name=`"Description`"$LF$LF" +
            "test description$LF")
        $memoryStream.Write($description, 0, $description.Length)
        $finalBoundary = [System.Text.Encoding]::UTF8.GetBytes("--$boundary--$LF")
        $memoryStream.Write($finalBoundary, 0, $finalBoundary.Length)
        $memoryStream.Seek(0, [System.IO.SeekOrigin]::Begin) | Out-Null
        $bodyBytes = $memoryStream.ToArray()
        Invoke-RestMethod -Uri "https://api.security.microsoft.com/api/libraryfiles" -Method Post -Headers $headers -ContentType "multipart/form-data; boundary=$boundary" -Body $bodyBytes -ErrorAction Stop | Out-Null
        Write-Host "Successfully uploaded file: $fileName"
    } catch {
        if ($_.Exception.Message -notlike "*already exists*") {
            Write-Host "Error uploading script to library: $($_.Exception.Message)"
            exit
        }
    }
}

function Invoke-PutFile {
    param (
        [Parameter(Mandatory = $true)]
        [string]$token,
        [Parameter(Mandatory = $true)]
        [string]$fileName,
        [Parameter(Mandatory = $true)]
        [string[]]$DeviceIds
    )
    $headers = @{
        "Authorization" = "Bearer $token"
    }
    $responses = @()
    foreach ($DeviceId in $DeviceIds) {
        Write-Host "Starting PutFile on DeviceId: $DeviceId"
        $body = @{
            "Commands" = @(
                @{
                    "type" = "PutFile"
                    "params" = @(
                        @{
                            "key" = "FileName"
                            "value" = "$fileName"
                        }
                    )
                }
            )
            "Comment" = "MDEAutomator"
        } | ConvertTo-Json -Depth 10

        try {
            $response = Invoke-WithRetry -ScriptBlock {
                Invoke-RestMethod -Uri "https://api.securitycenter.microsoft.com/api/machines/$DeviceId/runliveresponse" -Method Post -Headers $headers -Body $body -ContentType "application/json" -ErrorAction Stop
            }

            $actionId = $response.id
            if ([string]::IsNullOrEmpty($actionId)) {
                Write-Host "No machine action ID received for DeviceId: $DeviceId. Marking as failed and continuing."
                continue
            }
            Start-Sleep -Seconds 5
            $statusSucceeded = Get-MachineActionStatus -machineActionId $actionId -token $token

            if ($statusSucceeded) {
                Write-Host "PutFile complete on DeviceId: $DeviceId"
            } else {
                Write-Error "PutFile failed on DeviceId: $DeviceId"
            }

            $responses += [PSCustomObject]@{
                DeviceId = $DeviceId
                Response = [PSCustomObject]@{
                    Id = $response.id
                    Type = $response.type
                    Title = $response.title
                    Requestor = $response.requestor
                    RequestorComment = $response.requestorComment
                    Status = $status
                    MachineId = $response.machineId
                    ComputerDnsName = $response.computerDnsName
                    CreationDateTimeUtc = $response.creationDateTimeUtc
                    LastUpdateDateTimeUtc = $response.lastUpdateDateTimeUtc
                    CancellationRequestor = $response.cancellationRequestor
                    CancellationComment = $response.cancellationComment
                    CancellationDateTimeUtc = $response.cancellationDateTimeUtc
                    ErrorHResult = $response.errorHResult
                    Scope = $response.scope
                    ExternalId = $response.externalId
                    RequestSource = $response.requestSource
                    RelatedFileInfo = $response.relatedFileInfo
                    Commands = $response.commands
                    TroubleshootInfo = $response.troubleshootInfo
                }
            }
        } catch {
            Write-Error "Failed to PutFile on DeviceId: $DeviceId $_"
            $responses += [PSCustomObject]@{
                DeviceId = $DeviceId
                Error = $_.Exception.Message
            }
        }
    }
    return $responses
}
function Invoke-GetFile {
    param (
        [Parameter(Mandatory = $true)]
        [string]$token,
        [Parameter(Mandatory = $true)]
        [string]$filePath,
        [Parameter(Mandatory = $true)]
        [string[]]$DeviceIds
    )
    $headers = @{
        "Authorization" = "Bearer $token"
    }
    $responses = @()
    foreach ($DeviceId in $DeviceIds) {
        Write-Host "Starting GetFile on DeviceId: $DeviceId"
        $body = @{
            "Commands" = @(
                @{
                    "type" = "GetFile"
                    "params" = @(
                        @{"key" = "Path"; "value" = "$filePath"}
                    )
                }
            )
            "Comment" = "MDEAutomator"
        } | ConvertTo-Json -Depth 10

        try {
            $response = Invoke-WithRetry -ScriptBlock {
                Invoke-RestMethod -Uri "https://api.securitycenter.microsoft.com/api/machines/$DeviceId/runliveresponse" -Method Post -Headers $headers -Body $body -ContentType "application/json" -ErrorAction Stop
            }

            $actionId = $response.id
            if ([string]::IsNullOrEmpty($actionId)) {
                Write-Host "No machine action ID received for DeviceId: $DeviceId. Marking as failed and continuing."
                $responses += [PSCustomObject]@{
                    DeviceId = $DeviceId
                    Status = "Failed"
                    Error = "No machine action ID received"
                }
                continue
            }
            Start-Sleep -Seconds 5
            $statusSucceeded = Get-MachineActionStatus -machineActionId $actionId -token $token

            if ($statusSucceeded) {
                $downloadUri = "https://api.securitycenter.microsoft.com/api/machineactions/$actionId/GetLiveResponseResultDownloadLink(index=0)"
                $responses += [PSCustomObject]@{
                    DeviceId = $DeviceId
                    Status = "Success"
                    DownloadUri = $downloadUri
                    ActionId = $actionId
                }
            } else {
                Write-Error "Action failed or timed out for DeviceId: $DeviceId"
                $responses += [PSCustomObject]@{
                    DeviceId = $DeviceId
                    Status = "Failed"
                    Error = "Action failed or timed out"
                    ActionId = $actionId
                }
            }
        } catch {
            Write-Error "Exception occurred while processing DeviceId: $DeviceId. Error: $($_.Exception.Message)"
            $responses += [PSCustomObject]@{
                DeviceId = $DeviceId
                Status = "Failed"
                Error = $_.Exception.Message
            }
        }
    }
    return $responses
}

function Invoke-CollectInvestigationPackage {
    param (
        [Parameter(Mandatory = $true)]
        [string]$token,
        [Parameter(Mandatory = $true)]
        [string[]]$DeviceIds
    )
    
    $headers = @{
        "Authorization" = "Bearer $token"
    }
    $responses = @()

    foreach ($DeviceId in $DeviceIds) {
        try {
            $body = @{
                "Comment" = "MDEAutomator"
            }

            $response = Invoke-WithRetry -ScriptBlock {
                param($uri, $headers, $body)
                Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body $body -ContentType "application/json" -ErrorAction Stop
            } -ScriptBlockArgs @("https://api.securitycenter.microsoft.com/api/machines/$DeviceId/collectInvestigationPackage", $headers, ($body | ConvertTo-Json))

            $actionId = $response.id
            if ([string]::IsNullOrEmpty($actionId)) {
                Write-Host "No machine action ID received for DeviceId: $DeviceId. Marking as failed and continuing."
                $responses += [PSCustomObject]@{
                    DeviceId = $DeviceId
                    Status = "Failed"
                    Error = "No action ID received"
                }
                continue
            }
            Start-Sleep -Seconds 5
            $statusSucceeded = Get-MachineActionStatus -machineActionId $actionId -token $token

            if ($statusSucceeded) {
                Write-Host "Package collection succeeded for DeviceId: $DeviceId"
                Start-Sleep -Seconds 5
                
                $packageUriResponse = Invoke-WithRetry -ScriptBlock {
                    param($uri, $headers)
                    Invoke-RestMethod -Uri $uri -Method Get -Headers $headers -ErrorAction Stop
                } -ScriptBlockArgs @("https://api.securitycenter.microsoft.com/api/machineactions/$actionId/getPackageUri", $headers)

                if ($packageUriResponse.value) {
                    $packageUri = $packageUriResponse.value
                    $responses += [PSCustomObject]@{
                        DeviceId = $DeviceId
                        Status = "Success"
                        PackageUri = $packageUri
                        ActionId = $actionId
                    }
                } else {
                    Write-Error "No package URI returned for DeviceId: $DeviceId"
                    $responses += [PSCustomObject]@{
                        DeviceId = $DeviceId
                        Status = "Failed"
                        Error = "No package URI returned"
                        ActionId = $actionId
                    }
                }
            } else {
                Write-Error "Package collection failed for DeviceId: $DeviceId"
                $responses += [PSCustomObject]@{
                    DeviceId = $DeviceId
                    Status = "Failed"
                    Error = "Package collection failed or timed out"
                    ActionId = $actionId
                }
            }
        } catch {
            Write-Error "Failed to process DeviceId: $DeviceId - $($_.Exception.Message)"
            $responses += [PSCustomObject]@{
                DeviceId = $DeviceId
                Status = "Failed"
                Error = $_.Exception.Message
            }
        }
    }
    return $responses
}

function Invoke-LRScript {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]] $DeviceIds,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $scriptName,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $token
    )

    $responses = @()

    foreach ($DeviceId in $DeviceIds) {
        Write-Host "Starting LRScript execution on DeviceId: $DeviceId"

        try {
            $body = @{
                Commands = @(
                    @{
                        type = "RunScript"
                        params = @(
                            @{
                                key = "ScriptName"
                                value = $scriptName
                            }
                        )
                    }
                )
                Comment = "MDEAutomator"
            } | ConvertTo-Json -Depth 10

            $response = Invoke-WithRetry -ScriptBlock {
                param($DeviceId, $token, $body)
                try {
                    Invoke-RestMethod -Uri "https://api.securitycenter.microsoft.com/api/machines/$DeviceId/runliveresponse" `
                        -Method Post `
                        -Headers @{ Authorization = "Bearer $token" } `
                        -Body $body `
                        -ContentType "application/json" `
                        -ErrorAction Stop
                }
                catch {
                    if ($_.Exception.Response.StatusCode -eq 400) {
                        Write-Host "Failed: DeviceId-$DeviceId"
                        return $null
                    }
                    throw
                }
            } -ScriptBlockArgs @($DeviceId, $token, $body) -AllowNullResponse $true

            if ($response.status -eq "Pending") {
                Write-Host "Running Live Response script on DeviceId: $($response.id)"
                Start-Sleep -Seconds 5

                $machineActionId = $response.id
                $statusSucceeded = Get-MachineActionStatus -machineActionId $machineActionId -token $token
                $responses += [PSCustomObject]@{
                    DeviceId        = $DeviceId
                    MachineActionId = $machineActionId
                    Success         = [bool]$statusSucceeded
                }
                continue
            }

            $responses += [PSCustomObject]@{
                DeviceId        = $DeviceId
                MachineActionId = $null
                Success         = $false
            }
        }
        catch {
            Write-Error "Error processing DeviceId $DeviceId : $($_.Exception.Message)"
            $responses += [PSCustomObject]@{
                DeviceId        = $DeviceId
                MachineActionId = $null
                Success         = $false
            }
        }
    }
    Write-Host "Live Response script execution completed. Total responses: $($responses.Count)"
    return $responses
}

Function Get-MachineActionStatus {
    param (
        [Parameter(Mandatory=$true)]
        [string] $machineActionId,
        [Parameter(Mandatory=$true)]
        [string] $token
    )

    $uri = "https://api.securitycenter.microsoft.com/api/machineactions/$machineActionId"
    $headers = @{
        "Authorization" = "Bearer $token"
    }

    $timeout = New-TimeSpan -Minutes 11
    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()

    while ($stopwatch.Elapsed -lt $timeout) {
        try {
            $response = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers
            $status = $response.status

            switch ($status) {
                "Succeeded" {
                    Write-Host "MDE Machine action has succeeded."
                    return $true
                }
                "Failed" {
                    Write-Host "MDE Machine action has failed."
                    return $false
                }
                "Pending" {
                    Write-Host "MDE Machine action is pending."
                    Start-Sleep -Seconds 15
                }
                "InProgress" {
                    Write-Host "MDE Machine action is pending."
                    Start-Sleep -Seconds 15
                }
                default {
                    Write-Host "Unknown status: $status"
                    Write-Host "Full response received:"
                    Write-Host ($response | Out-String)
                    return $false
                }
            }
        } catch {
            Write-Host "An error occurred: $_"
            return $false
        }
    }
    Write-Host "MDE Machine action has timed out."
    return $false
}

Function Get-LiveResponseOutput {
    param (
        [Parameter(Mandatory=$true)]
        [string] $machineActionId,
        [Parameter(Mandatory=$true)]
        [string] $token
    )

    $uri = "https://api.securitycenter.microsoft.com/api/machineactions/$machineActionId/GetLiveResponseResultDownloadLink(index=0)"
    $headers = @{
        "Authorization" = "Bearer $token"
    }

    try {
        $response = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers
        if ($response -and $response.'@odata.context') {
            $downloadLink = $response.value
            $tempFilePath = [System.IO.Path]::GetTempFileName()
            Invoke-WebRequest -Uri $downloadLink -OutFile $tempFilePath
            $content = Get-Content -Path $tempFilePath -Raw
            try {
                $jsonResponse = $content | ConvertFrom-Json
                $scriptName = $jsonResponse.script_name
                $exitCode = $jsonResponse.exit_code
                $scriptOutput = $jsonResponse.script_output
                $scriptErrors = $jsonResponse.script_errors
                Remove-Item -Path $tempFilePath
                return [PSCustomObject]@{
                    ScriptName      = $scriptName
                    ExitCode        = $exitCode
                    ScriptOutput    = $scriptOutput
                    ScriptErrors    = $scriptErrors
                    Status          = "Success"
                    MachineActionId = $machineActionId
                }
            } catch {
                Remove-Item -Path $tempFilePath
                return [PSCustomObject]@{
                    ScriptName      = $null
                    ExitCode        = $null
                    ScriptOutput    = $null
                    ScriptErrors    = $null
                    Status          = "Failed"
                    MachineActionId = $machineActionId
                    Error           = "Output is not valid JSON. Raw output: $content"
                }
            }
        } else {
            Write-Output "Failed to retrieve the download link."
            return [PSCustomObject]@{
                Status = "Failed"
                MachineActionId = $machineActionId
                Error = "Failed to retrieve the download link."
            }
        }
    } catch {
        Write-Output "An error occurred: $_"
        return [PSCustomObject]@{
            Status = "Failed"
            MachineActionId = $machineActionId
            Error = $_.Exception.Message
        }
    }
}

function Get-Machines {
    param (
        [Parameter(Mandatory = $true)]
        [string]$token,
        [Parameter(Mandatory = $false)]
        [string]$filter
    )
    $baseFilter = "onboardingStatus eq 'Onboarded' and healthStatus eq 'Active'"
    if ($filter) {
        $combinedFilter = "$baseFilter and $filter"
    } else {
        $combinedFilter = $baseFilter
    }
    $uri = "https://api.securitycenter.microsoft.com/api/machines?`$filter=$combinedFilter" 
    $headers = @{
        "Authorization" = "Bearer $token"
    }
    $responses = @()
    try {
        do {
            $response = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers -ErrorAction Stop
            $responses += $response.value | ForEach-Object {
                [PSCustomObject]@{
                    Id = $_.id
                    MergedIntoMachineId = $_.mergedIntoMachineId
                    IsPotentialDuplication = $_.isPotentialDuplication
                    IsExcluded = $_.isExcluded
                    ExclusionReason = $_.exclusionReason
                    ComputerDnsName = $_.computerDnsName
                    FirstSeen = $_.firstSeen
                    LastSeen = $_.lastSeen
                    OsPlatform = $_.osPlatform
                    OsVersion = $_.osVersion
                    OsProcessor = $_.osProcessor
                    Version = $_.version
                    LastIpAddress = $_.lastIpAddress
                    LastExternalIpAddress = $_.lastExternalIpAddress
                    AgentVersion = $_.agentVersion
                    OsBuild = $_.osBuild
                    HealthStatus = $_.healthStatus
                    DeviceValue = $_.deviceValue
                    RbacGroupId = $_.rbacGroupId
                    RbacGroupName = $_.rbacGroupName
                    RiskScore = $_.riskScore
                    ExposureLevel = $_.exposureLevel
                    IsAadJoined = $_.isAadJoined
                    AadDeviceId = $_.aadDeviceId
                    MachineTags = $_.machineTags
                    DefenderAvStatus = $_.defenderAvStatus
                    OnboardingStatus = $_.onboardingStatus
                    OsArchitecture = $_.osArchitecture
                    ManagedBy = $_.managedBy
                    ManagedByStatus = $_.managedByStatus
                    IpAddresses = $_.ipAddresses
                    VmMetadata = $_.vmMetadata
                }
            }
            $uri = $response.'@odata.nextLink'
        } while ($uri)
        return $responses
    } catch {
        Write-Error "Failed to retrieve machines: $_"
    }
}

function Get-Actions {
    param (
        [Parameter(Mandatory = $true)]
        [string]$token
    )
    $startDate = (Get-Date).AddDays(-90).ToString("yyyy-MM-ddTHH:mm:ssZ")
    $uri = "https://api.securitycenter.microsoft.com/api/machineactions?`$filter=CreationDateTimeUtc ge $startDate&`$orderby=CreationDateTimeUtc desc"
    $headers = @{
        "Authorization" = "Bearer $token"
    }
    $allResults = @()
    try {
        $response = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers -ErrorAction Stop
        $allResults += $response.value | ForEach-Object {
            [PSCustomObject]@{
                Id = $_.id
                Type = $_.type
                Title = $_.title
                Requestor = $_.requestor
                RequestorComment = $_.requestorComment
                Status = $_.status
                MachineId = $_.machineId
                ComputerDnsName = $_.computerDnsName
                CreationDateTimeUtc = $_.creationDateTimeUtc
                LastUpdateDateTimeUtc = $_.lastUpdateDateTimeUtc
                CancellationRequestor = $_.cancellationRequestor
                CancellationComment = $_.cancellationComment
                CancellationDateTimeUtc = $_.cancellationDateTimeUtc
                ErrorHResult = $_.errorHResult
                Scope = $_.scope
                ExternalId = $_.externalId
                RequestSource = $_.requestSource
                RelatedFileInfo = $_.relatedFileInfo
                Commands = $_.commands
                TroubleshootInfo = $_.troubleshootInfo
            }
        }
        while ($response.'@odata.nextLink') {
            $response = Invoke-RestMethod -Uri $response.'@odata.nextLink' -Method Get -Headers $headers -ErrorAction Stop
            $allResults += $response.value | ForEach-Object {
                [PSCustomObject]@{
                    Id = $_.id
                    Type = $_.type
                    Title = $_.title
                    Requestor = $_.requestor
                    RequestorComment = $_.requestorComment
                    Status = $_.status
                    MachineId = $_.machineId
                    ComputerDnsName = $_.computerDnsName
                    CreationDateTimeUtc = $_.creationDateTimeUtc
                    LastUpdateDateTimeUtc = $_.lastUpdateDateTimeUtc
                    CancellationRequestor = $_.cancellationRequestor
                    CancellationComment = $_.cancellationComment
                    CancellationDateTimeUtc = $_.cancellationDateTimeUtc
                    ErrorHResult = $_.errorHResult
                    Scope = $_.scope
                    ExternalId = $_.externalId
                    RequestSource = $_.requestSource
                    RelatedFileInfo = $_.relatedFileInfo
                    Commands = $_.commands
                    TroubleshootInfo = $_.troubleshootInfo
                }
            }
        }
        return $allResults
    } catch {
        Write-Error "Failed to retrieve machine actions: $_"
    }
}
function Undo-Actions {
    param (
        [Parameter(Mandatory = $true)]
        [string]$token
    )

    $allActions = Get-Actions -token $token
    $pendingActions = $allActions | Where-Object { $_.Status -eq "Pending" }

    Write-Host "Found $($pendingActions.Count) pending actions to cancel."

    $headers = @{
        "Authorization" = "Bearer $token"
    }
    $responses = @()

    foreach ($action in $pendingActions) {
        $actionId = $action.Id
        $uri = "https://api.securitycenter.microsoft.com/api/machineactions/$actionId/cancel"
        $body = @{
            "Comment" = "MDEAutomator"
        } | ConvertTo-Json

        try {
            $response = Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body $body
            $responses += [PSCustomObject]@{
                ActionId = $actionId
                Status = "Canceled"
                Response = $response
            }
        } catch {
            if ($_.Exception.Response.StatusCode -eq 400) {
                Write-Host "Action $actionId could not be canceled. Skipping."
                $responses += [PSCustomObject]@{
                    ActionId = $actionId
                    Status = "Skipped"
                    Error = $_.Exception.Message
                }
                continue
            }
            Write-Error "Failed to cancel action $actionId $($_.Exception.Message)"
            $responses += [PSCustomObject]@{
                ActionId = $actionId
                Status = "Failed"
                Error = $_.Exception.Message
            }
        }
    }
    Write-Host "Undo-Actions completed. Total processed: $($responses.Count)"
    return $responses
}

function Invoke-MachineIsolation {
    param (
        [Parameter(Mandatory = $true)]
        [string]$token,
        [Parameter(Mandatory = $true)]
        [string[]]$DeviceIds
    )
    $headers = @{
        "Authorization" = "Bearer $token"
    }
    $body = @{
        "Comment" = "MDEAutomator"
        "IsolationType" = "Selective"
    }
    $responses = @()

    foreach ($DeviceId in $DeviceIds) {
        $uri = "https://api.securitycenter.microsoft.com/api/machines/$DeviceId/isolate"
        try {
            $response = Invoke-WithRetry -ScriptBlock {
                Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body ($body | ConvertTo-Json) -ContentType "application/json" -ErrorAction Stop
            }

            $actionId = $response.id
            if ([string]::IsNullOrEmpty($response.id)) {
                Write-Host "No machine action ID received for DeviceId: $DeviceId. Marking as failed and continuing."
                continue
            }
            Start-Sleep -Seconds 5
            $statusSucceeded = Get-MachineActionStatus -machineActionId $actionId -token $token

            if ($statusSucceeded) {
                Write-Host "Successfully isolated DeviceId: $DeviceId"
            } else {
                Write-Error "Failed to isolate DeviceId: $DeviceId"
            }

            $responses += [PSCustomObject]@{
                DeviceId = $DeviceId
                Response = [PSCustomObject]@{
                    Id = $response.id
                    Type = $response.type
                    Title = $response.title
                    Requestor = $response.requestor
                    RequestorComment = $response.requestorComment
                    Status = if ($statusSucceeded) { "Succeeded" } else { "Failed" }
                    MachineId = $response.machineId
                    ComputerDnsName = $response.computerDnsName
                    CreationDateTimeUtc = $response.creationDateTimeUtc
                    LastUpdateDateTimeUtc = $response.lastUpdateDateTimeUtc
                    CancellationRequestor = $response.cancellationRequestor
                    CancellationComment = $response.cancellationComment
                    CancellationDateTimeUtc = $response.cancellationDateTimeUtc
                    ErrorHResult = $response.errorHResult
                    Scope = $response.scope
                    ExternalId = $response.externalId
                    RequestSource = $response.requestSource
                    RelatedFileInfo = $response.relatedFileInfo
                    Commands = $response.commands
                    TroubleshootInfo = $response.troubleshootInfo
                }
            }
        } catch {
            Write-Error "Failed to initiate isolation for DeviceId: $DeviceId $_"
            $responses += [PSCustomObject]@{
                DeviceId = $DeviceId
                Error = $_.Exception.Message
            }
        }
    }
    return $responses
}

function Undo-MachineIsolation {
    param (
        [Parameter(Mandatory = $true)]
        [string]$token,
        [Parameter(Mandatory = $true)]
        [string[]]$DeviceIds
    )
    $headers = @{
        "Authorization" = "Bearer $token"
    }
    $body = @{
        "Comment" = "MDEAutomator"
    }
    $responses = @()

    foreach ($DeviceId in $DeviceIds) {
        $uri = "https://api.securitycenter.microsoft.com/api/machines/$DeviceId/unisolate"
        try {
            $response = Invoke-WithRetry -ScriptBlock {
                Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body ($body | ConvertTo-Json) -ContentType "application/json" -ErrorAction Stop
            }

            $actionId = $response.id
            if ([string]::IsNullOrEmpty($response.id)) {
                Write-Host "No machine action ID received for DeviceId: $DeviceId. Marking as failed and continuing."
                continue
            }
            Start-Sleep -Seconds 5
            $statusSucceeded = Get-MachineActionStatus -machineActionId $actionId -token $token

            if ($statusSucceeded) {
                Write-Host "Successfully unisolated DeviceId: $DeviceId"
            } else {
                Write-Error "Failed to unisolate DeviceId: $DeviceId"
            }

            $responses += [PSCustomObject]@{
                DeviceId = $DeviceId
                Response = [PSCustomObject]@{
                    Id = $response.id
                    Type = $response.type
                    Title = $response.title
                    Requestor = $response.requestor
                    RequestorComment = $response.requestorComment
                    Status = if ($statusSucceeded) { "Succeeded" } else { "Failed" }
                    MachineId = $response.machineId
                    ComputerDnsName = $response.computerDnsName
                    CreationDateTimeUtc = $response.creationDateTimeUtc
                    LastUpdateDateTimeUtc = $response.lastUpdateDateTimeUtc
                    CancellationRequestor = $response.cancellationRequestor
                    CancellationComment = $response.cancellationComment
                    CancellationDateTimeUtc = $response.cancellationDateTimeUtc
                    ErrorHResult = $response.errorHResult
                    Scope = $response.scope
                    ExternalId = $response.externalId
                    RequestSource = $response.requestSource
                    RelatedFileInfo = $response.relatedFileInfo
                    Commands = $response.commands
                    TroubleshootInfo = $response.troubleshootInfo
                }
            }
        } catch {
            Write-Error "Failed to unisolate DeviceId: $DeviceId $_"
            $responses += [PSCustomObject]@{
                DeviceId = $DeviceId
                Error = $_.Exception.Message
            }
        }
    }
    return $responses 
}

function Invoke-ContainDevice {
    param (
        [Parameter(Mandatory = $true)]
        [string]$token,
        [Parameter(Mandatory = $true)]
        [string[]]$DeviceIds
    )
    $headers = @{
        "Authorization" = "Bearer $token"
    }
    $body = @{
        "Comment" = "MDEAutomator"
        "IsolationType" = "UnManagedDevice"
    }
    $responses = @()

    foreach ($DeviceId in $DeviceIds) {
        $uri = "https://api.securitycenter.microsoft.com/api/machines/$DeviceId/isolate"
        try {
            $response = Invoke-WithRetry -ScriptBlock {
                Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body ($body | ConvertTo-Json) -ContentType "application/json" -ErrorAction Stop
            }

            $actionId = $response.id
            if ([string]::IsNullOrEmpty($response.id)) {
                Write-Host "No machine action ID received for DeviceId: $DeviceId. Marking as failed and continuing."
                continue
            }
            Start-Sleep -Seconds 5
            $statusSucceeded = Get-MachineActionStatus -machineActionId $actionId -token $token

            if ($statusSucceeded) {
                Write-Host "Successfully contained DeviceId: $DeviceId"
            } else {
                Write-Error "Failed to contain DeviceId: $DeviceId"
            }

            $responses += [PSCustomObject]@{
                DeviceId = $DeviceId
                Response = [PSCustomObject]@{
                    Id = $response.id
                    Type = $response.type
                    Title = $response.title
                    Requestor = $response.requestor
                    RequestorComment = $response.requestorComment
                    Status = if ($statusSucceeded) { "Succeeded" } else { "Failed" }
                    MachineId = $response.machineId
                    ComputerDnsName = $response.computerDnsName
                    CreationDateTimeUtc = $response.creationDateTimeUtc
                    LastUpdateDateTimeUtc = $response.lastUpdateDateTimeUtc
                    CancellationRequestor = $response.cancellationRequestor
                    CancellationComment = $response.cancellationComment
                    CancellationDateTimeUtc = $response.cancellationDateTimeUtc
                    ErrorHResult = $response.errorHResult
                    Scope = $response.scope
                    ExternalId = $response.externalId
                    RequestSource = $response.requestSource
                    RelatedFileInfo = $response.relatedFileInfo
                    Commands = $response.commands
                    TroubleshootInfo = $response.troubleshootInfo
                }
            }
        } catch {
            Write-Error "Failed to initiate containment for DeviceId: $DeviceId $_"
            $responses += [PSCustomObject]@{
                DeviceId = $DeviceId
                Error = $_.Exception.Message
            }
        }
    }
    return $responses
}

function Undo-ContainDevice {
    param (
        [Parameter(Mandatory = $true)]
        [string]$token,
        [Parameter(Mandatory = $true)]
        [string[]]$DeviceIds
    )
    $headers = @{
        "Authorization" = "Bearer $token"
    }
    $body = @{
        "Comment" = "MDEAutomator"
    }
    $responses = @()

    foreach ($DeviceId in $DeviceIds) {
        $uri = "https://api.securitycenter.microsoft.com/api/machines/$DeviceId/unisolate"
        try {
            $response = Invoke-WithRetry -ScriptBlock {
                Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body ($body | ConvertTo-Json) -ContentType "application/json" -ErrorAction Stop
            }

            $actionId = $response.id
            if ([string]::IsNullOrEmpty($response.id)) {
                Write-Host "No machine action ID received for DeviceId: $DeviceId. Marking as failed and continuing."
                continue
            }
            Start-Sleep -Seconds 5
            $statusSucceeded = Get-MachineActionStatus -machineActionId $actionId -token $token

            if ($statusSucceeded) {
                Write-Host "Successfully uncontained DeviceId: $DeviceId"
            } else {
                Write-Error "Failed to uncontain DeviceId: $DeviceId"
            }

            $responses += [PSCustomObject]@{
                DeviceId = $DeviceId
                Response = [PSCustomObject]@{
                    Id = $response.id
                    Type = $response.type
                    Title = $response.title
                    Requestor = $response.requestor
                    RequestorComment = $response.requestorComment
                    Status = if ($statusSucceeded) { "Succeeded" } else { "Failed" }
                    MachineId = $response.machineId
                    ComputerDnsName = $response.computerDnsName
                    CreationDateTimeUtc = $response.creationDateTimeUtc
                    LastUpdateDateTimeUtc = $response.lastUpdateDateTimeUtc
                    CancellationRequestor = $response.cancellationRequestor
                    CancellationComment = $response.cancellationComment
                    CancellationDateTimeUtc = $response.cancellationDateTimeUtc
                    ErrorHResult = $response.errorHResult
                    Scope = $response.scope
                    ExternalId = $response.externalId
                    RequestSource = $response.requestSource
                    RelatedFileInfo = $response.relatedFileInfo
                    Commands = $response.commands
                    TroubleshootInfo = $response.troubleshootInfo
                }
            }
        } catch {
            Write-Error "Failed to uncontain DeviceId: $DeviceId $_"
            $responses += [PSCustomObject]@{
                DeviceId = $DeviceId
                Error = $_.Exception.Message
            }
        }
    }
    return $responses 
}


function Invoke-RestrictAppExecution {
    param (
        [Parameter(Mandatory = $true)]
        [string]$token,
        [Parameter(Mandatory = $true)]
        [string[]]$DeviceIds
    )
    $headers = @{
        "Authorization" = "Bearer $token"
    }
    $body = @{
        "Comment" = "MDEAutomator"
    }
    $responses = @()

    foreach ($DeviceId in $DeviceIds) {
        $uri = "https://api.securitycenter.microsoft.com/api/machines/$DeviceId/restrictCodeExecution"
        try {
            $response = Invoke-WithRetry -ScriptBlock {
                Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body ($body | ConvertTo-Json) -ContentType "application/json" -ErrorAction Stop
            }

            $actionId = $response.id
            if ([string]::IsNullOrEmpty($response.id)) {
                Write-Host "No machine action ID received for DeviceId: $DeviceId. Marking as failed and continuing."
                continue
            }
            Start-Sleep -Seconds 5
            $statusSucceeded = Get-MachineActionStatus -machineActionId $actionId -token $token

            if ($statusSucceeded) {
                Write-Host "Successfully restricted code execution on DeviceId: $DeviceId"
            } else {
                Write-Error "Failed to restrict code execution on DeviceId: $DeviceId"
            }

            $responses += [PSCustomObject]@{
                DeviceId = $DeviceId
                Response = [PSCustomObject]@{
                    Id = $response.id
                    Type = $response.type
                    Title = $response.title
                    Requestor = $response.requestor
                    RequestorComment = $response.requestorComment
                    Status = if ($statusSucceeded) { "Succeeded" } else { "Failed" }
                    MachineId = $response.machineId
                    ComputerDnsName = $response.computerDnsName
                    CreationDateTimeUtc = $response.creationDateTimeUtc
                    LastUpdateDateTimeUtc = $response.lastUpdateDateTimeUtc
                    CancellationRequestor = $response.cancellationRequestor
                    CancellationComment = $response.cancellationComment
                    CancellationDateTimeUtc = $response.cancellationDateTimeUtc
                    ErrorHResult = $response.errorHResult
                    Scope = $response.scope
                    ExternalId = $response.externalId
                    RequestSource = $response.requestSource
                    RelatedFileInfo = $response.relatedFileInfo
                    Commands = $response.commands
                    TroubleshootInfo = $response.troubleshootInfo
                }
            }
        } catch {
            Write-Error "Failed to restrict code execution on DeviceId: $DeviceId $_"
            $responses += [PSCustomObject]@{
                DeviceId = $DeviceId
                Error = $_.Exception.Message
            }
        }
    }
    return $responses
}

function Undo-RestrictAppExecution {
    param (
        [Parameter(Mandatory = $true)]
        [string]$token,
        [Parameter(Mandatory = $true)]
        [string[]]$DeviceIds
    )
    $headers = @{
        "Authorization" = "Bearer $token"
    }
    $body = @{
        "Comment" = "MDEAutomator"
    }
    $responses = @()

    foreach ($DeviceId in $DeviceIds) {
        $uri = "https://api.securitycenter.microsoft.com/api/machines/$DeviceId/unrestrictCodeExecution"
        try {
            $response = Invoke-WithRetry -ScriptBlock {
                Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body ($body | ConvertTo-Json) -ContentType "application/json" -ErrorAction Stop
            }

            $actionId = $response.id
            if ([string]::IsNullOrEmpty($response.id)) {
                Write-Host "No machine action ID received for DeviceId: $DeviceId. Marking as failed and continuing."
                continue
            }
            Start-Sleep -Seconds 5
            $statusSucceeded = Get-MachineActionStatus -machineActionId $actionId -token $token

            if ($statusSucceeded) {
                Write-Host "Successfully unrestricted code execution on DeviceId: $DeviceId"
            } else {
                Write-Error "Failed to unrestrict code execution on DeviceId: $DeviceId"
            }

            $responses += [PSCustomObject]@{
                DeviceId = $DeviceId
                Response = [PSCustomObject]@{
                    Id = $response.id
                    Type = $response.type
                    Title = $response.title
                    Requestor = $response.requestor
                    RequestorComment = $response.requestorComment
                    Status = if ($statusSucceeded) { "Succeeded" } else { "Failed" }
                    MachineId = $response.machineId
                    ComputerDnsName = $response.computerDnsName
                    CreationDateTimeUtc = $response.creationDateTimeUtc
                    LastUpdateDateTimeUtc = $response.lastUpdateDateTimeUtc
                    CancellationRequestor = $response.cancellationRequestor
                    CancellationComment = $response.cancellationComment
                    CancellationDateTimeUtc = $response.cancellationDateTimeUtc
                    ErrorHResult = $response.errorHResult
                    Scope = $response.scope
                    ExternalId = $response.externalId
                    RequestSource = $response.requestSource
                    RelatedFileInfo = $response.relatedFileInfo
                    Commands = $response.commands
                    TroubleshootInfo = $response.troubleshootInfo
                }
            }
        } catch {
            Write-Error "Failed to unrestrict code execution on DeviceId: $DeviceId $_"
            $responses += [PSCustomObject]@{
                DeviceId = $DeviceId
                Error = $_.Exception.Message
            }
        }
    }
    return $responses
}

function Invoke-TiFile {
    param (
        [Parameter(Mandatory = $true)]
        [string]$token,
        [Parameter(Mandatory = $false)]
        [string[]]$Sha1s,
        [Parameter(Mandatory = $false)]
        [string[]]$Sha256s
    )
    $uri = "https://api.securitycenter.microsoft.com/api/indicators"
    $headers = @{
        "Authorization" = "Bearer $token"
    }
    $responses = @()

    if ($Sha1s) {
        foreach ($Sha1 in $Sha1s) {
            $body = @{
                "indicatorValue" = $Sha1
                "indicatorType" = "FileSha1"
                "title" = "MDEAutomator $Sha1"
                "action" = "BlockAndRemediate"
                "severity" = "High"
                "description" = "MDEautomator has created this Custom Threat Indicator."
                "recommendedActions" = "Investigate & take appropriate action."
            }
            try {
                $response = Invoke-WithRetry -ScriptBlock {
                    Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body ($body | ConvertTo-Json) -ContentType "application/json" -ErrorAction Stop
                }
                Write-Output "Successfully created Threat Indicator for Sha1: $Sha1"
                $responses += [PSCustomObject]@{
                    Sha1 = $Sha1
                    Response = $response
                }
            } catch {
                Write-Error "Failed to create Threat Indicator for Sha1: $Sha1 $_"
                $responses += [PSCustomObject]@{
                    Sha1 = $Sha1
                    Error = $_.Exception.Message
                }
            }
        }
    }

    if ($Sha256s) {
        foreach ($Sha256 in $Sha256s) {
            $body = @{
                "indicatorValue" = $Sha256
                "indicatorType" = "FileSha256"
                "title" = "MDEAutomator $Sha256"
                "action" = "BlockAndRemediate"
                "severity" = "High"
                "description" = "MDEautomator has created this Custom Threat Indicator."
                "recommendedActions" = "Investigate & take appropriate action."
            }
            try {
                $response = Invoke-WithRetry -ScriptBlock {
                    Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body ($body | ConvertTo-Json) -ContentType "application/json" -ErrorAction Stop
                }
                Write-Output "Successfully created Threat Indicator for Sha256: $Sha256"
                $responses += [PSCustomObject]@{
                    Sha256 = $Sha256
                    Response = $response
                }
            } catch {
                Write-Error "Failed to create Threat Indicator for Sha256: $Sha256 $_"
                $responses += [PSCustomObject]@{
                    Sha256 = $Sha256
                    Error = $_.Exception.Message
                }
            }
        }
    }

    return $responses
}

function Undo-TiFile {
    param (
        [Parameter(Mandatory = $true)]
        [string]$token,
        [Parameter(Mandatory = $false)]
        [string[]]$Sha1s,
        [Parameter(Mandatory = $false)]
        [string[]]$Sha256s
    )
    $headers = @{
        "Authorization" = "Bearer $token"
    }
    $responses = @()

    if ($Sha1s) {
        foreach ($Sha1 in $Sha1s) {
            $uriGet = "https://api.securitycenter.microsoft.com/api/indicators?`$filter=indicatorValue eq '$Sha1'"
            try {
                $responseGet = Invoke-RestMethod -Uri $uriGet -Method Get -Headers $headers -ErrorAction Stop
                if ($responseGet.value.Count -eq 0) {
                    Write-Error "No Threat Indicator found for Sha1: $Sha1"
                    $responses += [PSCustomObject]@{
                        Sha1 = $Sha1
                        Error = "No Threat Indicator found"
                    }
                    continue
                }

                $indicatorId = $responseGet.value[0].id
                $uriDelete = "https://api.securitycenter.microsoft.com/api/indicators/$indicatorId"

                $responseDelete = Invoke-WithRetry -ScriptBlock {
                    Invoke-RestMethod -Uri $uriDelete -Method Delete -Headers $headers -ErrorAction Stop
                }
                Write-Output "Successfully deleted Threat Indicator for Sha1: $Sha1"
                $responses += [PSCustomObject]@{
                    Sha1 = $Sha1
                    Response = $responseDelete
                }
            } catch {
                Write-Error "Failed to delete Threat Indicator for Sha1: $Sha1 $_"
                $responses += [PSCustomObject]@{
                    Sha1 = $Sha1
                    Error = $_.Exception.Message
                }
            }
        }
    }

    if ($Sha256s) {
        foreach ($Sha256 in $Sha256s) {
            $uriGet = "https://api.securitycenter.microsoft.com/api/indicators?`$filter=indicatorValue eq '$Sha256'"
            try {
                $responseGet = Invoke-RestMethod -Uri $uriGet -Method Get -Headers $headers -ErrorAction Stop
                if ($responseGet.value.Count -eq 0) {
                    Write-Error "No Threat Indicator found for Sha256: $Sha256"
                    $responses += [PSCustomObject]@{
                        Sha256 = $Sha256
                        Error = "No Threat Indicator found"
                    }
                    continue
                }

                $indicatorId = $responseGet.value[0].id
                $uriDelete = "https://api.securitycenter.microsoft.com/api/indicators/$indicatorId"

                $responseDelete = Invoke-WithRetry -ScriptBlock {
                    Invoke-RestMethod -Uri $uriDelete -Method Delete -Headers $headers -ErrorAction Stop
                }
                Write-Output "Successfully deleted Threat Indicator for Sha256: $Sha256"
                $responses += [PSCustomObject]@{
                    Sha256 = $Sha256
                    Response = $responseDelete
                }
            } catch {
                Write-Error "Failed to delete Threat Indicator for Sha256: $Sha256 $_"
                $responses += [PSCustomObject]@{
                    Sha256 = $Sha256
                    Error = $_.Exception.Message
                }
            }
        }
    }

    return $responses 
}

function Invoke-TiIP {
    param (
        [Parameter(Mandatory = $true)]
        [string]$token,
        [Parameter(Mandatory = $true)]
        [string[]]$IPs
    )
    $uri = "https://api.securitycenter.microsoft.com/api/indicators"
    $headers = @{
        "Authorization" = "Bearer $token"
    }
    $responses = @()

    foreach ($IP in $IPs) {
        $body = @{
            "indicatorValue" = $IP
            "indicatorType" = "IpAddress"
            "action" = "Block"
            "severity" = "High"
            "title" = "MDEAutomator $IP"
            "description" = "MDEautomator has created this Custom Threat Indicator."
            "recommendedActions" = "Investigate & take appropriate action."
        }
        try {
            $response = Invoke-WithRetry -ScriptBlock {
                try {
                    Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body ($body | ConvertTo-Json) -ContentType "application/json" -ErrorAction Stop
                } catch {
                    if ($_.Exception.Response.StatusCode -eq 404) {
                        Write-Host "API responded with 'not found'. Continuing execution."
                    } else {
                        throw $_
                    }
                }
            }
            Write-Host "Successfully created Threat Indicator for IP: $IP"
            $responses += [PSCustomObject]@{
                IP = $IP
                Response = $response
            }
        } catch {
            Write-Error "Failed to create Threat Indicator for IP: $IP $_"
            $responses += [PSCustomObject]@{
                IP = $IP
                Error = $_.Exception.Message
            }
        }
    }
    return $responses
}

function Undo-TiURL {
    param (
        [Parameter(Mandatory = $true)]
        [string]$token,
        [Parameter(Mandatory = $true)]
        [string[]]$URLs
    )
    $headers = @{
        "Authorization" = "Bearer $token"
    }
    $responses = @()

    foreach ($URL in $URLs) {
        $uriGet = "https://api.securitycenter.microsoft.com/api/indicators?`$filter=indicatorValue eq '$URL'"
        try {
            $responseGet = Invoke-RestMethod -Uri $uriGet -Method Get -Headers $headers -ErrorAction Stop
            if ($responseGet.value.Count -eq 0) {
                Write-Error "No Threat Indicator found for URL: $URL"
                $responses += [PSCustomObject]@{
                    URL = $URL
                    Error = "No Threat Indicator found"
                }
                continue
            }

            $indicatorId = $responseGet.value[0].id
            $uriDelete = "https://api.securitycenter.microsoft.com/api/indicators/$indicatorId"

            $responseDelete = Invoke-WithRetry -ScriptBlock {
                Invoke-RestMethod -Uri $uriDelete -Method Delete -Headers $headers -ErrorAction Stop
            }
            Write-Output "Successfully deleted Threat Indicator for URL: $URL"
            $responses += [PSCustomObject]@{
                URL = $URL
                Response = $responseDelete
            }
        } catch {
            Write-Error "Failed to delete Threat Indicator for URL: $URL $_"
            $responses += [PSCustomObject]@{
                URL = $URL
                Error = $_.Exception.Message
            }
        }
    }
    return $responses
}

function Invoke-TiURL {
    param (
        [Parameter(Mandatory = $true)]
        [string]$token,
        [Parameter(Mandatory = $true)]
        [string[]]$URLs
    )
    $uri = "https://api.securitycenter.microsoft.com/api/indicators"
    $headers = @{
        "Authorization" = "Bearer $token"
    }
    $responses = @()

    foreach ($URL in $URLs) {
        $body = @{
            "indicatorValue" = "$URL"
            "indicatorType" = "DomainName"
            "action" = "Block"
            "severity" = "High"
            "title" = "MDEAutomator $URL"
            "description" = "MDEautomator has created this Custom Threat Indicator."
            "recommendedActions" = "Investigate & take appropriate action."
        }
        try {
            $response = Invoke-WithRetry -ScriptBlock {
                Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body ($body | ConvertTo-Json) -ContentType "application/json" -ErrorAction Stop
            }
            Write-Host "Successfully created Threat Indicator for URL: $URL"
            $responses += [PSCustomObject]@{
                URL = $URL
                Response = $response
            }
        } catch {
            Write-Error "Failed to create Threat Indicator for URL: $URL $_"
            $responses += [PSCustomObject]@{
                URL = $URL
                Error = $_.Exception.Message
            }
        }
    }
    return $responses
}

function Undo-TiIP {
    param (
        [Parameter(Mandatory = $true)]
        [string]$token,
        [Parameter(Mandatory = $true)]
        [string[]]$IPs
    )
    $headers = @{
        "Authorization" = "Bearer $token"
    }
    $responses = @()

    foreach ($IP in $IPs) {
        $uriGet = "https://api.securitycenter.microsoft.com/api/indicators?`$filter=indicatorValue eq '$IP'"
        try {
            $responseGet = Invoke-RestMethod -Uri $uriGet -Method Get -Headers $headers -ErrorAction Stop
            if ($responseGet.value.Count -eq 0) {
                Write-Error "No Threat Indicator found for IP: $IP"
                $responses += [PSCustomObject]@{
                    IP = $IP
                    Error = "No Threat Indicator found"
                }
                continue
            }

            $indicatorId = $responseGet.value[0].id
            $uriDelete = "https://api.securitycenter.microsoft.com/api/indicators/$indicatorId"

            $responseDelete = Invoke-WithRetry -ScriptBlock {
                Invoke-RestMethod -Uri $uriDelete -Method Delete -Headers $headers -ErrorAction Stop
            }
            Write-Output "Successfully deleted Threat Indicator for IP: $IP"
            $responses += [PSCustomObject]@{
                IP = $IP
                Response = $responseDelete
            }
        } catch {
            Write-Error "Failed to delete Threat Indicator for IP: $IP $_"
            $responses += [PSCustomObject]@{
                IP = $IP
                Error = $_.Exception.Message
            }
        }
    }
    return $responses
}
function Invoke-TiCert {
    param (
        [Parameter(Mandatory = $true)]
        [string]$token,
        [Parameter(Mandatory = $false)]
        [string[]]$Sha1s
    )
    $uri = "https://api.securitycenter.microsoft.com/api/indicators"
    $headers = @{
        "Authorization" = "Bearer $token"
    }
    $responses = @()

    if ($Sha1s) {
        foreach ($Sha1 in $Sha1s) {
            $body = @{
                "indicatorValue" = $Sha1
                "indicatorType" = "CertificateThumbprint"
                "title" = "MDEAutomator $Sha1"
                "action" = "Block"
                "severity" = "High"
                "description" = "MDEautomator has created this Custom Threat Indicator."
                "recommendedActions" = "Investigate & take appropriate action."
            }
            try {
                $response = Invoke-WithRetry -ScriptBlock {
                    Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body ($body | ConvertTo-Json) -ContentType "application/json" -ErrorAction Stop
                }
                Write-Output "Successfully created Threat Indicator for Sha1: $Sha1"
                $responses += [PSCustomObject]@{
                    Sha1 = $Sha1
                    Response = $response
                }
            } catch {
                Write-Error "Failed to create Threat Indicator for Sha1: $Sha1 $_"
                $responses += [PSCustomObject]@{
                    Sha1 = $Sha1
                    Error = $_.Exception.Message
                }
            }
        }
    }

    return $responses
}

function Undo-TiCert {
    param (
        [Parameter(Mandatory = $true)]
        [string]$token,
        [Parameter(Mandatory = $false)]
        [string[]]$Sha1s
    )
    $headers = @{
        "Authorization" = "Bearer $token"
    }
    $responses = @()

    if ($Sha1s) {
        foreach ($Sha1 in $Sha1s) {
            $uriGet = "https://api.securitycenter.microsoft.com/api/indicators?`$filter=indicatorValue eq '$Sha1'"
            try {
                $responseGet = Invoke-RestMethod -Uri $uriGet -Method Get -Headers $headers -ErrorAction Stop
                if ($responseGet.value.Count -eq 0) {
                    Write-Error "No Threat Indicator found for Sha1: $Sha1"
                    $responses += [PSCustomObject]@{
                        Sha1 = $Sha1
                        Error = "No Threat Indicator found"
                    }
                    continue
                }

                $indicatorId = $responseGet.value[0].id
                $uriDelete = "https://api.securitycenter.microsoft.com/api/indicators/$indicatorId"

                $responseDelete = Invoke-WithRetry -ScriptBlock {
                    Invoke-RestMethod -Uri $uriDelete -Method Delete -Headers $headers -ErrorAction Stop
                }
                Write-Output "Successfully deleted Threat Indicator for Sha1: $Sha1"
                $responses += [PSCustomObject]@{
                    Sha1 = $Sha1
                    Response = $responseDelete
                }
            } catch {
                Write-Error "Failed to delete Threat Indicator for Sha1: $Sha1 $_"
                $responses += [PSCustomObject]@{
                    Sha1 = $Sha1
                    Error = $_.Exception.Message
                }
            }
        }
    }

    return $responses
}

# Export the functions
Export-ModuleMember -Function Connect-MDE, Get-AccessToken, Get-Machines, Get-Actions, Undo-Actions, Invoke-MachineIsolation, Undo-MachineIsolation, Invoke-ContainDevice, Undo-ContainDevice,
    Invoke-RestrictAppExecution, Undo-RestrictAppExecution, Invoke-TiFile, Undo-TiFile, Invoke-TiCert, Undo-TiCert, Invoke-TiIP, Undo-TiIP, 
    Invoke-TiURL, Undo-TiURL, Get-RequestParam, Get-SecretFromKeyVault, 
    Invoke-WithRetry, Invoke-UploadLR, Invoke-PutFile, Invoke-GetFile, Invoke-CollectInvestigationPackage, Invoke-LRScript, 
    Get-MachineActionStatus, Get-LiveResponseOutput, Invoke-FullDiskScan