Private/EntraMonitor/Core/Get-EntraMonitorThreatScore.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-EntraMonitorThreatScore { [CmdletBinding()] param( [Parameter(Mandatory)] [PSCustomObject]$Profile, [hashtable]$Weights ) # Default weights if (-not $Weights) { $Weights = @{ entraRiskySignIn = 50 entraForeignCountry = 40 entraCloudIp = 35 entraVpnTor = 45 entraImpossibleTravel = 70 entraUnfamiliarSignIn = 30 entraAnonymousIp = 50 entraMalwareIp = 80 entraLeakedCredential = 90 entraPasswordSpray = 75 entraAnomalousToken = 65 entraPrivilegedRole = 50 entraGlobalAdmin = 90 entraCAPolicyChange = 60 entraServicePrincipalCred = 70 entraAppPermission = 55 entraFederationChange = 85 entraGuestInvitation = 15 entraAdminUnitChange = 30 entraAuthMethodChange = 40 entraAuditLogGap = 60 entraTenantSettingChange = 45 entraSubscriptionPermChange = 50 } } $score = 0.0 $indicators = [System.Collections.Generic.List[string]]::new() # Leaked credentials — strongest identity signal if ($Profile.LeakedCredentials.Count -gt 0) { $n = $Profile.LeakedCredentials.Count $score += $Weights.entraLeakedCredential $indicators.Add( "LEAKED CREDENTIAL - $n credential leak detection(s) from Entra ID Protection" ) } # Global Admin assignment — critical privilege escalation if ($Profile.GlobalAdminAssignments.Count -gt 0) { $n = $Profile.GlobalAdminAssignments.Count $score += $Weights.entraGlobalAdmin $targets = @($Profile.GlobalAdminAssignments | ForEach-Object { $_.TargetUser } | Where-Object { $_ } | Sort-Object -Unique) $targetDisplay = if ($targets.Count -gt 0) { $targets -join ', ' } else { 'unknown' } $indicators.Add( "GLOBAL ADMIN ASSIGNMENT - $n Global Administrator role assignment(s) to: $targetDisplay" ) } # Federation changes — domain trust manipulation if ($Profile.FederationChanges.Count -gt 0) { $n = $Profile.FederationChanges.Count $score += $Weights.entraFederationChange $domains = @($Profile.FederationChanges | ForEach-Object { $_.DomainName } | Where-Object { $_ } | Sort-Object -Unique) $domainDisplay = if ($domains.Count -gt 0) { $domains -join ', ' } else { 'tenant' } $indicators.Add( "FEDERATION CHANGE - $n federation/domain trust modification(s) affecting: $domainDisplay" ) } # Malware IP sign-ins if ($Profile.MalwareIpSignIns.Count -gt 0) { $n = $Profile.MalwareIpSignIns.Count $score += $Weights.entraMalwareIp $ips = @($Profile.MalwareIpSignIns | ForEach-Object { $_.IpAddress } | Where-Object { $_ } | Sort-Object -Unique) $indicators.Add( "MALWARE IP - $n sign-in(s) from known malicious IP(s): $($ips -join ', ')" ) } # Password spray if ($Profile.PasswordSprayDetections.Count -gt 0) { $n = $Profile.PasswordSprayDetections.Count $score += $Weights.entraPasswordSpray $indicators.Add( "PASSWORD SPRAY - $n password spray detection(s) from Entra ID Protection" ) } # Impossible travel if ($Profile.ImpossibleTravelDetections.Count -gt 0) { $n = $Profile.ImpossibleTravelDetections.Count $score += $Weights.entraImpossibleTravel $locations = @($Profile.ImpossibleTravelDetections | ForEach-Object { $loc = $_.Location if ($loc.Country) { $loc.Country } elseif ($loc.City) { $loc.City } else { 'unknown' } } | Sort-Object -Unique) $indicators.Add( "IMPOSSIBLE TRAVEL - $n impossible travel detection(s) involving: $($locations -join ', ')" ) } # Service principal credential changes if ($Profile.ServicePrincipalCredChanges.Count -gt 0) { $n = $Profile.ServicePrincipalCredChanges.Count $score += $Weights.entraServicePrincipalCred $apps = @($Profile.ServicePrincipalCredChanges | ForEach-Object { $_.AppName } | Where-Object { $_ } | Sort-Object -Unique) $appDisplay = if ($apps.Count -gt 0) { $apps -join ', ' } else { 'unknown' } $indicators.Add( "SERVICE PRINCIPAL CREDENTIAL - $n credential addition/change(s) on: $appDisplay" ) } # Anomalous token if ($Profile.AnomalousTokenDetections.Count -gt 0) { $n = $Profile.AnomalousTokenDetections.Count $score += $Weights.entraAnomalousToken $indicators.Add( "ANOMALOUS TOKEN - $n anomalous token detection(s) from Entra ID Protection" ) } # Conditional Access policy changes if ($Profile.CAPolicyChanges.Count -gt 0) { $n = $Profile.CAPolicyChanges.Count $score += $Weights.entraCAPolicyChange $disabling = @($Profile.CAPolicyChanges | Where-Object { $_.IsDisabling }) $detail = if ($disabling.Count -gt 0) { "$($disabling.Count) disabled/deleted" } else { "$n modified" } $policies = @($Profile.CAPolicyChanges | ForEach-Object { $_.PolicyName } | Where-Object { $_ } | Sort-Object -Unique | Select-Object -First 3) $policyDisplay = if ($policies.Count -gt 0) { $policies -join ', ' } else { 'unknown' } $indicators.Add( "CA POLICY CHANGE - $detail, policies: $policyDisplay" ) } # App permission grants if ($Profile.AppPermissionGrants.Count -gt 0) { $n = $Profile.AppPermissionGrants.Count $score += $Weights.entraAppPermission $highPriv = @($Profile.AppPermissionGrants | Where-Object { $_.IsHighPrivilege }) $detail = if ($highPriv.Count -gt 0) { "$($highPriv.Count) high-privilege" } else { "$n granted" } $apps = @($Profile.AppPermissionGrants | ForEach-Object { $_.AppName } | Where-Object { $_ } | Sort-Object -Unique | Select-Object -First 3) $appDisplay = if ($apps.Count -gt 0) { $apps -join ', ' } else { 'unknown' } $indicators.Add( "APP PERMISSION GRANT - $detail permission grant(s) to: $appDisplay" ) } # Risky sign-ins if ($Profile.RiskySignIns.Count -gt 0) { $n = $Profile.RiskySignIns.Count $score += $Weights.entraRiskySignIn $highRisk = @($Profile.RiskySignIns | Where-Object { $_.RiskLevel -eq 'high' }) $detail = if ($highRisk.Count -gt 0) { "$($highRisk.Count) high-risk" } else { "$n medium-risk" } $indicators.Add( "RISKY SIGN-IN - $detail sign-in(s) flagged by Entra ID Protection" ) } # Anonymous IP sign-ins if ($Profile.AnonymousIpSignIns.Count -gt 0) { $n = $Profile.AnonymousIpSignIns.Count $score += $Weights.entraAnonymousIp $indicators.Add( "ANONYMOUS IP - $n sign-in(s) from anonymized/anonymous IP addresses" ) } # Privileged role changes if ($Profile.PrivilegedRoleChanges.Count -gt 0) { $n = $Profile.PrivilegedRoleChanges.Count $score += $Weights.entraPrivilegedRole $roles = @($Profile.PrivilegedRoleChanges | ForEach-Object { $_.RoleName } | Where-Object { $_ } | Sort-Object -Unique | Select-Object -First 3) $roleDisplay = if ($roles.Count -gt 0) { $roles -join ', ' } else { 'unknown' } $indicators.Add( "PRIVILEGED ROLE CHANGE - $n role assignment change(s): $roleDisplay" ) } # Subscription permission changes if ($Profile.SubscriptionPermChanges.Count -gt 0) { $n = $Profile.SubscriptionPermChanges.Count $sensitive = @($Profile.SubscriptionPermChanges | Where-Object { $_.IsSensitive }) if ($sensitive.Count -gt 0) { $score += $Weights.entraSubscriptionPermChange $indicators.Add( "SUBSCRIPTION PERMISSION - $($sensitive.Count) sensitive Azure RBAC/ownership change(s)" ) } } # VPN/Tor sign-ins if ($Profile.VpnTorSignIns.Count -gt 0) { $n = $Profile.VpnTorSignIns.Count $score += $Weights.entraVpnTor $classes = @($Profile.VpnTorSignIns | ForEach-Object { $_.IpClass } | Sort-Object -Unique) $indicators.Add( "VPN/TOR SIGN-IN - $n sign-in(s) from $($classes -join ', ') services" ) } # Tenant setting changes if ($Profile.TenantSettingChanges.Count -gt 0) { $highSev = @($Profile.TenantSettingChanges | Where-Object { $_.IsHighSeverity }) if ($highSev.Count -gt 0) { $score += $Weights.entraTenantSettingChange $settings = @($highSev | ForEach-Object { $_.SettingName } | Where-Object { $_ } | Sort-Object -Unique | Select-Object -First 3) $settingDisplay = if ($settings.Count -gt 0) { $settings -join ', ' } else { 'unknown' } $indicators.Add( "TENANT SETTING CHANGE - $($highSev.Count) security-relevant tenant setting change(s): $settingDisplay" ) } } # Auth method changes if ($Profile.AuthMethodChanges.Count -gt 0) { $adminActions = @($Profile.AuthMethodChanges | Where-Object { $_.IsAdminAction }) if ($adminActions.Count -gt 0) { $score += $Weights.entraAuthMethodChange $targets = @($adminActions | ForEach-Object { $_.TargetUser } | Where-Object { $_ } | Sort-Object -Unique) $targetDisplay = if ($targets.Count -gt 0) { $targets -join ', ' } else { 'unknown' } $indicators.Add( "AUTH METHOD CHANGE - $($adminActions.Count) admin-initiated auth method change(s) for: $targetDisplay" ) } } # Foreign country sign-ins if ($Profile.ForeignCountrySignIns.Count -gt 0) { $n = $Profile.ForeignCountrySignIns.Count $score += $Weights.entraForeignCountry $countries = @($Profile.ForeignCountrySignIns | ForEach-Object { $_.GeoCountry } | Sort-Object -Unique) $countryDisplay = $countries | ForEach-Object { $name = if ($script:SuspiciousCountries) { $script:SuspiciousCountries.displayNames.$_ } else { $null } if ($name) { "$name ($_)" } else { $_ } } $indicators.Add( "FOREIGN COUNTRY SIGN-IN - $n sign-in(s) from suspicious countries: $($countryDisplay -join ', ')" ) } # Cloud IP sign-ins if ($Profile.CloudIpSignIns.Count -gt 0) { $n = $Profile.CloudIpSignIns.Count $score += $Weights.entraCloudIp $uniqueIps = @($Profile.CloudIpSignIns | ForEach-Object { $_.IpAddress } | Sort-Object -Unique) $indicators.Add( "CLOUD IP SIGN-IN - $n sign-in(s) from $($uniqueIps.Count) cloud/hosting provider IP(s)" ) } # Unfamiliar sign-ins if ($Profile.UnfamiliarSignIns.Count -gt 0) { $n = $Profile.UnfamiliarSignIns.Count $score += $Weights.entraUnfamiliarSignIn $indicators.Add( "UNFAMILIAR SIGN-IN - $n unfamiliar feature detection(s) from Entra ID Protection" ) } # Admin unit changes if ($Profile.AdminUnitChanges.Count -gt 0) { $n = $Profile.AdminUnitChanges.Count $score += $Weights.entraAdminUnitChange $units = @($Profile.AdminUnitChanges | ForEach-Object { $_.AdminUnitName } | Where-Object { $_ } | Sort-Object -Unique | Select-Object -First 3) $unitDisplay = if ($units.Count -gt 0) { $units -join ', ' } else { 'unknown' } $indicators.Add( "ADMIN UNIT CHANGE - $n administrative unit change(s): $unitDisplay" ) } # Audit log gaps if ($Profile.AuditLogGaps.Count -gt 0) { $n = $Profile.AuditLogGaps.Count $score += $Weights.entraAuditLogGap $maxGap = ($Profile.AuditLogGaps | Sort-Object GapHours -Descending | Select-Object -First 1).GapHours $indicators.Add( "AUDIT LOG GAP - $n gap(s) detected in audit logs, max gap: ${maxGap}h" ) } # Guest invitations — low signal if ($Profile.GuestInvitations.Count -gt 0) { $n = $Profile.GuestInvitations.Count $score += $Weights.entraGuestInvitation $guests = @($Profile.GuestInvitations | ForEach-Object { $_.InvitedEmail } | Where-Object { $_ } | Sort-Object -Unique | Select-Object -First 3) $guestDisplay = if ($guests.Count -gt 0) { $guests -join ', ' } else { 'unknown' } $indicators.Add( "GUEST INVITATION - $n external user invitation(s): $guestDisplay" ) } # 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 } |