Public/Remove-AzSentinelTIIndicators.ps1

<#
.SYNOPSIS
    Removes Threat Intelligence indicators from Azure Sentinel workspace.

.DESCRIPTION
    This script removes Threat Intelligence indicators from a specified Azure Sentinel workspace
    based on the provided source. It handles authentication, pagination, and bulk deletion of indicators.

.PARAMETER TIsource
    The source of the Threat Intelligence indicators to be deleted.

.PARAMETER SubscriptionId
    The Azure subscription ID containing the Log Analytics workspace.

.PARAMETER LogAnalyticsResourceGroup
    The resource group containing the Log Analytics workspace.

.PARAMETER LogAnalyticsWorkspaceName
    The name of the Log Analytics workspace.

.PARAMETER DaysOld
    Optional. Remove only indicators older than specified number of days.
    
.EXAMPLE
    Remove-AzSentinelTIIndicators -TIsource "MySource" -SubscriptionId "1234" -LogAnalyticsResourceGroup "rg-sentinel" -LogAnalyticsWorkspaceName "law-sentinel" -DaysOld 30
    # Removes all indicators from "MySource" that are older than 30 days

.NOTES
    Version: 1.0
    Requires: Az.Accounts 2.2.3, Az.Kusto 2.3.0, PowerShell 6.2
#>

