Private/Core/Match-ThreatActorProfile.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 Match-ThreatActorProfile { <# .SYNOPSIS Matches detected threat indicators to known threat actor profiles. .DESCRIPTION Compares a user's threat indicators and score against threat actor profiles in ThreatActorProfiles.json. Returns matching profiles with confidence levels. .PARAMETER ThreatProfile A flagged user threat object with ThreatScore, ThreatLevel, and Indicators properties. .PARAMETER ActorProfiles Pre-loaded threat actor profile data. If not provided, loads from Data/ThreatActorProfiles.json. #> [CmdletBinding()] param( [Parameter(Mandatory)] [PSCustomObject]$ThreatProfile, [hashtable]$ActorProfiles ) if (-not $ActorProfiles) { $profilePath = Join-Path $PSScriptRoot '../../Data/ThreatActorProfiles.json' if (Test-Path $profilePath) { $ActorProfiles = Get-Content -Path $profilePath -Raw | ConvertFrom-Json -AsHashtable } else { Write-Warning "ThreatActorProfiles.json not found at $profilePath" return @() } } # Extract indicator keywords from the threat profile's indicator strings $indicatorKeywords = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $indicatorMap = @{ 'KNOWN ATTACKER IP' = 'knownAttackerIp' 'REAUTH FROM CLOUD' = 'reauthFromCloud' 'IMPOSSIBLE TRAVEL' = 'impossibleTravel' 'RISKY SENSITIVE ACTION' = 'riskyAction' 'RISKY ACTION FROM CLOUD' = 'riskyActionFromCloud' 'CONCURRENT SESSIONS' = 'concurrentSessions' 'SUSPICIOUS COUNTRY' = 'suspiciousCountry' 'BRUTE FORCE ATTEMPT' = 'bruteForceAttempt' 'BRUTE FORCE SUCCESS' = 'bruteForceSuccess' 'USER AGENT ANOMALY' = 'userAgentAnomaly' 'OAUTH FROM CLOUD' = 'oauthFromCloud' 'AFTER HOURS LOGIN' = 'afterHoursLogin' 'CLOUD IP LOGINS' = 'cloudLoginsOnly' 'NEW DEVICE FROM CLOUD' = 'newDeviceFromCloud' 'NEW DEVICE' = 'newDevice' 'ADMIN PRIVILEGE ESCALATION' = 'adminPrivilegeEscalation' 'EMAIL FORWARDING RULE' = 'emailForwardingRule' 'DRIVE EXTERNAL SHARING' = 'driveExternalSharing' 'BULK FILE DOWNLOAD' = 'bulkFileDownload' 'HIGH-RISK OAUTH APP' = 'highRiskOAuthApp' 'USER SUSPENSION' = 'userSuspension' '2SV DISABLEMENT' = 'twoSvDisablement' 'DOMAIN-WIDE DELEGATION' = 'domainWideDelegation' 'WORKSPACE SETTING CHANGE' = 'workspaceSettingChange' } $indicators = @($ThreatProfile.Indicators ?? @()) foreach ($indicator in $indicators) { foreach ($mapKey in $indicatorMap.Keys) { if ($indicator -match [regex]::Escape($mapKey)) { $indicatorKeywords.Add($indicatorMap[$mapKey]) | Out-Null } } } $threatScore = [int]($ThreatProfile.ThreatScore ?? 0) $matches = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($actor in $ActorProfiles.profiles) { $criteria = $actor.matchCriteria if (-not $criteria) { continue } # Check minimum threat score $minScore = [int]($criteria.minThreatScore ?? 0) if ($threatScore -lt $minScore) { continue } # Check required indicators $requiredMet = $true $requiredMatched = 0 $requiredIndicators = @($criteria.requiredIndicators ?? @()) foreach ($req in $requiredIndicators) { if ($indicatorKeywords.Contains($req)) { $requiredMatched++ } else { $requiredMet = $false } } if (-not $requiredMet) { continue } # Check optional indicators $optionalMatched = 0 $optionalIndicators = @($criteria.optionalIndicators ?? @()) foreach ($opt in $optionalIndicators) { if ($indicatorKeywords.Contains($opt)) { $optionalMatched++ } } $minOptional = [int]($criteria.minOptionalMatch ?? 0) if ($optionalMatched -lt $minOptional) { continue } # Calculate confidence $totalIndicators = $requiredIndicators.Count + $optionalIndicators.Count $totalMatched = $requiredMatched + $optionalMatched $matchRatio = if ($totalIndicators -gt 0) { $totalMatched / $totalIndicators } else { 0 } $confidence = switch ($true) { ($matchRatio -ge 0.75) { 'High'; break } ($matchRatio -ge 0.50) { 'Medium'; break } default { 'Low' } } $matches.Add([PSCustomObject]@{ PSTypeName = 'PSGuerrilla.ThreatActorMatch' ActorId = $actor.id ActorName = $actor.name Description = $actor.description Sophistication = $actor.sophistication Motivation = $actor.motivation Confidence = $confidence MatchRatio = [Math]::Round($matchRatio, 2) RequiredMatched = $requiredMatched OptionalMatched = $optionalMatched TotalMatched = $totalMatched TotalIndicators = $totalIndicators MitreTechniques = @($actor.ttps.mitre ?? @()) TtpDescription = $actor.ttps.description ?? '' }) } return @($matches | Sort-Object -Property MatchRatio -Descending) } |