Private/EntraMonitor/Core/New-EntraRiskProfile.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 New-EntraRiskProfile { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$UserPrincipalName, [hashtable[]]$SignInEvents = @(), [hashtable[]]$RiskDetections = @(), [hashtable[]]$AuditEvents = @(), [hashtable]$GeoData = @{}, [hashtable]$DetectionConfig = @{}, [hashtable]$DetectionFilter = @{} ) # Helper: check if a detection signal is enabled in the filter function Test-DetectionEnabled([string]$SignalKey) { if (-not $DetectionFilter -or $DetectionFilter.Count -eq 0) { return $true } return $DetectionFilter[$SignalKey] -ne $false } $profile = [PSCustomObject]@{ PSTypeName = 'PSGuerrilla.EntraRiskProfile' UserPrincipalName = $UserPrincipalName ThreatLevel = 'Clean' ThreatScore = 0.0 Indicators = @() # Sign-in based detections RiskySignIns = [System.Collections.Generic.List[PSCustomObject]]::new() ForeignCountrySignIns = [System.Collections.Generic.List[PSCustomObject]]::new() CloudIpSignIns = [System.Collections.Generic.List[PSCustomObject]]::new() VpnTorSignIns = [System.Collections.Generic.List[PSCustomObject]]::new() # Risk detection based detections ImpossibleTravelDetections = [System.Collections.Generic.List[PSCustomObject]]::new() UnfamiliarSignIns = [System.Collections.Generic.List[PSCustomObject]]::new() AnonymousIpSignIns = [System.Collections.Generic.List[PSCustomObject]]::new() MalwareIpSignIns = [System.Collections.Generic.List[PSCustomObject]]::new() LeakedCredentials = [System.Collections.Generic.List[PSCustomObject]]::new() PasswordSprayDetections = [System.Collections.Generic.List[PSCustomObject]]::new() AnomalousTokenDetections = [System.Collections.Generic.List[PSCustomObject]]::new() # Audit-based detections PrivilegedRoleChanges = [System.Collections.Generic.List[PSCustomObject]]::new() GlobalAdminAssignments = [System.Collections.Generic.List[PSCustomObject]]::new() CAPolicyChanges = [System.Collections.Generic.List[PSCustomObject]]::new() ServicePrincipalCredChanges = [System.Collections.Generic.List[PSCustomObject]]::new() AppPermissionGrants = [System.Collections.Generic.List[PSCustomObject]]::new() FederationChanges = [System.Collections.Generic.List[PSCustomObject]]::new() GuestInvitations = [System.Collections.Generic.List[PSCustomObject]]::new() AdminUnitChanges = [System.Collections.Generic.List[PSCustomObject]]::new() AuthMethodChanges = [System.Collections.Generic.List[PSCustomObject]]::new() AuditLogGaps = @() TenantSettingChanges = [System.Collections.Generic.List[PSCustomObject]]::new() SubscriptionPermChanges = [System.Collections.Generic.List[PSCustomObject]]::new() # Metadata TotalSignInEvents = $SignInEvents.Count TotalRiskDetections = $RiskDetections.Count TotalAuditEvents = $AuditEvents.Count IpClassifications = @{} } # --- Sign-in event analysis --- # Suspicious country set $suspiciousCountryCodes = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) if ($script:SuspiciousCountries -and $script:SuspiciousCountries.codes) { foreach ($code in $script:SuspiciousCountries.codes) { [void]$suspiciousCountryCodes.Add($code) } } foreach ($event in $SignInEvents) { $ip = $event.IpAddress if (-not $ip) { continue } $ipClass = Get-CloudIpClassification -IpAddress $ip $geoCountry = if ($GeoData.ContainsKey($ip) -and $GeoData[$ip]) { $GeoData[$ip].CountryCode } else { '' } # Track IP classifications if (-not $profile.IpClassifications.ContainsKey($ip)) { $profile.IpClassifications[$ip] = @{ Class = $ipClass Country = $geoCountry Events = [System.Collections.Generic.List[string]]::new() } } $profile.IpClassifications[$ip].Events.Add($event.AppDisplayName ?? 'sign-in') # Cloud IP sign-ins (any cloud provider or known attacker) $isCloudIp = $ipClass -and ($ipClass -eq 'known_attacker' -or ($script:CloudProviderClasses -and $script:CloudProviderClasses.Contains($ipClass))) if ((Test-DetectionEnabled 'cloudIpSignIns') -and $isCloudIp) { $profile.CloudIpSignIns.Add([PSCustomObject]@{ Timestamp = $event.Timestamp UserPrincipalName = $event.UserPrincipalName IpAddress = $ip IpClass = $ipClass AppDisplayName = $event.AppDisplayName Location = $event.Location }) } # VPN/Tor sign-ins if ((Test-DetectionEnabled 'vpnTorSignIns') -and $ipClass -in @('vpn', 'tor', 'proxy')) { $profile.VpnTorSignIns.Add([PSCustomObject]@{ Timestamp = $event.Timestamp UserPrincipalName = $event.UserPrincipalName IpAddress = $ip IpClass = $ipClass AppDisplayName = $event.AppDisplayName Location = $event.Location }) } # Foreign country sign-ins $locationCountry = $event.Location.Country ?? '' if (-not $geoCountry -and $locationCountry) { $geoCountry = $locationCountry } if ((Test-DetectionEnabled 'foreignCountrySignIns') -and $geoCountry -and $suspiciousCountryCodes.Contains($geoCountry)) { $profile.ForeignCountrySignIns.Add([PSCustomObject]@{ Timestamp = $event.Timestamp UserPrincipalName = $event.UserPrincipalName IpAddress = $ip GeoCountry = $geoCountry AppDisplayName = $event.AppDisplayName Location = $event.Location }) } } # --- Risky sign-in detection --- if ((Test-DetectionEnabled 'riskySignIns') -and $SignInEvents.Count -gt 0) { foreach ($item in (Test-EntraRiskySignIn -SignInEvents $SignInEvents)) { $profile.RiskySignIns.Add($item) } } # --- Risk detection based signals --- if ($RiskDetections.Count -gt 0) { if (Test-DetectionEnabled 'impossibleTravel') { foreach ($item in (Test-EntraImpossibleTravel -RiskDetections $RiskDetections)) { $profile.ImpossibleTravelDetections.Add($item) } } if (Test-DetectionEnabled 'unfamiliarProperties') { foreach ($item in (Test-EntraUnfamiliarSignIn -RiskDetections $RiskDetections)) { $profile.UnfamiliarSignIns.Add($item) } } if (Test-DetectionEnabled 'anonymousIp') { foreach ($item in (Test-EntraAnonymousIp -RiskDetections $RiskDetections)) { $profile.AnonymousIpSignIns.Add($item) } } if (Test-DetectionEnabled 'malwareIp') { foreach ($item in (Test-EntraMalwareIp -RiskDetections $RiskDetections)) { $profile.MalwareIpSignIns.Add($item) } } if (Test-DetectionEnabled 'leakedCredentials') { foreach ($item in (Test-EntraLeakedCredential -RiskDetections $RiskDetections)) { $profile.LeakedCredentials.Add($item) } } if (Test-DetectionEnabled 'passwordSpray') { foreach ($item in (Test-EntraPasswordSpray -RiskDetections $RiskDetections)) { $profile.PasswordSprayDetections.Add($item) } } if (Test-DetectionEnabled 'anomalousToken') { foreach ($item in (Test-EntraAnomalousToken -RiskDetections $RiskDetections)) { $profile.AnomalousTokenDetections.Add($item) } } } # --- Audit-based signals --- if ($AuditEvents.Count -gt 0) { if (Test-DetectionEnabled 'privilegedRoleChanges') { foreach ($item in (Test-EntraPrivilegedRoleChange -AuditEvents $AuditEvents)) { $profile.PrivilegedRoleChanges.Add($item) } } if (Test-DetectionEnabled 'globalAdminAssignment') { foreach ($item in (Test-EntraGlobalAdminAssignment -AuditEvents $AuditEvents)) { $profile.GlobalAdminAssignments.Add($item) } } if (Test-DetectionEnabled 'conditionalAccessChanges') { foreach ($item in (Test-EntraCAPolicyChange -AuditEvents $AuditEvents)) { $profile.CAPolicyChanges.Add($item) } } if (Test-DetectionEnabled 'servicePrincipalCredentials') { foreach ($item in (Test-EntraServicePrincipalCred -AuditEvents $AuditEvents)) { $profile.ServicePrincipalCredChanges.Add($item) } } if (Test-DetectionEnabled 'appPermissionGrants') { foreach ($item in (Test-EntraAppPermissionGrant -AuditEvents $AuditEvents)) { $profile.AppPermissionGrants.Add($item) } } if (Test-DetectionEnabled 'federationChanges') { foreach ($item in (Test-EntraFederationChange -AuditEvents $AuditEvents)) { $profile.FederationChanges.Add($item) } } if (Test-DetectionEnabled 'guestInvitations') { foreach ($item in (Test-EntraGuestInvitation -AuditEvents $AuditEvents)) { $profile.GuestInvitations.Add($item) } } if (Test-DetectionEnabled 'adminUnitChanges') { foreach ($item in (Test-EntraAdminUnitChange -AuditEvents $AuditEvents)) { $profile.AdminUnitChanges.Add($item) } } if (Test-DetectionEnabled 'authMethodChanges') { foreach ($item in (Test-EntraAuthMethodChange -AuditEvents $AuditEvents)) { $profile.AuthMethodChanges.Add($item) } } if (Test-DetectionEnabled 'tenantSettingChanges') { foreach ($item in (Test-EntraTenantSettingChange -AuditEvents $AuditEvents)) { $profile.TenantSettingChanges.Add($item) } } if (Test-DetectionEnabled 'subscriptionPermChanges') { foreach ($item in (Test-EntraSubscriptionPermChange -AuditEvents $AuditEvents)) { $profile.SubscriptionPermChanges.Add($item) } } # Audit log gaps if (Test-DetectionEnabled 'auditLogGaps') { $gapThreshold = if ($DetectionConfig.auditLogGapThresholdHours) { $DetectionConfig.auditLogGapThresholdHours } else { 24 } $profile.AuditLogGaps = @(Test-EntraAuditLogGap -AuditEvents $AuditEvents -GapThresholdHours $gapThreshold) } } # --- Score the profile --- $weights = $null if ($DetectionConfig.entraWeights) { $weights = $DetectionConfig.entraWeights } $profile = Get-EntraMonitorThreatScore -Profile $profile -Weights $weights return $profile } |