function Remove-AzSentinelTIIndicators {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $TIsource,

        [Parameter(Mandatory = $true)]
        [ValidatePattern('^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$')]
        [string] $SubscriptionId,

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

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

        [Parameter(Mandatory = $false)]
        [int] $DaysOld = 0
    )
    
    # Initialize logging
    $LogFileName = "TIIndicatorDeletion_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
    
    # Ensure required modules are installed
    try {
        Get-RequiredTIModules -LogFileName $LogFileName
    }
    catch {
        Write-Log -Message "Failed to initialize required modules: $_" -LogFileName $LogFileName -Severity Error
        throw
    }
    
    # Ensure Azure login and correct subscription
    try {
        Confirm-AzLogin -SubscriptionId $SubscriptionId
    }
    catch {
        Write-Log -Message "Failed to authenticate to Azure: $($_.Exception.Message)" -LogFileName $LogFileName -Severity Error
        throw $_
    }

    # Constants
    $ThreatIndicatorsApi = "https://management.azure.com/subscriptions/$SubscriptionId/resourcegroups/$LogAnalyticsResourceGroup/providers/Microsoft.OperationalInsights/workspaces/$LogAnalyticsWorkspaceName/providers/Microsoft.SecurityInsights/threatIntelligence/"
    $SECURITY_INSIGHTS_API_VERSION = "api-version=2022-07-01-preview"
    $PAGE_SIZE = "100"
    $getAllIndicatorsWithSourceFilterUri = $ThreatIndicatorsApi + "query?$SECURITY_INSIGHTS_API_VERSION"
    
    # Add age filter to query parameters if specified
    $queryParams = @{
        "pageSize" = $PAGE_SIZE
        "sources"  = @($TIsource)
    }
    
    if ($DaysOld -gt 0) {
        $cutoffDate = (Get-Date).AddDays(-$DaysOld).ToString("yyyy-MM-ddTHH:mm:ssZ")
        $queryParams.Add("validUntilTime", $cutoffDate)
        Write-Log -Message "Filtering for indicators older than $DaysOld days (before $cutoffDate)" -LogFileName $LogFileName -Severity Information
    }
    
    $getAllIndicatorsPostParameters = $queryParams | ConvertTo-Json
    $bulkApi = "https://management.azure.com/batch?api-version=2020-06-01"

    # This flag checks whether the initial count of indicators in the workspace is already 0 or not
    $indicatorsFound = $false

    # Total count of indicators fetched for the customer's workspace ,and for the provided source
    $indicatorsFetched = 0

    # Total count of indicators deleted
    $indicatorsDeleted = 0

    # We have a max page size of 100 hence at a time, the fetch indicators call can only fetch a list of 100 indicators for any workspace. However, since the bulk
    # API can only support 20 requests in one single request we search for the first 20 results. Because a workspace can also have more than 100 indicators we will
    # loop untill we finish.
    while ($true) {
        try {
            $response = Invoke-AzRestMethod -Uri $getAllIndicatorsWithSourceFilterUri -Method POST -Payload $getAllIndicatorsPostParameters
            if ($response -eq $null -or $response.StatusCode -ne 200) {            
                Write-Log -Message "Failed to fetch indicators. Status Code = $($response.StatusCode)" -LogFileName $LogFileName -Severity Information
                exit 1
            }
    
            $indicatorList = ($response.Content | ConvertFrom-Json).value
        }
        catch {        
            Write-Log -Message "Failed to get all indicators with the specified source. $($_.Exception)" -LogFileName $LogFileName -Severity Error    
            exit 1
        }
    
        if ($indicatorList.Count -eq 0) {
            # If the initial count of indicators in the customer's workspace is already 0, exit.
            if ($indicatorsFound -eq $false) {
                Write-Log -Message "No indicators found with source = $Source! Exiting ..." -LogFileName $LogFileName -Severity Error            
                break
            }
            else {
                Write-Log -Message "Finished querying workspace = $WorkspaceName for indicators with Source = $Source ..." -LogFileName $LogFileName -Severity Information
                Write-Log -Message "Fetched $indicatorsFetched indicators" -LogFileName $LogFileName -Severity Information
                Write-Log -Message "Deleted $indicatorsDeleted indicators" -LogFileName $LogFileName -Severity Information

                if ($indicatorsFetched -eq $indicatorsDeleted) {                
                    Write-Log -Message "Successfully deleted all indicators in workspace = $WorkspaceName with Source = $Source" -LogFileName $LogFileName -Severity Information
                }
                else {                
                    Write-Log -Message "Please re-run the script to delete remaining indicators or reach out to the script owners if you're facing any issues." -LogFileName $LogFileName -Severity Information
                }
                break
            }
        }

        $indicatorsFound = $true    
        Write-Log -Message "Successfully fetched $($indicatorList.Count) indicators for source = $Source. Deleting ..." -LogFileName $LogFileName -Severity Information
    
        $indicatorsFetched += $indicatorList.Count

        try {
            if ($indicatorList.Count -le 20) {
                $indicatorChunks = @($indicatorList)
            }
            else {
                $indicatorChunks = $indicatorList | Split-Collection -Count 20
            }
            
            foreach ($indicatorChunk in $indicatorChunks) {
                $bulkDeletePayload = @{"requests" = (New-Object System.Collections.ArrayList) }
                $totalDels = $indicatorChunk.Count
                foreach ($indicator in $indicatorChunk) {
                    $indicatorName = $($indicator).name
                    Write-Log -Message "Preparing indicator with ID: ($indicatorName) for deleteion" -LogFileName $LogFileName -Severity Information
                    $deleteIndicatorUri = $ThreatIndicatorsApi + $indicator.name + "?$SECURITY_INSIGHTS_API_VERSION"
                    $bulkDeleteRequest = @{"url" = $deleteIndicatorUri; "httpMethod" = "DELETE" }
                    $bulkDeletePayload.requests.Add($bulkDeleteRequest)
                }
                $bulkDeletePayloadJson = $bulkDeletePayload | ConvertTo-Json
                $response = Invoke-AzRestMethod -Uri $bulkApi -Payload $bulkDeletePayloadJson -Method POST
                $bulkDeletePayload = @{"requests" = (New-Object System.Collections.ArrayList) }
                if ($response -eq $null -or $response.StatusCode -ne 200) {                
                    Write-Log -Message "Failed to bulk delete indicators. Status Code = $($response.StatusCode)" -LogFileName $LogFileName -Severity Information
                    Write-Log -Message $response.Content -LogFileName $LogFileName -Severity Information
                    break
                }
                $responseContent = $response.Content | ConvertFrom-Json
                $delFailures = 0
                $index = 0
                $failure = $false
                foreach ($responseItem in $responseContent.responses) {
                    if ($responseItem.httpStatusCode -ne "200") {
                        $failure = $true
                        $indicatorId = $indicatorChunk[$index].name
                        $delFailures++
                        Write-Log -Message "Failed to delete indicator with ID ($indicatorId). Status Code = $($responseItem.httpStatusCode)" -LogFileName $LogFileName -Severity Information
                    }
                    $index++
                }
                $totalDels -= $delFailures
                $indicatorsDeleted += $totalDels
                if ($failure) {
                    Write-Log -Message "Successfully deleted $($totalDels) and failed $($delFailures) indicators" -LogFileName $LogFileName -Severity Information
                    continue
                }
                Write-Log -Message "Successfully deleted all $($indicatorChunk.Count) indicators" -LogFileName $LogFileName -Severity Information
            }
        }
        catch {
            Write-Log -Message "Failed to delete indicator info: $($_.Exception)" -LogFileName $LogFileName -Severity Information        
        }
    }
}