Enable Microsoft Entra ID Protection, configure user risk and sign-in risk policies, set up Conditional Access with risk-based controls, investigate risky users and sign-ins, integrate with Defender XDR, and automate remediation for compromised identities.
Entra ID Protection uses Microsoft’s global identity intelligence to detect identity-based risks in real time. It identifies leaked credentials, impossible travel, anonymous IP usage, and suspicious sign-in patterns, then enables automated response through risk-based Conditional Access policies.
An enterprise with 20,000 users and hybrid identity detects 50+ risky sign-ins daily. Manual investigation takes 30 minutes per event. They need automated risk-based policies that block high-risk sign-ins and force MFA for medium-risk, reducing mean time to respond from hours to seconds.
Identity is the new perimeter. 80% of breaches involve compromised credentials. Manual investigation of risky sign-ins doesn’t scale. Automated risk-based policies ensure compromised accounts are contained in seconds, not hours.
risk-test-user@contoso.com and exclude your break-glass accounts from all risk policies.Start by exploring the Entra ID Protection dashboard to understand the current risk landscape in your tenant. The dashboard provides an at-a-glance view of risky users, risky sign-ins, and risk detections.
# Install the Microsoft Graph PowerShell SDK (if not already installed)
Install-Module Microsoft.Graph -Scope CurrentUser -Force
# Connect to Microsoft Graph with required scopes
# WHAT: Authenticates to Graph API with permissions needed for Identity Protection
# WHY: Each scope grants specific access:
# IdentityRiskyUser.Read.All - Read all risky user data (risk level, state, history)
# IdentityRiskEvent.Read.All - Read risk detection events (leaked creds, anon IP, etc.)
# Policy.Read.All - Read Conditional Access policies
# Policy.ReadWrite.ConditionalAccess - Create/modify CA policies
Connect-MgGraph -Scopes "IdentityRiskyUser.Read.All","IdentityRiskEvent.Read.All","Policy.Read.All","Policy.ReadWrite.ConditionalAccess"
# Verify connection - confirms the authenticated account, tenant, and granted scopes
Get-MgContext | Select-Object Account, TenantId, Scopes
# List current risky users filtered to high risk level
# OUTPUT fields:
# RiskLevel - none | low | medium | high | hidden (aggregated risk across all detections)
# RiskState - atRisk | confirmedCompromised | remediated | dismissed | unknownFutureValue
# RiskLastUpdatedDateTime - when the risk level was last recalculated
Get-MgRiskyUser -Filter "riskLevel eq 'high'" | Select-Object UserDisplayName, UserPrincipalName, RiskLevel, RiskState, RiskLastUpdatedDateTime
# List the 10 most recent risk detections sorted by detection time
# OUTPUT fields:
# RiskEventType - e.g. leakedCredentials, anonymizedIPAddress, impossibleTravel,
# unfamiliarFeatures, malwareInfectedIPAddress, passwordSpray
# RiskLevel - low | medium | high (severity of this individual detection)
# IpAddress - source IP of the risky sign-in (useful for threat intel correlation)
Get-MgRiskDetection -Top 10 -OrderBy "detectedDateTime desc" | Select-Object UserDisplayName, RiskEventType, RiskLevel, DetectedDateTime, IpAddressThe user risk policy evaluates the aggregate risk associated with a user account. When a user is determined to be at risk (e.g., leaked credentials detected), this policy forces a secure password change at next sign-in.
CA-UserRisk-RequirePasswordChange# WHAT: Create a Conditional Access policy that forces password change for high-risk users
# WHY: When Entra ID Protection detects a user's credentials are likely compromised
# (e.g., leaked credentials found on dark web), this policy forces a secure password
# reset at the next sign-in, automatically remediating the risk
$params = @{
DisplayName = "CA-UserRisk-RequirePasswordChange"
# State values: enabled | disabled | enabledForReportingButNotEnforced (report-only)
# IMPORTANT: Always start in report-only mode to assess impact before enforcing
State = "enabledForReportingButNotEnforced"
Conditions = @{
Users = @{
IncludeUsers = @("All")
# CRITICAL: Always exclude break-glass accounts to prevent lockout
ExcludeUsers = @("<break-glass-account-object-id>")
}
Applications = @{
IncludeApplications = @("All") # Apply to all cloud apps
}
# UserRiskLevels: low | medium | high
# User risk = offline/aggregated risk (leaked creds, threat intel)
# Unlike sign-in risk, user risk persists until remediated
UserRiskLevels = @("high")
}
GrantControls = @{
Operator = "OR"
# BuiltInControls options: block, mfa, compliantDevice,
# domainJoinedDevice, approvedApplication, passwordChange
BuiltInControls = @("passwordChange")
}
SessionControls = @{
SignInFrequency = @{
Value = 0
Type = "everyTime" # Forces re-authentication every time
IsEnabled = $true
}
}
}
New-MgIdentityConditionalAccessPolicy -BodyParameter $params
# Verify the policy was created and check its enforcement state
Get-MgIdentityConditionalAccessPolicy -Filter "displayName eq 'CA-UserRisk-RequirePasswordChange'" | Select-Object DisplayName, StateThe sign-in risk policy evaluates each sign-in attempt in real time. It detects anomalies such as anonymous IP addresses, impossible travel, unfamiliar sign-in properties, and password spray attacks, then requires MFA or blocks the sign-in.
CA-SignInRisk-RequireMFA# WHAT: Create a CA policy requiring MFA for medium and high sign-in risk
# WHY: Sign-in risk is evaluated in REAL TIME during each authentication.
# Unlike user risk (offline/aggregated), sign-in risk detects anomalies
# in the current session: anonymous IP, impossible travel, password spray, etc.
# SignInRiskLevels: low | medium | high
# medium = suspicious but not confirmed (e.g., unfamiliar sign-in properties)
# high = strong indicators of compromise (e.g., anonymous IP + atypical travel)
$signInRiskPolicy = @{
DisplayName = "CA-SignInRisk-RequireMFA"
State = "enabledForReportingButNotEnforced"
Conditions = @{
Users = @{
IncludeUsers = @("All")
ExcludeUsers = @("<break-glass-account-object-id>")
}
Applications = @{
IncludeApplications = @("All")
}
SignInRiskLevels = @("high", "medium")
}
GrantControls = @{
Operator = "OR"
# Requires step-up MFA - user must complete additional authentication
BuiltInControls = @("mfa")
}
}
New-MgIdentityConditionalAccessPolicy -BodyParameter $signInRiskPolicy
# WHAT: Create a BLOCK policy for high-risk sign-ins only
# WHY: Layered defence - high-risk sign-ins are fully blocked (no MFA bypass)
# while medium-risk sign-ins can proceed if user satisfies MFA
# NOTE: Deploy this AFTER validating false positive rates on the MFA policy above
$blockHighRisk = @{
DisplayName = "CA-SignInRisk-BlockHighRisk"
State = "enabledForReportingButNotEnforced"
Conditions = @{
Users = @{
IncludeUsers = @("All")
ExcludeUsers = @("<break-glass-account-object-id>")
}
Applications = @{
IncludeApplications = @("All")
}
SignInRiskLevels = @("high")
}
GrantControls = @{
Operator = "OR"
BuiltInControls = @("block") # Completely blocks the sign-in attempt
}
}
New-MgIdentityConditionalAccessPolicy -BodyParameter $blockHighRisk
# Verify: List all Conditional Access policies with "Risk" in their name
# OUTPUT: DisplayName and State (enabled, disabled, or report-only)
Get-MgIdentityConditionalAccessPolicy | Where-Object { $_.DisplayName -like "*Risk*" } | Select-Object DisplayName, StateCombine user risk, sign-in risk, device compliance, and named locations into comprehensive Conditional Access policies that adapt to the threat context of each authentication.
Corporate-Network and mark as TrustedCA-Combined-RiskPolicy-CorpAppsCorporate-Network# Combined risk-based Conditional Access policy via Graph API
$combinedPolicy = @{
DisplayName = "CA-Combined-RiskPolicy-CorpApps"
State = "enabledForReportingButNotEnforced"
Conditions = @{
Users = @{
IncludeUsers = @("All")
ExcludeUsers = @("<break-glass-object-id-1>", "<break-glass-object-id-2>")
}
Applications = @{
IncludeApplications = @(
"797f4846-ba00-4fd7-ba43-dac1f8f63013" # Azure Management
"00000002-0000-0ff1-ce00-000000000000" # Exchange Online
"00000003-0000-0ff1-ce00-000000000000" # SharePoint Online
)
}
UserRiskLevels = @("high", "medium")
SignInRiskLevels = @("high", "medium")
Locations = @{
IncludeLocations = @("All")
ExcludeLocations = @("<corporate-named-location-id>")
}
}
GrantControls = @{
Operator = "AND"
BuiltInControls = @("mfa", "compliantDevice")
}
SessionControls = @{
SignInFrequency = @{
Value = 1
Type = "hours"
IsEnabled = $true
}
}
}
New-MgIdentityConditionalAccessPolicy -BodyParameter $combinedPolicyWhen Identity Protection flags a user as risky, investigate the risk history timeline to determine whether the user is truly compromised or if the detection is a false positive.
# Get all high-risk users
$riskyUsers = Get-MgRiskyUser -Filter "riskLevel eq 'high'" -All
$riskyUsers | Select-Object UserDisplayName, UserPrincipalName, RiskLevel, RiskState, RiskDetail, RiskLastUpdatedDateTime | Format-Table -AutoSize
# Get risk history for a specific user
$userId = (Get-MgRiskyUser -Filter "userPrincipalName eq 'john.doe@contoso.com'").Id
Get-MgRiskyUserHistory -RiskyUserId $userId | Select-Object RiskLevel, RiskState, RiskDetail, InitiatedBy, @{N='DetectedDateTime';E={$_.Activity.RiskEventTypes}} | Format-Table
# Confirm a user is compromised (sets risk to High)
Confirm-MgRiskyUserCompromised -UserIds @($userId)
# Dismiss risk for a false positive
Invoke-MgDismissRiskyUser -UserIds @($userId)
# Bulk export risky users to CSV
Get-MgRiskyUser -All | Select-Object UserDisplayName, UserPrincipalName, RiskLevel, RiskState, RiskLastUpdatedDateTime | Export-Csv -Path "RiskyUsers-$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformationRisky sign-ins are individual authentication events where Identity Protection detected a risk signal. Investigate these to understand the specific threat context and correlate with Defender XDR incidents.
// Risky sign-ins from the last 7 days
SigninLogs
| where TimeGenerated > ago(7d)
| where RiskLevelDuringSignIn in ("high", "medium")
| project TimeGenerated, UserPrincipalName, AppDisplayName, IPAddress,
LocationDetails, RiskLevelDuringSignIn, RiskEventTypes_V2,
DeviceDetail, ConditionalAccessStatus
| order by TimeGenerated desc
// Sign-ins from anonymous IP addresses
SigninLogs
| where TimeGenerated > ago(7d)
| where RiskEventTypes_V2 has "anonymizedIPAddress"
| project TimeGenerated, UserPrincipalName, IPAddress,
LocationDetails, AppDisplayName, ResultType
| order by TimeGenerated desc
// Impossible travel detections
SigninLogs
| where TimeGenerated > ago(7d)
| where RiskEventTypes_V2 has "impossibleTravel"
| project TimeGenerated, UserPrincipalName, IPAddress,
LocationDetails, DeviceDetail
| order by TimeGenerated desc
// Correlate risky sign-ins with AADRiskyUsers
let riskySignIns = SigninLogs
| where TimeGenerated > ago(24h)
| where RiskLevelDuringSignIn in ("high", "medium")
| distinct UserPrincipalName;
AADRiskyUsers
| where UserPrincipalName in (riskySignIns)
| project UserPrincipalName, RiskLevel, RiskState, RiskDetail, RiskLastUpdatedDateTimeRiskEventTypes_V2 field in SigninLogs contains the risk detection types as a JSON array. Use the has operator (not ==) to filter for specific risk types. Common values: anonymizedIPAddress, impossibleTravel, unfamiliarFeatures, leakedCredentials, passwordSpray.Enable users to self-remediate their own risk by performing MFA and changing their password through Self-Service Password Reset (SSPR). This removes the SOC burden for low-to-medium risk events while maintaining security.
# Confirm test user as compromised to trigger risk policies
$testUserId = (Get-MgUser -Filter "userPrincipalName eq 'risk-test-user@contoso.com'").Id
Confirm-MgRiskyUserCompromised -UserIds @($testUserId)
# Verify the user is now flagged as high risk
Get-MgRiskyUser -Filter "userPrincipalName eq 'risk-test-user@contoso.com'" | Select-Object UserDisplayName, RiskLevel, RiskStaterisk-test-user@contoso.com# Verify risk was cleared after self-remediation
Get-MgRiskyUser -Filter "userPrincipalName eq 'risk-test-user@contoso.com'" | Select-Object UserDisplayName, RiskLevel, RiskState, RiskDetail
# Expected output:
# RiskLevel: none
# RiskState: remediated
# RiskDetail: userPerformedSecuredPasswordChangeEntra ID Protection risk signals feed directly into Microsoft Defender XDR incidents. This integration provides SOC analysts with a unified view of identity risk alongside endpoint, email, and cloud app signals.
// Correlate Entra ID risky sign-ins with endpoint logon events
let riskyUsers = AADSignInEventsBeta
| where TimeGenerated > ago(24h)
| where RiskLevelDuringSignIn in ("high", "medium")
| distinct AccountUpn;
IdentityLogonEvents
| where TimeGenerated > ago(24h)
| where AccountUpn in (riskyUsers)
| project TimeGenerated, AccountUpn, DeviceName, LogonType,
Application, Protocol, ActionType
| order by TimeGenerated desc
// Join Identity Protection risk events with endpoint alerts
let riskyAccounts = AADRiskyUsers
| where RiskLevel in ("high", "medium")
| project UserPrincipalName, RiskLevel, RiskState;
AlertEvidence
| where TimeGenerated > ago(7d)
| where EntityType == "User"
| where RemediationStatus != "Resolved"
| join kind=inner riskyAccounts on $left.AccountUpn == $right.UserPrincipalName
| project TimeGenerated, Title, AccountUpn, RiskLevel, Severity
// Find users with both identity risk and suspicious mailbox activity
let riskyIds = AADRiskyUsers
| where RiskLevel == "high"
| project UserPrincipalName;
EmailEvents
| where TimeGenerated > ago(24h)
| where SenderFromAddress in (riskyIds) or RecipientEmailAddress in (riskyIds)
| where DeliveryAction == "Delivered"
| project TimeGenerated, SenderFromAddress, RecipientEmailAddress, Subject, DeliveryActionCreate Azure Monitor alerts for high-risk detections and build Sentinel workbook tiles for ongoing identity risk monitoring and trend analysis.
// Alert: High-risk user detected in the last 15 minutes
SigninLogs
| where TimeGenerated > ago(15m)
| where RiskLevelAggregated == "high"
| summarize Count=count() by UserPrincipalName, RiskLevelAggregated
| where Count > 0// Tile 1: Risk Detection Trend (30 days)
SigninLogs
| where TimeGenerated > ago(30d)
| where RiskLevelDuringSignIn in ("high", "medium", "low")
| summarize RiskCount=count() by bin(TimeGenerated, 1d), RiskLevelDuringSignIn
| render timechart
// Tile 2: Top Risk Detection Types
SigninLogs
| where TimeGenerated > ago(30d)
| where RiskEventTypes_V2 != "[]"
| mv-expand RiskType = parse_json(RiskEventTypes_V2)
| summarize Count=count() by tostring(RiskType)
| top 10 by Count
| render barchart
// Tile 3: Risky User Trends (30 days)
AADRiskyUsers
| summarize
HighRisk=countif(RiskLevel == "high"),
MediumRisk=countif(RiskLevel == "medium"),
LowRisk=countif(RiskLevel == "low")
// Tile 4: Risk Remediation Effectiveness
SigninLogs
| where TimeGenerated > ago(30d)
| where RiskLevelDuringSignIn in ("high", "medium")
| summarize
TotalRisky=count(),
MFASatisfied=countif(AuthenticationRequirement == "multiFactorAuthentication"
and ResultType == "0"),
Blocked=countif(ResultType == "53003"),
PasswordChanged=countif(ResultType == "50097")
| extend RemediationRate = round(todouble(MFASatisfied + Blocked + PasswordChanged) / TotalRisky * 100, 1)
// Tile 5: Geographic Distribution of Risky Sign-Ins
SigninLogs
| where TimeGenerated > ago(30d)
| where RiskLevelDuringSignIn in ("high", "medium")
| extend City = tostring(LocationDetails.city),
Country = tostring(LocationDetails.countryOrRegion)
| summarize Count=count() by Country, City
| top 20 by Count
| render barchartEntra ID Protection. Risk DashboardReview your Identity Protection configuration, disable test policies, and plan your ongoing operational procedures.
# List all risk-related Conditional Access policies
Get-MgIdentityConditionalAccessPolicy | Where-Object { $_.DisplayName -like "*Risk*" } | Select-Object Id, DisplayName, State | Format-Table
# Disable test policies (set to Report-only or Off)
$testPolicies = Get-MgIdentityConditionalAccessPolicy | Where-Object { $_.DisplayName -like "*Risk*" -and $_.State -eq "enabledForReportingButNotEnforced" }
foreach ($policy in $testPolicies) {
# To disable (off): Update-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $policy.Id -State "disabled"
Write-Host "Policy: $($policy.DisplayName). State: $($policy.State)"
}
# Dismiss risk for test users
$testUserId = (Get-MgUser -Filter "userPrincipalName eq 'risk-test-user@contoso.com'").Id
Invoke-MgDismissRiskyUser -UserIds @($testUserId)
# Disconnect Graph session
Disconnect-MgGraph| Resource | Description |
|---|---|
| Entra ID Protection overview | Feature overview and architecture |
| Configure risk policies | Step-by-step guide for user risk and sign-in risk policies |
| Investigate risk | How to investigate risky users and sign-ins |
| Conditional Access overview | Policy framework for adaptive access control |
| Risk detection types | Complete reference of all risk detection types and descriptions |
| Microsoft Graph Identity Protection APIs | Programmatic access to risk data via Graph API |
| Remediate risks and unblock users | Self-service and admin remediation workflows |
| Identity Protection workbooks | Built-in workbooks for risk monitoring and analysis |