Microsoft Entra ID Protection represents a paradigm shift in identity security, moving from reactive incident response to proactive risk detection and automated remediation. In enterprise environments where identities are the new perimeter, traditional security models that rely on periodic reviews and manual interventions are no longer sufficient. Entra ID Protection leverages machine learning models trained on 78 trillion security signals analyzed daily across Microsoft’s ecosystem to detect identity-based risks in real-time, enabling organizations to respond to threats before they result in breaches.
This comprehensive guide explores the architecture, implementation patterns, and automation workflows that enable enterprises to operationalize identity risk detection at scale. We examine how to configure risk-based Conditional Access policies, automate remediation workflows using Microsoft Graph APIs, integrate with SIEM platforms for correlation analysis, and implement continuous monitoring frameworks that adapt to evolving threat landscapes.
Understanding Identity Risk in Modern Enterprises
Identity compromise represents one of the most critical attack vectors in modern cybersecurity. When an attacker gains access to legitimate credentials, they can move laterally within networks, exfiltrate sensitive data, and maintain persistent access while appearing as authorized users. Entra ID Protection addresses this challenge through two distinct risk detection mechanisms: user risk and sign-in risk. User risk indicates the probability that a user account itself is compromised, triggered by detections such as leaked credentials discovered on the dark web, anomalous user activity patterns, or confirmed compromises from threat intelligence feeds. Sign-in risk represents the probability that a specific authentication request is not authorized by the legitimate account owner, detected through signals like impossible travel patterns, sign-ins from anonymous IP addresses, or unfamiliar device characteristics.
The distinction between these risk types is crucial for policy design. User risk detections such as leaked credentials require credential reset workflows regardless of how the user authenticates, since the compromise affects the account itself. Sign-in risk detections can often be remediated through additional authentication factors like MFA, since they represent suspicious authentication attempts rather than confirmed account compromises. Organizations must configure different policy responses for each risk type to balance security effectiveness with user productivity.
Entra ID Protection continuously evaluates risk using machine learning algorithms that analyze hundreds of signals during each authentication event. These signals include IP reputation data from Microsoft’s threat intelligence feeds, device compliance status from Microsoft Intune, historical user behavior patterns, token characteristics, and correlations with known attack patterns. The system calculates risk levels categorized as low, medium, or high based on confidence scores that represent how certain Microsoft is that unauthorized access is occurring. High-risk detections such as verified threat actor IP addresses or confirmed leaked credentials trigger immediate policy responses, while medium-risk detections may require additional context before enforcement actions are applied.
Risk Detection Types and Detection Mechanisms
Entra ID Protection implements both real-time and offline risk detections, each serving different operational requirements. Real-time detections occur synchronously during authentication flows, allowing policies to block or challenge suspicious sign-in attempts before granting access. These detections include anonymous IP address sign-ins detected through proxy identification, unfamiliar sign-in properties based on historical patterns, malware-linked IP addresses identified through threat intelligence, and password spray attempts where attackers test common passwords against multiple accounts. Real-time detections enable immediate policy enforcement, preventing unauthorized access at the authentication boundary before attackers gain initial foothold in the environment.
Offline risk detections occur asynchronously after authentication events complete, using batch processing and correlation analysis that may take hours or days to identify threats. These detections include impossible travel scenarios where sign-ins occur from geographically distant locations in timeframes that make physical travel impossible, leaked credentials discovered through monitoring dark web forums and credential dumps, anomalous user activity patterns detected through behavioral analytics, and suspicious inbox manipulation rules that may indicate account compromise for data exfiltration. While offline detections cannot prevent initial access, they enable organizations to identify compromised accounts quickly and trigger remediation workflows before attackers can establish persistence or move laterally.
Microsoft continuously updates the detection catalog based on emerging threat intelligence. Recent additions include suspicious API traffic detection that identifies anomalous Microsoft Graph API calls potentially indicating reconnaissance activity, anomalous token characteristics that detect token replay attacks, and suspicious browser detection that identifies automation tools used in credential stuffing attacks. Each detection type includes detailed metadata such as detection timing, confidence level, associated IP addresses, device information, and links to MITRE ATT&CK tactics for contextual threat intelligence.
Risk-Based Conditional Access Architecture
Risk-based Conditional Access policies represent the enforcement mechanism that transforms risk detections into actionable security controls. These policies evaluate risk levels calculated by Entra ID Protection during authentication flows and apply appropriate access controls based on organizational risk tolerance. The architecture follows a layered approach where sign-in risk policies operate at the authentication boundary, evaluating each sign-in attempt in real-time, while user risk policies operate at the account level, triggering remediation workflows when accounts are confirmed or suspected to be compromised.
Sign-in risk policies enable organizations to require additional authentication factors when suspicious authentication attempts are detected. When a user attempts to sign in and the calculated sign-in risk level meets or exceeds the policy threshold, Entra ID challenges the user with MFA. If the user successfully completes MFA, proving they possess the second factor, the risk is automatically remediated and the session is granted access. This automatic remediation eliminates administrator workload while maintaining security, since legitimate users can prove their identity through the second factor they possess. Organizations typically configure sign-in risk policies to require MFA at medium or high risk levels, balancing security with user experience.
User risk policies address scenarios where accounts themselves are compromised rather than just authentication attempts. When user risk reaches configured thresholds, organizations can require secure password changes that force users to reset their credentials through self-service password reset workflows. Microsoft-managed remediation, available with Entra ID P2 licensing, extends this capability to support both password-based and passwordless authentication methods. For users with compromised passwords, the system requires secure password change and revokes all active sessions. For passwordless users whose compromises do not involve passwords, the system revokes sessions and requires reauthentication through their passwordless methods, ensuring appropriate remediation regardless of authentication approach.
The following Mermaid diagram illustrates the risk-based Conditional Access decision flow:
flowchart TD
A[User Authentication Request] --> B{Real-Time Risk
Detection}
B --> C{Calculate Sign-In
Risk Level}
C --> D{Risk Level
Evaluation}
D -->|Low Risk| E[Grant Access]
D -->|Medium Risk| F{Sign-In Risk
Policy Check}
D -->|High Risk| F
F -->|Policy: Require MFA| G[Challenge with MFA]
G -->|MFA Success| H[Auto-Remediate Risk
Grant Access]
G -->|MFA Failure| I[Block Access]
F -->|Policy: Block| I
E --> J{Offline Risk
Detection Processing}
H --> J
J --> K{Calculate User
Risk Level}
K -->|Risk Detected| L{User Risk
Policy Check}
L -->|Require Password Change| M[Force Secure
Password Reset]
L -->|Require Remediation| N[Microsoft-Managed
Remediation Flow]
N -->|Password Auth| O[Password Change +
Session Revocation]
N -->|Passwordless Auth| P[Session Revocation +
Reauthentication]
M --> Q[Risk Remediated]
O --> Q
P --> Q
K -->|No Risk| R[Continue Monitoring]
style A fill:#e1f5ff
style H fill:#c8e6c9
style Q fill:#c8e6c9
style I fill:#ffcdd2
style B fill:#fff9c4
style J fill:#fff9c4Implementing Risk Detection with Python and Microsoft Graph API
Automating risk detection and remediation workflows requires programmatic access to Entra ID Protection data through Microsoft Graph APIs. The following Python implementation demonstrates a comprehensive identity protection manager that retrieves risk detections, monitors risky users, configures automated policies, and implements remediation workflows. This production-ready implementation uses the Microsoft Authentication Library (MSAL) for secure authentication and implements retry logic, error handling, and logging for enterprise reliability.
import msal
import requests
import json
from datetime import datetime, timedelta
from typing import List, Dict, Optional
import logging
class EntraIDProtectionManager:
"""
Comprehensive manager for Microsoft Entra ID Protection operations.
Handles risk detection, user risk monitoring, and automated remediation.
"""
def __init__(self, tenant_id: str, client_id: str, client_secret: str):
"""
Initialize the ID Protection manager with authentication credentials.
Args:
tenant_id: Azure AD tenant ID
client_id: Application (client) ID
client_secret: Client secret for authentication
"""
self.tenant_id = tenant_id
self.client_id = client_id
self.client_secret = client_secret
self.authority = f"https://login.microsoftonline.com/{tenant_id}"
self.scope = ["https://graph.microsoft.com/.default"]
self.graph_endpoint = "https://graph.microsoft.com/v1.0"
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
self.logger = logging.getLogger(__name__)
# Initialize MSAL confidential client
self.app = msal.ConfidentialClientApplication(
self.client_id,
authority=self.authority,
client_credential=self.client_secret
)
def _get_access_token(self) -> str:
"""Acquire access token for Microsoft Graph API calls."""
result = self.app.acquire_token_silent(self.scope, account=None)
if not result:
self.logger.info("No cached token available, acquiring new token")
result = self.app.acquire_token_for_client(scopes=self.scope)
if "access_token" in result:
return result["access_token"]
else:
error_description = result.get("error_description", "Unknown error")
raise Exception(f"Failed to acquire token: {error_description}")
def get_risk_detections(
self,
risk_state: Optional[str] = None,
risk_level: Optional[str] = None,
top: int = 50
) -> List[Dict]:
"""
Retrieve risk detections from Entra ID Protection.
Args:
risk_state: Filter by risk state (atRisk, confirmedCompromised, remediated, dismissed)
risk_level: Filter by risk level (low, medium, high)
top: Maximum number of results to return
Returns:
List of risk detection objects
"""
token = self._get_access_token()
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
# Build filter query
filters = []
if risk_state:
filters.append(f"riskState eq '{risk_state}'")
if risk_level:
filters.append(f"riskLevel eq '{risk_level}'")
filter_query = " and ".join(filters) if filters else ""
url = f"{self.graph_endpoint}/identityProtection/riskDetections"
params = {
"$top": top,
"$orderby": "detectedDateTime desc"
}
if filter_query:
params["$filter"] = filter_query
try:
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
data = response.json()
detections = data.get("value", [])
self.logger.info(f"Retrieved {len(detections)} risk detections")
return detections
except requests.exceptions.RequestException as e:
self.logger.error(f"Error retrieving risk detections: {e}")
raise
def get_risky_users(
self,
risk_level: Optional[str] = None,
risk_state: Optional[str] = None,
top: int = 50
) -> List[Dict]:
"""
Retrieve users flagged as risky by ID Protection.
Args:
risk_level: Filter by risk level (low, medium, high)
risk_state: Filter by risk state (atRisk, confirmedCompromised, remediated, dismissed)
top: Maximum number of results
Returns:
List of risky user objects
"""
token = self._get_access_token()
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
filters = []
if risk_level:
filters.append(f"riskLevel eq '{risk_level}'")
if risk_state:
filters.append(f"riskState eq '{risk_state}'")
filter_query = " and ".join(filters) if filters else ""
url = f"{self.graph_endpoint}/identityProtection/riskyUsers"
params = {
"$top": top,
"$orderby": "riskLastUpdatedDateTime desc"
}
if filter_query:
params["$filter"] = filter_query
try:
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
data = response.json()
risky_users = data.get("value", [])
self.logger.info(f"Retrieved {len(risky_users)} risky users")
return risky_users
except requests.exceptions.RequestException as e:
self.logger.error(f"Error retrieving risky users: {e}")
raise
def confirm_user_compromised(self, user_ids: List[str]) -> bool:
"""
Confirm that user accounts are compromised, triggering remediation policies.
Args:
user_ids: List of user IDs to mark as compromised
Returns:
True if operation succeeded
"""
token = self._get_access_token()
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
url = f"{self.graph_endpoint}/identityProtection/riskyUsers/confirmCompromised"
payload = {
"userIds": user_ids
}
try:
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
self.logger.info(f"Confirmed {len(user_ids)} users as compromised")
return True
except requests.exceptions.RequestException as e:
self.logger.error(f"Error confirming compromised users: {e}")
raise
def dismiss_user_risk(self, user_ids: List[str]) -> bool:
"""
Dismiss risk for users after investigation determines false positive.
Args:
user_ids: List of user IDs to dismiss risk for
Returns:
True if operation succeeded
"""
token = self._get_access_token()
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
url = f"{self.graph_endpoint}/identityProtection/riskyUsers/dismiss"
payload = {
"userIds": user_ids
}
try:
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
self.logger.info(f"Dismissed risk for {len(user_ids)} users")
return True
except requests.exceptions.RequestException as e:
self.logger.error(f"Error dismissing user risk: {e}")
raise
def get_user_risk_history(self, user_id: str) -> List[Dict]:
"""
Retrieve risk detection history for a specific user.
Args:
user_id: User ID to retrieve history for
Returns:
List of risk detection events for the user
"""
token = self._get_access_token()
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
url = f"{self.graph_endpoint}/identityProtection/riskDetections"
params = {
"$filter": f"userId eq '{user_id}'",
"$orderby": "detectedDateTime desc"
}
try:
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
data = response.json()
history = data.get("value", [])
self.logger.info(f"Retrieved {len(history)} risk events for user {user_id}")
return history
except requests.exceptions.RequestException as e:
self.logger.error(f"Error retrieving user risk history: {e}")
raise
def analyze_risk_patterns(self, days_back: int = 7) -> Dict:
"""
Analyze risk detection patterns over a specified time period.
Args:
days_back: Number of days to analyze
Returns:
Dictionary containing risk pattern analysis
"""
cutoff_date = (datetime.utcnow() - timedelta(days=days_back)).isoformat() + "Z"
# Get all recent risk detections
detections = self.get_risk_detections(top=1000)
# Filter by date
recent_detections = [
d for d in detections
if d.get("detectedDateTime", "") >= cutoff_date
]
# Analyze patterns
analysis = {
"total_detections": len(recent_detections),
"by_risk_level": {},
"by_detection_type": {},
"by_risk_state": {},
"high_risk_users": []
}
for detection in recent_detections:
risk_level = detection.get("riskLevel", "unknown")
detection_type = detection.get("riskEventType", "unknown")
risk_state = detection.get("riskState", "unknown")
analysis["by_risk_level"][risk_level] = analysis["by_risk_level"].get(risk_level, 0) + 1
analysis["by_detection_type"][detection_type] = analysis["by_detection_type"].get(detection_type, 0) + 1
analysis["by_risk_state"][risk_state] = analysis["by_risk_state"].get(risk_state, 0) + 1
if risk_level == "high":
user_display_name = detection.get("userDisplayName", "Unknown")
if user_display_name not in analysis["high_risk_users"]:
analysis["high_risk_users"].append(user_display_name)
self.logger.info(f"Analyzed {len(recent_detections)} risk detections from last {days_back} days")
return analysis
def export_risk_data_for_siem(self, output_file: str = "risk_export.json"):
"""
Export risk detection data in format suitable for SIEM integration.
Args:
output_file: Output filename for exported data
"""
detections = self.get_risk_detections(top=1000)
risky_users = self.get_risky_users(top=1000)
export_data = {
"export_timestamp": datetime.utcnow().isoformat() + "Z",
"risk_detections": detections,
"risky_users": risky_users,
"summary": {
"total_detections": len(detections),
"total_risky_users": len(risky_users),
"high_risk_detections": len([d for d in detections if d.get("riskLevel") == "high"]),
"confirmed_compromised": len([u for u in risky_users if u.get("riskState") == "confirmedCompromised"])
}
}
with open(output_file, 'w') as f:
json.dump(export_data, f, indent=2)
self.logger.info(f"Exported risk data to {output_file}")
# Example usage demonstrating enterprise workflows
if __name__ == "__main__":
# Initialize manager with service principal credentials
manager = EntraIDProtectionManager(
tenant_id="your-tenant-id",
client_id="your-client-id",
client_secret="your-client-secret"
)
# Retrieve high-risk detections for immediate investigation
high_risk_detections = manager.get_risk_detections(
risk_level="high",
risk_state="atRisk"
)
print(f"\nHigh Risk Detections: {len(high_risk_detections)}")
for detection in high_risk_detections[:5]:
print(f" - {detection['userDisplayName']}: {detection['riskEventType']}")
print(f" Risk Level: {detection['riskLevel']}, Detected: {detection['detectedDateTime']}")
# Get all risky users requiring remediation
risky_users = manager.get_risky_users(
risk_state="atRisk",
risk_level="high"
)
print(f"\nRisky Users Requiring Attention: {len(risky_users)}")
for user in risky_users[:5]:
print(f" - {user['userDisplayName']} (Risk: {user['riskLevel']})")
print(f" Last Updated: {user['riskLastUpdatedDateTime']}")
# Analyze risk patterns over last 7 days
analysis = manager.analyze_risk_patterns(days_back=7)
print("\nRisk Pattern Analysis (Last 7 Days):")
print(f" Total Detections: {analysis['total_detections']}")
print(f" By Risk Level: {analysis['by_risk_level']}")
print(f" High Risk Users: {', '.join(analysis['high_risk_users'][:5])}")
# Export data for SIEM integration
manager.export_risk_data_for_siem("entra_id_risk_export.json")
print("\nRisk data exported for SIEM integration")Configuring Risk-Based Conditional Access Policies
Organizations must carefully configure risk thresholds and policy responses to balance security effectiveness with user productivity. Setting policies too aggressively results in excessive MFA prompts and password resets that frustrate legitimate users and reduce productivity. Setting policies too permissively allows attackers to operate undetected until they cause significant damage. The optimal configuration depends on organizational risk tolerance, user population characteristics, and the sensitivity of resources being protected.
For sign-in risk policies, most organizations configure MFA requirements at medium or high-risk levels. This approach challenges suspicious authentication attempts while allowing low-risk sign-ins from familiar locations and devices to proceed without additional friction. Organizations with highly sensitive data may set thresholds at low risk, requiring MFA for any detected risk, while organizations prioritizing user experience may only enforce MFA at high-risk levels. The policy should exclude emergency access accounts and break-glass scenarios to prevent complete lockouts during authentication system failures.
User risk policies typically require secure password changes at medium or high-risk levels, since these detections indicate probable account compromise rather than just suspicious authentication attempts. Organizations should ensure users are pre-registered for MFA and self-service password reset before enabling user risk policies, since remediation flows require these capabilities. For hybrid environments synchronizing passwords to on-premises Active Directory, password writeback must be configured to allow password changes initiated in Azure to synchronize back to on-premises directories.
The following Python code demonstrates creating risk-based Conditional Access policies through the Microsoft Graph API:
def create_signin_risk_policy(
self,
policy_name: str,
risk_levels: List[str],
included_users: List[str],
excluded_users: Optional[List[str]] = None
) -> Dict:
"""
Create a Conditional Access policy for sign-in risk.
Args:
policy_name: Display name for the policy
risk_levels: Risk levels to trigger policy (low, medium, high)
included_users: User IDs or groups to include
excluded_users: User IDs or groups to exclude (e.g., break-glass accounts)
Returns:
Created policy object
"""
token = self._get_access_token()
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
policy_definition = {
"displayName": policy_name,
"state": "enabled",
"conditions": {
"signInRiskLevels": risk_levels,
"applications": {
"includeApplications": ["All"]
},
"users": {
"includeUsers": included_users,
"excludeUsers": excluded_users or []
}
},
"grantControls": {
"operator": "OR",
"builtInControls": ["mfa"]
}
}
url = f"{self.graph_endpoint}/identity/conditionalAccess/policies"
try:
response = requests.post(url, headers=headers, json=policy_definition)
response.raise_for_status()
policy = response.json()
self.logger.info(f"Created sign-in risk policy: {policy_name}")
return policy
except requests.exceptions.RequestException as e:
self.logger.error(f"Error creating sign-in risk policy: {e}")
raise
def create_user_risk_policy(
self,
policy_name: str,
risk_levels: List[str],
included_users: List[str],
excluded_users: Optional[List[str]] = None,
use_managed_remediation: bool = True
) -> Dict:
"""
Create a Conditional Access policy for user risk.
Args:
policy_name: Display name for the policy
risk_levels: Risk levels to trigger policy
included_users: User IDs or groups to include
excluded_users: User IDs or groups to exclude
use_managed_remediation: Use Microsoft-managed remediation (requires P2)
Returns:
Created policy object
"""
token = self._get_access_token()
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
grant_control = "compliantDevice" if use_managed_remediation else "passwordChange"
policy_definition = {
"displayName": policy_name,
"state": "enabled",
"conditions": {
"userRiskLevels": risk_levels,
"applications": {
"includeApplications": ["All"]
},
"users": {
"includeUsers": included_users,
"excludeUsers": excluded_users or []
}
},
"grantControls": {
"operator": "OR",
"builtInControls": [grant_control]
},
"sessionControls": {
"signInFrequency": {
"value": 0,
"type": "everyTime",
"isEnabled": True
}
}
}
url = f"{self.graph_endpoint}/identity/conditionalAccess/policies"
try:
response = requests.post(url, headers=headers, json=policy_definition)
response.raise_for_status()
policy = response.json()
self.logger.info(f"Created user risk policy: {policy_name}")
return policy
except requests.exceptions.RequestException as e:
self.logger.error(f"Error creating user risk policy: {e}")
raiseIntegrating ID Protection with SIEM Platforms
Enterprise security operations require correlating identity risk data with security events from other systems to identify complex attack patterns. Entra ID Protection integrates with SIEM platforms like Microsoft Sentinel, Splunk, and QRadar through diagnostic settings that stream risk detection events to Log Analytics workspaces or Event Hubs. This integration enables security analysts to correlate identity risks with network traffic patterns, endpoint detection events, and application logs to build comprehensive threat intelligence.
Organizations should configure diagnostic settings to export SignInLogs, AuditLogs, RiskyUsers, and UserRiskEvents to their SIEM platform. These logs provide complete visibility into authentication events, risk detections, policy enforcement actions, and remediation activities. Security teams can create correlation rules that trigger alerts when multiple indicators of compromise are detected across different security layers, such as a risky sign-in followed by unusual data access patterns or privilege escalation attempts.
The following Kusto Query Language (KQL) example demonstrates identifying high-risk sign-in patterns in Log Analytics:
// Query to identify users with frequent high-risk sign-in attempts
SigninLogs
| where TimeGenerated > ago(7d)
| where RiskLevelDuringSignIn == "high" or RiskLevelAggregated == "high"
| summarize
HighRiskAttempts = count(),
UniqueLocations = dcount(Location),
UniqueIPAddresses = dcount(IPAddress),
RiskEventTypes = make_set(RiskEventTypes),
LatestAttempt = max(TimeGenerated)
by UserPrincipalName, AppDisplayName
| where HighRiskAttempts >= 3
| order by HighRiskAttempts desc
| project
UserPrincipalName,
AppDisplayName,
HighRiskAttempts,
UniqueLocations,
UniqueIPAddresses,
RiskEventTypes,
LatestAttemptAutomated Remediation Workflows and Response Playbooks
Mature identity security programs implement automated response workflows that reduce mean time to remediation (MTTR) and minimize administrator workload. Azure Logic Apps and Microsoft Sentinel playbooks enable organizations to orchestrate complex remediation workflows triggered by risk detections. These workflows can automatically disable compromised accounts, revoke active sessions, notify security teams through multiple channels, create incident tickets in ServiceNow or Jira, and initiate forensic data collection for investigation.
A typical automated remediation workflow begins when Entra ID Protection detects high-risk user activity. The detection triggers a Logic App through an Event Grid subscription or Microsoft Sentinel analytics rule. The workflow retrieves additional context about the user from Microsoft Graph, such as recent sign-in history, assigned roles, and group memberships. Based on this context and organizational policies, the workflow may automatically confirm the user as compromised, force password reset, revoke all active sessions, and remove the user from privileged groups pending investigation. Security analysts receive notifications through Microsoft Teams or email with all relevant context to begin investigation immediately.
Organizations should implement tiered response workflows based on risk severity and user criticality. Standard user accounts with medium-risk detections may trigger automated MFA challenges or secure password resets. Privileged accounts or high-risk detections trigger more aggressive responses including immediate session revocation, account suspension, and escalation to security operations teams. Executive accounts or service accounts critical to business operations may trigger notification workflows rather than automated remediation to prevent disrupting critical business processes.
Tuning Policies and Reducing False Positives
Machine learning models that power risk detection inevitably produce false positives where legitimate user behavior is flagged as risky. Organizations must implement feedback loops that improve detection accuracy over time while minimizing user friction. Configuring named locations for corporate VPN IP ranges and office networks significantly reduces false positives for impossible travel and unfamiliar location detections. Users signing in from recognized corporate networks receive lower risk scores, reducing unnecessary MFA prompts.
Security administrators should regularly review risk dismissals and user feedback to identify patterns in false positives. If multiple users are consistently flagged for risky sign-ins when accessing specific applications or from specific locations, these may indicate configuration issues rather than actual threats. Organizations can use the risk dismissal API to programmatically mark false positives, providing feedback that helps Microsoft refine their machine learning models for improved accuracy in future detections.
The Impact Analysis workbook available in Entra ID Protection helps organizations understand policy impact before enforcement. This workbook analyzes historical sign-in data to predict how many users would be challenged or blocked by proposed policy configurations. Organizations should use this workbook during policy design phases to ensure policies provide security benefits without creating excessive user friction that reduces productivity or drives shadow IT adoption.
Node.js Implementation for Webhook-Based Automation
Organizations using Node.js-based automation platforms can implement webhook listeners that respond to ID Protection events in real-time. The following implementation demonstrates a production-ready webhook handler using Express.js:
const express = require('express');
const msal = require('@azure/msal-node');
const axios = require('axios');
class IDProtectionWebhookHandler {
constructor(config) {
this.config = config;
this.app = express();
this.app.use(express.json());
// Initialize MSAL client
const msalConfig = {
auth: {
clientId: config.clientId,
authority: `https://login.microsoftonline.com/${config.tenantId}`,
clientSecret: config.clientSecret
}
};
this.msalClient = new msal.ConfidentialClientApplication(msalConfig);
this.graphEndpoint = 'https://graph.microsoft.com/v1.0';
}
async getAccessToken() {
const tokenRequest = {
scopes: ['https://graph.microsoft.com/.default']
};
try {
const response = await this.msalClient.acquireTokenByClientCredential(tokenRequest);
return response.accessToken;
} catch (error) {
console.error('Error acquiring token:', error);
throw error;
}
}
async handleHighRiskUser(userId, riskLevel, riskEventType) {
console.log(`Processing high-risk user: ${userId}`);
const token = await this.getAccessToken();
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
// Get user details
const userResponse = await axios.get(
`${this.graphEndpoint}/users/${userId}`,
{ headers }
);
const user = userResponse.data;
// Check if user has privileged roles
const rolesResponse = await axios.get(
`${this.graphEndpoint}/users/${userId}/memberOf`,
{ headers }
);
const hasPrivilegedRoles = rolesResponse.data.value.some(group =>
group.displayName && group.displayName.includes('Admin')
);
// Automated response based on risk and user criticality
if (hasPrivilegedRoles) {
console.log(`High-risk privileged account detected: ${user.userPrincipalName}`);
// Confirm user compromised to trigger remediation
await axios.post(
`${this.graphEndpoint}/identityProtection/riskyUsers/confirmCompromised`,
{ userIds: [userId] },
{ headers }
);
// Revoke all sessions
await axios.post(
`${this.graphEndpoint}/users/${userId}/revokeSignInSessions`,
{},
{ headers }
);
// Send Teams notification to security team
await this.sendTeamsNotification({
title: 'CRITICAL: Privileged Account Compromised',
user: user.userPrincipalName,
riskLevel: riskLevel,
riskEventType: riskEventType,
actionsToken: 'Sessions revoked, user confirmed compromised'
});
} else {
console.log(`High-risk standard account detected: ${user.userPrincipalName}`);
// Force password reset for non-privileged users
await axios.post(
`${this.graphEndpoint}/identityProtection/riskyUsers/confirmCompromised`,
{ userIds: [userId] },
{ headers }
);
// Log event for audit
console.log(`Remediation triggered for user: ${user.userPrincipalName}`);
}
}
async sendTeamsNotification(incident) {
// Implementation for Microsoft Teams webhook
const teamsWebhook = this.config.teamsWebhookUrl;
const messageCard = {
"@type": "MessageCard",
"@context": "https://schema.org/extensions",
"summary": incident.title,
"themeColor": "FF0000",
"title": incident.title,
"sections": [{
"facts": [
{ "name": "User", "value": incident.user },
{ "name": "Risk Level", "value": incident.riskLevel },
{ "name": "Risk Event", "value": incident.riskEventType },
{ "name": "Actions Taken", "value": incident.actionsToken }
]
}]
};
try {
await axios.post(teamsWebhook, messageCard);
console.log('Teams notification sent successfully');
} catch (error) {
console.error('Error sending Teams notification:', error);
}
}
setupRoutes() {
// Webhook endpoint for risk detection events
this.app.post('/webhook/risk-detection', async (req, res) => {
try {
const event = req.body;
console.log('Received risk detection event:', event);
// Validate event signature (implement based on your security requirements)
if (event.riskLevel === 'high') {
await this.handleHighRiskUser(
event.userId,
event.riskLevel,
event.riskEventType
);
}
res.status(200).json({ status: 'processed' });
} catch (error) {
console.error('Error processing webhook:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Health check endpoint
this.app.get('/health', (req, res) => {
res.status(200).json({ status: 'healthy' });
});
}
start(port = 3000) {
this.setupRoutes();
this.app.listen(port, () => {
console.log(`ID Protection webhook handler listening on port ${port}`);
});
}
}
// Example usage
const config = {
tenantId: process.env.TENANT_ID,
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
teamsWebhookUrl: process.env.TEAMS_WEBHOOK_URL
};
const handler = new IDProtectionWebhookHandler(config);
handler.start();C# Implementation for Enterprise Integration
Organizations with .NET-based enterprise architectures can leverage the Microsoft Graph SDK for strongly-typed access to ID Protection APIs. The following C# implementation demonstrates integration patterns for enterprise applications:
using Microsoft.Graph;
using Microsoft.Identity.Client;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public class EntraIDProtectionService
{
private readonly IConfidentialClientApplication _msalClient;
private readonly GraphServiceClient _graphClient;
public EntraIDProtectionService(string tenantId, string clientId, string clientSecret)
{
_msalClient = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithAuthority($"https://login.microsoftonline.com/{tenantId}")
.WithClientSecret(clientSecret)
.Build();
_graphClient = new GraphServiceClient(new DelegateAuthenticationProvider(async (request) =>
{
var result = await _msalClient
.AcquireTokenForClient(new[] { "https://graph.microsoft.com/.default" })
.ExecuteAsync();
request.Headers.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", result.AccessToken);
}));
}
public async Task> GetHighRiskDetectionsAsync()
{
var riskDetections = await _graphClient.IdentityProtection.RiskDetections
.Request()
.Filter("riskLevel eq 'high' and riskState eq 'atRisk'")
.OrderBy("detectedDateTime desc")
.Top(100)
.GetAsync();
return riskDetections.CurrentPage;
}
public async Task> GetRiskyUsersAsync(string riskLevel = null)
{
var requestBuilder = _graphClient.IdentityProtection.RiskyUsers.Request();
if (!string.IsNullOrEmpty(riskLevel))
{
requestBuilder = requestBuilder.Filter($"riskLevel eq '{riskLevel}'");
}
var riskyUsers = await requestBuilder
.OrderBy("riskLastUpdatedDateTime desc")
.Top(100)
.GetAsync();
return riskyUsers.CurrentPage;
}
public async Task ConfirmUserCompromisedAsync(params string[] userIds)
{
await _graphClient.IdentityProtection.RiskyUsers
.ConfirmCompromised(userIds)
.Request()
.PostAsync();
Console.WriteLine($"Confirmed {userIds.Length} users as compromised");
}
public async Task DismissUserRiskAsync(params string[] userIds)
{
await _graphClient.IdentityProtection.RiskyUsers
.Dismiss(userIds)
.Request()
.PostAsync();
Console.WriteLine($"Dismissed risk for {userIds.Length} users");
}
public async Task AnalyzeRiskPatternsAsync(int daysBack = 7)
{
var cutoffDate = DateTime.UtcNow.AddDays(-daysBack);
var detections = await GetHighRiskDetectionsAsync();
var recentDetections = detections
.Where(d => d.DetectedDateTime.HasValue && d.DetectedDateTime.Value >= cutoffDate)
.ToList();
var summary = new RiskAnalysisSummary
{
TotalDetections = recentDetections.Count,
ByRiskLevel = recentDetections
.GroupBy(d => d.RiskLevel)
.ToDictionary(g => g.Key.ToString(), g => g.Count()),
ByDetectionType = recentDetections
.GroupBy(d => d.RiskEventType)
.ToDictionary(g => g.Key, g => g.Count()),
HighRiskUsers = recentDetections
.Where(d => d.RiskLevel == RiskLevel.High)
.Select(d => d.UserDisplayName)
.Distinct()
.ToList()
};
return summary;
}
}
public class RiskAnalysisSummary
{
public int TotalDetections { get; set; }
public Dictionary ByRiskLevel { get; set; }
public Dictionary ByDetectionType { get; set; }
public List HighRiskUsers { get; set; }
} Monitoring and Operational Excellence
Successful ID Protection implementations require continuous monitoring and optimization. Organizations should establish key performance indicators (KPIs) that measure both security effectiveness and operational efficiency. Security metrics include mean time to detect (MTTD) identity compromises, mean time to remediate (MTTR) confirmed risks, percentage of risks remediated through self-service workflows versus administrator intervention, and false positive rates for different detection types. Operational metrics include user satisfaction scores, help desk ticket volume related to authentication issues, and policy exception requests.
Weekly digest emails from ID Protection provide administrators with summary reports of new risk detections, risky user counts, and policy effectiveness. Organizations should configure these digests to deliver to security operations teams and identity administrators who can triage findings and adjust policies based on emerging patterns. Real-time alerts should be configured for high-severity events such as confirmed compromises of privileged accounts or detection of threat actor IP addresses, ensuring immediate response to critical security incidents.
Azure Monitor workbooks provide visualization dashboards that track risk detection trends, policy enforcement actions, and remediation metrics over time. Organizations should create custom workbooks that align with their specific KPIs and present data in formats that support executive reporting and compliance documentation. These visualizations help justify security investments, demonstrate regulatory compliance, and identify areas requiring additional security controls or user training.
Conclusion
Microsoft Entra ID Protection transforms identity security from reactive incident response to proactive risk management through continuous evaluation, automated remediation, and adaptive access controls. Organizations that successfully implement ID Protection achieve significant reductions in identity-related breaches while maintaining user productivity through intelligent risk-based policies. The platform’s integration with Microsoft Graph APIs enables comprehensive automation workflows that scale across enterprise environments with thousands or millions of identities.
Effective implementation requires careful policy design that balances security requirements with user experience, continuous monitoring and tuning to reduce false positives, integration with SIEM platforms for comprehensive threat correlation, and automated remediation workflows that minimize administrator workload. Organizations should approach ID Protection as a foundational component of Zero Trust architecture, combining identity risk detection with device compliance, application protection, and data security controls to create defense-in-depth strategies that protect against sophisticated attacks targeting the identity layer.
References
- Microsoft Learn – What is Microsoft Entra ID Protection?
- Microsoft Learn – What are risk detections?
- Microsoft Learn – Risk detection types and levels
- Microsoft Learn – Risk-based access policies
- Microsoft Learn – Investigate risk with Microsoft Entra ID Protection
- Microsoft Graph – Identity Protection APIs overview
- Microsoft Learn – Identify and remediate risk using ID Protection APIs
- Microsoft Learn – Detect protected resource risks with ID Protection
- Reco – Microsoft Entra ID Protection Guide for Technical Teams
- DZone – Graph API for Entra ID Object Management
