Private/Core/Get-AlertDeduplication.ps1
|
# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0 # https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/ # AI/LLM use: see AI-USAGE.md for required attribution function Get-AlertDeduplication { <# .SYNOPSIS Checks if a threat alert has already been sent within the suppression window. .DESCRIPTION Maintains a history of sent alerts in a JSON file. Each alert is identified by a dedup key (SHA256 hash of email + threat level + sorted indicators). Returns whether the alert should be suppressed and manages the history file. .PARAMETER Threat The threat object to check for deduplication. .PARAMETER SuppressionHours Number of hours to suppress duplicate alerts. Default: 24. .PARAMETER HistoryPath Override path to the alert history file. #> [CmdletBinding()] param( [Parameter(Mandatory)] [PSCustomObject]$Threat, [int]$SuppressionHours = 24, [string]$HistoryPath ) $dataDir = Get-PSGuerrillaDataRoot $path = if ($HistoryPath) { $HistoryPath } else { Join-Path $dataDir 'alert-history.json' } # Build dedup key: SHA256 of email:threatLevel:sortedIndicators $email = $Threat.Email ?? $Threat.UserPrincipalName ?? 'unknown' $level = $Threat.ThreatLevel ?? 'UNKNOWN' $sortedIndicators = ($Threat.Indicators | Sort-Object) -join '|' $keyMaterial = "${email}:${level}:${sortedIndicators}" $sha256 = [System.Security.Cryptography.SHA256]::Create() $hashBytes = $sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($keyMaterial)) $dedupKey = [System.BitConverter]::ToString($hashBytes).Replace('-', '').ToLower() $sha256.Dispose() # Load history $history = @{} if (Test-Path $path) { try { $history = Get-Content -Path $path -Raw | ConvertFrom-Json -AsHashtable if ($history -isnot [hashtable]) { $history = @{} } } catch { Write-Verbose "Alert history corrupted, resetting: $_" $history = @{} } } # Prune expired entries $cutoff = [datetime]::UtcNow.AddHours(-$SuppressionHours) $keysToRemove = @() foreach ($key in $history.Keys) { $entryTime = [datetime]::MinValue try { $entryTime = [datetime]::Parse($history[$key].timestamp) } catch { } if ($entryTime -lt $cutoff) { $keysToRemove += $key } } foreach ($key in $keysToRemove) { $history.Remove($key) } # Check if this alert is suppressed $isSuppressed = $false if ($history.ContainsKey($dedupKey)) { $existingTime = [datetime]::MinValue try { $existingTime = [datetime]::Parse($history[$dedupKey].timestamp) } catch { } if ($existingTime -ge $cutoff) { $isSuppressed = $true } } return [PSCustomObject]@{ DedupKey = $dedupKey IsSuppressed = $isSuppressed Email = $email ThreatLevel = $level HistoryPath = $path } } function Save-AlertHistory { <# .SYNOPSIS Records an alert in the dedup history after it has been successfully sent. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$DedupKey, [Parameter(Mandatory)] [string]$Email, [Parameter(Mandatory)] [string]$ThreatLevel, [string]$HistoryPath ) $dataDir = Get-PSGuerrillaDataRoot $path = if ($HistoryPath) { $HistoryPath } else { Join-Path $dataDir 'alert-history.json' } # Load existing history $history = @{} if (Test-Path $path) { try { $history = Get-Content -Path $path -Raw | ConvertFrom-Json -AsHashtable if ($history -isnot [hashtable]) { $history = @{} } } catch { $history = @{} } } $history[$DedupKey] = @{ email = $Email threatLevel = $ThreatLevel timestamp = [datetime]::UtcNow.ToString('o') } if (-not (Test-Path $dataDir)) { New-Item -Path $dataDir -ItemType Directory -Force | Out-Null } $history | ConvertTo-Json -Depth 5 | Set-Content -Path $path -Encoding UTF8 } |