Private/Core/Get-ThreatScore.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-ThreatScore { [CmdletBinding()] param( [Parameter(Mandatory)] [PSCustomObject]$Profile, [hashtable]$Weights ) # Default weights if (-not $Weights) { $Weights = @{ knownAttackerIp = 100 reauthFromCloud = 60 impossibleTravel = 70 riskyAction = 50 riskyActionFromCloud = 30 # bonus on top of riskyAction concurrentSessions = 45 suspiciousCountry = 40 bruteForceAttempt = 20 bruteForceSuccess = 55 # replaces attempt weight when success follows userAgentAnomaly = 30 oauthFromCloud = 25 afterHoursLogin = 15 cloudLoginsOnly = 15 newDevice = 10 newDeviceFromCloud = 35 # replaces newDevice when from cloud IP # Phase 4.1: Google Workspace expanded monitoring signals adminPrivilegeEscalation = 60 emailForwardingRule = 45 driveExternalSharing = 25 bulkFileDownload = 40 highRiskOAuthApp = 55 userSuspension = 20 twoSvDisablement = 50 domainWideDelegation = 80 workspaceSettingChange = 35 } } $score = 0.0 $indicators = [System.Collections.Generic.List[string]]::new() # Known attacker IPs — strongest signal if ($Profile.KnownAttackerIpLogins.Count -gt 0) { $n = $Profile.KnownAttackerIpLogins.Count $score += $Weights.knownAttackerIp $uniqueIps = @($Profile.KnownAttackerIpLogins | ForEach-Object { $_.IpAddress } | Sort-Object -Unique) $indicators.Add( "KNOWN ATTACKER IP - $n login(s) from $($uniqueIps.Count) known attacker IP(s): $($uniqueIps -join ', ')" ) } # Reauth from cloud IP — exact attack fingerprint if ($Profile.ReauthFromCloud.Count -gt 0) { $n = $Profile.ReauthFromCloud.Count $score += $Weights.reauthFromCloud $indicators.Add( "REAUTH FROM CLOUD - $n reauth login(s) from cloud provider IPs (matches attack pattern)" ) } # Risky sensitive actions if ($Profile.RiskyActions.Count -gt 0) { $n = $Profile.RiskyActions.Count $score += $Weights.riskyAction $cloudRisky = @($Profile.RiskyActions | Where-Object { $_.IpClass -and ($_.IpClass -eq 'known_attacker' -or $script:CloudProviderClasses.Contains($_.IpClass)) }) if ($cloudRisky.Count -gt 0) { $score += $Weights.riskyActionFromCloud $indicators.Add( "RISKY ACTION FROM CLOUD IP - $($cloudRisky.Count) risky sensitive action(s) from cloud/hosting IPs" ) } else { $indicators.Add( "RISKY SENSITIVE ACTION - $n risky action(s) allowed" ) } } # Suspicious country logins if ($Profile.SuspiciousCountryLogins.Count -gt 0) { $n = $Profile.SuspiciousCountryLogins.Count $score += $Weights.suspiciousCountry $countries = @($Profile.SuspiciousCountryLogins | ForEach-Object { $_.GeoCountry } | Sort-Object -Unique) $countryDisplay = $countries | ForEach-Object { $name = $script:SuspiciousCountries.displayNames.$_ if ($name) { "$name ($_)" } else { $_ } } $indicators.Add( "SUSPICIOUS COUNTRY LOGIN - $n login(s) from $($countryDisplay -join ', ')" ) } # OAuth grants from cloud IPs if ($Profile.SuspiciousOAuthGrants.Count -gt 0) { $n = $Profile.SuspiciousOAuthGrants.Count $score += $Weights.oauthFromCloud $apps = @($Profile.SuspiciousOAuthGrants | ForEach-Object { $_.Params.app_name } | Where-Object { $_ } | Sort-Object -Unique) $appList = if ($apps.Count -gt 0) { $apps -join ', ' } else { 'unknown' } $indicators.Add( "OAUTH FROM CLOUD IP - $n OAuth grant(s) from cloud IPs: $appList" ) } # Cloud IP logins without other strong signals if ($Profile.CloudIpLogins.Count -gt 0 -and $Profile.ReauthFromCloud.Count -eq 0 -and $Profile.KnownAttackerIpLogins.Count -eq 0) { $n = $Profile.CloudIpLogins.Count if ($n -ge 3) { $score += $Weights.cloudLoginsOnly $indicators.Add( "CLOUD IP LOGINS - $n login(s) from cloud/hosting provider IPs" ) } } # Impossible travel if ($Profile.ImpossibleTravel.Count -gt 0) { $n = $Profile.ImpossibleTravel.Count $score += $Weights.impossibleTravel $trip = $Profile.ImpossibleTravel[0] $indicators.Add( "IMPOSSIBLE TRAVEL - $n instance(s), e.g. $($trip.FromCountry) to $($trip.ToCountry) ($($trip.DistanceKm) km in $($trip.TimeDiffHours)h)" ) } # Concurrent sessions if ($Profile.ConcurrentSessions.Count -gt 0) { $n = $Profile.ConcurrentSessions.Count $score += $Weights.concurrentSessions $maxIps = ($Profile.ConcurrentSessions | Sort-Object IpCount -Descending | Select-Object -First 1).IpCount $indicators.Add( "CONCURRENT SESSIONS - $n window(s) with multiple IPs (max $maxIps IPs simultaneously)" ) } # User agent anomalies if ($Profile.UserAgentAnomalies.Count -gt 0) { $n = $Profile.UserAgentAnomalies.Count $score += $Weights.userAgentAnomaly $labels = @($Profile.UserAgentAnomalies | ForEach-Object { $_.MatchLabel } | Sort-Object -Unique) $indicators.Add( "USER AGENT ANOMALY - $n suspicious client(s): $($labels -join ', ')" ) } # Brute force if ($Profile.BruteForce -and $Profile.BruteForce.Detected) { $bf = $Profile.BruteForce if ($bf.SuccessAfter) { $score += $Weights.bruteForceSuccess $indicators.Add( "BRUTE FORCE SUCCESS - $($bf.FailureCount) failures followed by successful login from $($bf.AttackingIps.Count) IP(s)" ) } else { $score += $Weights.bruteForceAttempt $indicators.Add( "BRUTE FORCE ATTEMPT - $($bf.FailureCount) login failures in $([Math]::Round($bf.FailureWindow.Duration.TotalMinutes, 1)) min from $($bf.AttackingIps.Count) IP(s)" ) } } # After-hours logins if ($Profile.AfterHoursLogins.Count -gt 0) { $n = $Profile.AfterHoursLogins.Count $score += $Weights.afterHoursLogin $weekendCount = @($Profile.AfterHoursLogins | Where-Object { $_.Reason -match 'Weekend|non-business' }).Count $lateCount = $n - $weekendCount $detail = @() if ($lateCount -gt 0) { $detail += "$lateCount outside hours" } if ($weekendCount -gt 0) { $detail += "$weekendCount weekend" } $indicators.Add( "AFTER HOURS LOGIN - $n login(s) outside business hours ($($detail -join ', '))" ) } # New devices if ($Profile.NewDevices.Count -gt 0) { $n = $Profile.NewDevices.Count $cloudDevices = @($Profile.NewDevices | Where-Object { $_.IsCloudIp }) if ($cloudDevices.Count -gt 0) { $score += $Weights.newDeviceFromCloud $indicators.Add( "NEW DEVICE FROM CLOUD IP - $($cloudDevices.Count) first-seen device(s) from cloud/hosting IPs" ) } else { $score += $Weights.newDevice $indicators.Add( "NEW DEVICE - $n first-seen device(s)" ) } } # --- Phase 4.1: Expanded Google Workspace monitoring signals --- # Admin privilege escalation if ($Profile.PSObject.Properties['AdminPrivilegeEscalations'] -and $Profile.AdminPrivilegeEscalations.Count -gt 0) { $n = $Profile.AdminPrivilegeEscalations.Count $score += $Weights.adminPrivilegeEscalation $roles = @($Profile.AdminPrivilegeEscalations | ForEach-Object { $_.RoleName } | Sort-Object -Unique) $indicators.Add( "ADMIN PRIVILEGE ESCALATION - $n admin role assignment(s): $($roles -join ', ')" ) } # Email forwarding rule creation if ($Profile.PSObject.Properties['EmailForwardingRules'] -and $Profile.EmailForwardingRules.Count -gt 0) { $n = $Profile.EmailForwardingRules.Count $score += $Weights.emailForwardingRule $destinations = @($Profile.EmailForwardingRules | ForEach-Object { $_.ForwardTo } | Where-Object { $_ } | Sort-Object -Unique) $destDisplay = if ($destinations.Count -gt 0) { $destinations -join ', ' } else { 'unknown' } $indicators.Add( "EMAIL FORWARDING RULE - $n forwarding rule(s) created to: $destDisplay" ) } # Drive external sharing if ($Profile.PSObject.Properties['DriveExternalShares'] -and $Profile.DriveExternalShares.Count -gt 0) { $n = $Profile.DriveExternalShares.Count $score += $Weights.driveExternalSharing $indicators.Add( "DRIVE EXTERNAL SHARING - $n file(s) shared externally" ) } # Bulk file download if ($Profile.PSObject.Properties['BulkFileDownloads'] -and $Profile.BulkFileDownloads.Count -gt 0) { $n = $Profile.BulkFileDownloads.Count $maxCount = ($Profile.BulkFileDownloads | Sort-Object EventCount -Descending | Select-Object -First 1).EventCount $score += $Weights.bulkFileDownload $indicators.Add( "BULK FILE DOWNLOAD - $n burst(s) detected, max $maxCount downloads in window" ) } # High-risk OAuth app if ($Profile.PSObject.Properties['HighRiskOAuthApps'] -and $Profile.HighRiskOAuthApps.Count -gt 0) { $n = $Profile.HighRiskOAuthApps.Count $score += $Weights.highRiskOAuthApp $apps = @($Profile.HighRiskOAuthApps | ForEach-Object { $_.AppName } | Where-Object { $_ } | Sort-Object -Unique) $appList = if ($apps.Count -gt 0) { $apps -join ', ' } else { 'unknown' } $indicators.Add( "HIGH-RISK OAUTH APP - $n risky OAuth app grant(s): $appList" ) } # User suspension/deletion (info signal) if ($Profile.PSObject.Properties['UserSuspensions'] -and $Profile.UserSuspensions.Count -gt 0) { $n = $Profile.UserSuspensions.Count $score += $Weights.userSuspension $targets = @($Profile.UserSuspensions | ForEach-Object { $_.TargetUser } | Sort-Object -Unique) $indicators.Add( "USER SUSPENSION/DELETION - $n user(s) suspended or deleted: $($targets -join ', ')" ) } # 2SV disablement if ($Profile.PSObject.Properties['TwoSvDisablements'] -and $Profile.TwoSvDisablements.Count -gt 0) { $n = $Profile.TwoSvDisablements.Count $adminActions = @($Profile.TwoSvDisablements | Where-Object { $_.IsAdminAction }) if ($adminActions.Count -gt 0) { $score += $Weights.twoSvDisablement $targets = @($adminActions | ForEach-Object { $_.TargetUser } | Sort-Object -Unique) $indicators.Add( "2SV DISABLEMENT - $($adminActions.Count) admin-initiated 2SV disable(s) for: $($targets -join ', ')" ) } } # Domain-wide delegation if ($Profile.PSObject.Properties['DomainWideDelegations'] -and $Profile.DomainWideDelegations.Count -gt 0) { $n = $Profile.DomainWideDelegations.Count $score += $Weights.domainWideDelegation $dangerous = @($Profile.DomainWideDelegations | Where-Object { $_.HasDangerousScope }) $detail = if ($dangerous.Count -gt 0) { "$($dangerous.Count) with dangerous scopes" } else { 'API client access grants' } $indicators.Add( "DOMAIN-WIDE DELEGATION - $n delegation grant(s): $detail" ) } # Workspace setting changes if ($Profile.PSObject.Properties['WorkspaceSettingChanges'] -and $Profile.WorkspaceSettingChanges.Count -gt 0) { $n = $Profile.WorkspaceSettingChanges.Count $highSev = @($Profile.WorkspaceSettingChanges | Where-Object { $_.IsHighSeverity }) if ($highSev.Count -gt 0) { $score += $Weights.workspaceSettingChange $settings = @($highSev | ForEach-Object { $_.SettingName } | Sort-Object -Unique | Select-Object -First 3) $indicators.Add( "WORKSPACE SETTING CHANGE - $($highSev.Count) security-relevant setting change(s): $($settings -join ', ')" ) } } # Known compromised baseline tag if ($Profile.IsKnownCompromised) { $indicators.Insert(0, 'CONFIRMED COMPROMISED (known victim)') if ($score -lt 100) { $score = [Math]::Max($score, 100) } } # Assign threat level $threatLevel = switch ($true) { ($score -ge 100) { 'CRITICAL'; break } ($score -ge 60) { 'HIGH'; break } ($score -ge 30) { 'MEDIUM'; break } ($score -gt 0) { 'LOW'; break } default { 'Clean' } } $Profile.ThreatScore = $score $Profile.ThreatLevel = $threatLevel $Profile.Indicators = @($indicators) return $Profile } |