Notification history

To avoid the creation of multiple planner tasks for the same vulnerability, I’ve created the notification history with an unique ID for each vulnerability. The ID is created by adding the Hostname, CveID (from NIST), SoftwareVendor and SoftwareName.

An example; The server “server235” is a windows server and has the vulnerability “CVE-2025-12345”, then the ID would be “server235-microsoft-windows_server_2022-cve-2025-12345”.

That ID can be saved in a storage account table or a sql table and then be retrieved with the script for exluding these in the Defender API request. The new results can be added after a notification.

Getting the excludes

SQL Table

# Convert-TableToKQLString is from my custom PowerShell Module
$sqlServerName = "yourDB.database.windows.net"
$sqlDatabaseName = "db01"
$sqlSelectQuery4excludes = "SELECT uuid FROM [dbo].[excludes]" # This is an SQL View that joins all the fields
$sqlAccessToken = (Get-AzAccessToken -ResourceUrl https://database.windows.net).Token

$excludes = Invoke-Sqlcmd -ServerInstance $sqlServerName -Database $sqlDatabaseName -Query $sqlSelectQuery4excludes -AccessToken $sqlAccessToken
$excludesString = Convert-TableToKQLString -table $excludes -fields @('uuid')

$kqlQuery = @"
let excludes = datatable(HostSrc: string, contact: string, oe: string)
[
    $excludesString
];
DeviceTvmSoftwareVulnerabilities
| join kind=leftanti (excludes) on EntryUUID
...

Custom Table

Adding excludes to SQL Table

$sqlServerName = "yourDB.database.windows.net"
$sqlDatabaseName = "db01"
$sqlSelectQuery4excludes = "SELECT uuid FROM [dbo].[excludes]" # This is an SQL View that joins all the fields
$sqlAccessToken = (Get-AzAccessToken -ResourceUrl https://database.windows.net).Token

foreach ($entry in $batch) {
    $excludes = [PSCustomObject]@{
        CveId           = $entry.CveId
        SoftwareVendor  = $entry.SoftwareVendor
        SoftwareName    = $entry.SoftwareName
        SoftwareVersion = $entry.SoftwareVersion
        cvssScore       = $entry.cvssScore
    }
    $cveId = $excludes.CveId
    $softwareVendor = $excludes.SoftwareVendor
    $softwareName = $excludes.SoftwareName
    $softwareVersion = $excludes.SoftwareVersion
    $cvssScore = $excludes.cvssScore

    $sqlInsertExcludes = "INSERT INTO [dbo].[excludes] (hostname, CveId, SoftwareVendor, SoftwareName, SoftwareVersion, cvssScore) VALUES ('$hostName', '$cveId', '$softwareVendor', '$softwareName', '$softwareVersion' , '$cvssScore' )"
    Invoke-Sqlcmd -ServerInstance $sqlServerName -Database $sqlDatabaseName -Query $sqlInsertExcludes -AccessToken $sqlAccessToken
}

Adding excludes to Custom Table

$excludesBatch = @()
$diskPathsBatch = @()

foreach ($entry in $batch) {
    $cveArray = $entry.CvesArray
    foreach ($cveObj in $cveArray) {

        # Log all notified vulnerabilities
        $exclude = [PSCustomObject]@{
            HostName        = $entry.HostName
            CveId           = $cveObj.CveId
            CvssScore       = $cveObj.CvssScore
            SoftwareVendor  = $entry.SoftwareVendor
            SoftwareName    = $entry.SoftwareName
            SoftwareVersion = $entry.SoftwareVersion
            TaskId          = $taskId
            TimeGenerated   = $now # Not relevant, LOA overwrites it
        }

        $excludesBatch += $exclude
    }

    # Log all notified disk paths per planner task
    if (![string]::IsNullOrWhiteSpace($entry.DiskPathsString)) { 

        $diskpathsarray = [PSCustomObject]@{
            TaskId              = $taskId
            SoftwareVendor      = $entry.SoftwareVendor
            SoftwareName        = $entry.SoftwareName
            SoftwareVersion     = $entry.SoftwareVersion
            DiskPaths           = $entry.DiskPathsString
        }
        $diskPathsBatch += $diskpathsarray
    }
}

$excludesBatch = @($excludesBatch)
$stream = "Custom-SVNmaster2026_CL"
Invoke-ArrayIntoLogAnalytics -dce $endpoint -dcrimmutableid $dcrimmutableid -stream $stream -token $loatoken -dataArray $excludesBatch

if($diskPathsBatch -and $diskPathsBatch -ne $null ) {
    $diskPathsBatch = @($diskPathsBatch)
    $stream = "Custom-SVNPaths_CL"
    Invoke-ArrayIntoLogAnalytics -dce $endpoint -dcrimmutableid $dcrimmutableid -stream $stream -token $loatoken -dataArray $diskPathsBatch
}

“…” means that there’s more code that is needed. Please check the GitHub Repo for the full code!