Use this skill when asked to trace authentication flows, analyze SessionId chains, investigate token reuse vs interactive MFA, or assess geographic anomalies in sign-ins...
This skill performs forensic analysis of Entra ID authentication flows to determine whether anomalous sign-ins represent:
The key distinction is whether the user actively performed MFA at a suspicious location or if the authentication used a refresh token from a prior session.
🚨 MANDATORY CHECKPOINT: Before providing ANY risk assessment for authentication anomalies:
Before executing ANY authentication tracing queries, you MUST:
ip_enrichment array) - PRIMARY DATA SOURCESkipping these steps will result in incomplete or incorrect analysis.
When investigating anomalous sign-ins (e.g., from new countries, IPs, or devices), it's critical to determine whether the user actively performed MFA at that location or if the authentication used a refresh token from a prior session.
| Value | Meaning | Implication |
|---|---|---|
RequestSequence: 1 or higher |
Interactive authentication | User was challenged and responded |
RequestSequence: 0 |
Token-based authentication | No user interaction required |
Interactive Pattern:
RequestSequence > 0Token Reuse Pattern:
authenticationStepDateTime references a time when NO interactive auth occurred, it indicates token reuseRequestSequence > 0 to trace token originCRITICAL: The investigation JSON contains a comprehensive ip_enrichment array with authoritative detection flags.
Always reference this data FIRST before making VPN/proxy/Tor determinations.
{
"ip": "203.0.113.42", // ← KEY: Use "ip" field, not "ip_address"
"city": "Singapore",
"region": "Singapore",
"country": "SG",
"org": "AS12345 Example Hosting Ltd",
"asn": "AS12345",
"timezone": "Asia/Singapore",
"risk_level": "HIGH", // ← Overall risk assessment (LOW/MEDIUM/HIGH)
"assessment": "⚠️ Threat Intelligence Match: Commercial VPN Service Detected",
"is_vpn": true, // ← PRIMARY VPN DETECTION FLAG (ipinfo.io detection)
"is_proxy": false, // ← PRIMARY PROXY DETECTION FLAG
"is_tor": false, // ← PRIMARY TOR DETECTION FLAG
"abuse_confidence_score": 0, // ← AbuseIPDB score 0-100 (0=clean, 75+=high risk)
"total_reports": 2, // ← Number of abuse reports in AbuseIPDB
"is_whitelisted": false,
"threat_description": "Commercial VPN Service: Known Anonymization Infrastructure",
"anomaly_type": "NewInteractiveIP",
"first_seen": "2025-10-16", // ← First sign-in from this IP (date string)
"last_seen": "2025-10-16", // ← Last sign-in from this IP (date string)
"hit_count": 5, // ← Number of anomaly detections
"signin_count": 8, // ← Total sign-ins from this IP
"success_count": 7, // ← Successful authentications
"failure_count": 1, // ← Failed authentications
"last_auth_result_detail": "MFA requirement satisfied by claim in the token",
"threat_detected": false, // ← Legacy field (use threat_description instead)
"threat_confidence": 0,
"threat_tlp_level": "",
"threat_activity_groups": ""
}
CRITICAL: Always use ip_enrichment[].ip to match IPs, NOT ip_address!
| Field | Purpose | Usage Example |
|---|---|---|
| is_vpn | Definitive VPN detection | is_vpn: true → Confirmed VPN endpoint (don't infer, use this flag) |
| is_proxy | Definitive proxy detection | is_proxy: true → Confirmed proxy (anonymized traffic) |
| is_tor | Definitive Tor detection | is_tor: true → Confirmed Tor exit node (high anonymity risk) |
| abuse_confidence_score | AbuseIPDB reputation (0-100) | >= 75 = High risk, >= 25 = Medium risk, 0 = Clean |
| threat_detected | Threat intel match flag | true → IP matches ThreatIntelIndicators table |
| threat_description | Threat intel details | "Surfshark VPN", "Malicious activity detected", etc. |
| org / asn | Network ownership | AS9009 = M247 Europe (VPN infrastructure provider) |
| signin_count | Total sign-ins from IP | High count (>100) = established pattern vs transient |
| last_auth_result_detail | Authentication method | "MFA satisfied by token" vs "Correct password" = interactive vs token reuse |
| first_seen / last_seen | Temporal pattern | Single day = transient, multi-day = established behavior |
is_vpn, is_proxy, is_tor) - Most authoritative sourceabuse_confidence_score, total_reports) - Community-validated risk datathreat_detected, threat_description) - IOC matches from Sentinelorg, asn, company_type) - Infrastructure context (hosting, ISP, etc.)last_auth_result_detail, signin_count) - Behavioral context⚠️ NEVER say "likely VPN" or "probably proxy" if enrichment data has explicit boolean flags!
Scenario: Anomalous sign-ins detected from new IP/location. Determine if user performed fresh MFA or reused token.
| Lookback | Tool | Table | Why |
|---|---|---|---|
| ≤ 30 days (default) | RunAdvancedHuntingQuery |
EntraIdSignInEvents |
Single table covers interactive + non-interactive. No union needed. Direct columns for Country, City, Browser, UserAgent. Free on Analytics tier. |
| > 30 days | mcp_sentinel-data_query_lake |
union SigninLogs, AADNonInteractiveUserSignInLogs |
AH Graph API caps at 30d. Data Lake retains 90d+. See Data Lake Fallback Queries below. |
Column mapping — EntraIdSignInEvents vs SigninLogs:
| EntraIdSignInEvents (AH) | SigninLogs (Data Lake) | Notes |
|---|---|---|
Timestamp |
TimeGenerated |
|
AccountUpn |
UserPrincipalName |
|
Application |
AppDisplayName |
|
ErrorCode (int) |
ResultType (string) |
AH: ErrorCode == 0, DL: ResultType == "0" |
Country, City (direct strings) |
Location or parse_json(LocationDetails) |
No parsing needed in AH |
LogonType (JSON array) |
Separate tables (SigninLogs vs AADNonInteractive) | AH: has "interactiveUser", DL: check which table |
AuthenticationRequirement |
AuthenticationRequirement |
Same values: singleFactorAuthentication, multiFactorAuthentication |
UserAgent, Browser, OSPlatform |
parse_json(DeviceDetail) |
Direct columns in AH |
UniqueTokenId |
(not available) | AH-only — token-level forensics |
SessionId |
SessionId |
Same |
| (not available) | AuthenticationDetails (JSON array) |
DL-only — per-step RequestSequence + authenticationMethod |
Key trade-off:
AuthenticationDetails(Data Lake only) provides per-stepRequestSequenceandauthenticationMethod("Password", "Previously satisfied", "Mobile app notification").EntraIdSignInEventsreplaces this with row-levelLogonType(interactive vs non-interactive) +AuthenticationRequirement(singleFactor/multiFactor) +UniqueTokenId. Both achieve the same forensic goal — determining interactive MFA vs token reuse — through different signals.
⚠️ Table name casing: Capital I in SignIn — EntraIdSignInEvents, NOT EntraIdSigninEvents.
⚠️ LogonType is a JSON array string (e.g., ["interactiveUser"]). Use has for filtering, NOT ==.
CRITICAL: START WITH SessionId - This is Your Primary and Most Efficient Investigation Pattern:
AVOID chronological searching without SessionId - it requires multiple queries and is less efficient.
Tool: RunAdvancedHuntingQuery
This single query gives you SessionId AND enough context to determine next steps:
let suspicious_ips = dynamic(["<IP_1>", "<IP_2>"]); // All suspicious IPs
EntraIdSignInEvents
| where Timestamp > ago(30d)
| where AccountUpn =~ '<UPN>'
| where IPAddress in (suspicious_ips)
| project Timestamp, IPAddress, Country, City, Application,
SessionId, LogonType, AuthenticationRequirement,
UserAgent, Browser, OSPlatform, ErrorCode, UniqueTokenId
| order by Timestamp asc
| take 50
What This Returns:
Critical Decision Point:
Tool: RunAdvancedHuntingQuery
Once you have SessionId from Step 1, query ALL authentications in that session:
let target_session_id = "<SESSION_ID_FROM_STEP_1>";
EntraIdSignInEvents
| where Timestamp > ago(30d)
| where AccountUpn =~ '<UPN>'
| where SessionId == target_session_id
| project Timestamp, IPAddress, Country, City, Application,
LogonType, AuthenticationRequirement, ErrorCode,
UserAgent, Browser, OSPlatform, UniqueTokenId
| order by Timestamp asc
This Single Query Reveals:
LogonType has "interactiveUser" + AuthenticationRequirement == "multiFactorAuthentication")LogonType has "nonInteractiveUser" — all subsequent events using cached tokens)UniqueTokenId — same token reused across IPs = session continuity)Critical Evidence - What SessionId Indicates:
Analysis Pattern:
LogonType has "interactiveUser" → User performed interactive authentication at that IP/locationAuthenticationRequirement → multiFactorAuthentication = MFA was required and satisfied; singleFactorAuthentication = password-onlyLogonType has "nonInteractiveUser" = token reuse (expected OAuth flow)UniqueTokenId — same token ID across geographically distant IPs = session continuity (could be VPN OR stolen token)Tool: RunAdvancedHuntingQuery (≤30d) or Data Lake fallback (>30d)
Use this when Step 2 shows only nonInteractiveUser logon types (no interactive auth in the session)
Query Pattern:
EntraIdSignInEvents
| where Timestamp > ago(30d)
| where AccountUpn =~ '<UPN>'
| where LogonType has "interactiveUser"
| where ErrorCode == 0
| summarize
SignInCount = count(),
Apps = make_set(Application, 5),
Countries = make_set(Country, 3),
AuthReqs = make_set(AuthenticationRequirement),
TokenIds = dcount(UniqueTokenId),
FirstSeen = min(Timestamp),
LastSeen = max(Timestamp)
by IPAddress, SessionId
| order by LastSeen desc
| take 20
What This Returns:
AuthenticationRequirement per IP — reveals whether MFA was required or bypassedTokenIds count — how many distinct tokens were issued from each IP/session pairProgressive expansion (if AH 30d window is insufficient):
Use these when the AH 30d window is insufficient — e.g., tracing token origins older than 30 days.
Tool: mcp_sentinel-data_query_lake with workspaceId
Step 1 (Data Lake):
let suspicious_ips = dynamic(["<IP_1>", "<IP_2>"]);
union isfuzzy=true SigninLogs, AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(90d)
| where UserPrincipalName =~ '<UPN>'
| where IPAddress in (suspicious_ips)
| project TimeGenerated, IPAddress, Location, AppDisplayName,
SessionId = tostring(SessionId), UserAgent, ResultType, CorrelationId
| order by TimeGenerated asc
| take 50
Step 2 (Data Lake) — with per-step auth detail:
let target_session_id = "<SESSION_ID>";
union isfuzzy=true SigninLogs, AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(90d)
| where UserPrincipalName =~ '<UPN>'
| where SessionId == target_session_id
| extend AuthDetails = parse_json(tostring(AuthenticationDetails))
| mv-expand AuthDetails
| extend AuthMethod = tostring(AuthDetails.authenticationMethod)
| extend AuthStepDateTime = todatetime(AuthDetails.authenticationStepDateTime)
| extend RequestSeq = toint(AuthDetails.RequestSequence)
| project TimeGenerated, IPAddress, Location, AppDisplayName,
AuthMethod, AuthStepDateTime, RequestSeq, UserAgent, ResultType
| order by TimeGenerated asc
Data Lake advantage:
AuthenticationDetailsprovides granular per-stepRequestSequenceandauthenticationMethod("Password", "Previously satisfied", "Mobile app notification") not available inEntraIdSignInEvents. Use this for forensic-grade MFA step tracing when AH'sLogonType+AuthenticationRequirementcolumns are insufficient.
Step 3 (Data Lake) — interactive MFA search:
union isfuzzy=true SigninLogs, AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(90d)
| where UserPrincipalName =~ '<UPN>'
| extend AuthDetails = parse_json(tostring(AuthenticationDetails))
| mv-expand AuthDetails
| extend AuthMethod = tostring(AuthDetails.authenticationMethod)
| extend RequestSeq = toint(AuthDetails.RequestSequence)
| where AuthMethod != "Previously satisfied"
| where RequestSeq > 0
| project TimeGenerated, IPAddress, Location, AppDisplayName, AuthMethod,
RequestSeq, SessionId = tostring(SessionId), UserAgent, ResultType
| order by TimeGenerated desc
| take 30
CRITICAL: After completing the SessionId trace, extract ALL unique IP addresses discovered:
Build comprehensive IP list for enrichment analysis.
MANDATORY: Search investigation JSON ip_enrichment array for EVERY IP in the authentication chain:
For each IP address discovered in Steps 1-3:
Locate IP in ip_enrichment array (search by "ip": "<IP_ADDRESS>" field)
Extract key risk indicators:
is_vpn, is_proxy, is_tor (anonymization detection)abuse_confidence_score, total_reports (reputation)threat_description, threat_detected (threat intel matches)org, asn (network ownership - hosting vs ISP)last_auth_result_detail (authentication pattern)signin_count, success_count, failure_count (frequency/behavior)first_seen, last_seen (temporal pattern - transient vs established)Document findings for EACH IP in the chain:
This creates a complete evidence picture showing the full authentication journey with enrichment context.
⚠️ MANDATORY CHECKPOINT - Before writing risk assessment:
Present findings in clear evidence trail:
Risk Assessment Framework - SessionId Interpretation:
Scenario: User sign-ins detected from two geographically distant locations within 18 hours.
Location A Analysis:
SMS verification and RequestSeq: 1authenticationStepDateTime: 2025-10-15T14:23:05Z with RequestSequence: 1Location B Analysis:
"MFA requirement satisfied by claim in the token"Query to compare sessions across both IPs:
let suspicious_ips = dynamic(["<IP_ADDRESS_1>", "<IP_ADDRESS_2>"]);
union isfuzzy=true SigninLogs, AADNonInteractiveUserSignInLogs
| where TimeGenerated between (datetime(<START_DATE>) .. datetime(<END_DATE>))
| where UserPrincipalName =~ '<UPN>'
| where IPAddress in (suspicious_ips)
| project TimeGenerated, IPAddress, Location, SessionId, UserAgent
| order by TimeGenerated asc
CRITICAL FINDING:
<SESSION_ID_EXAMPLE>Initial Appearance: Potential geographic anomaly requiring investigation Further Analysis Required: Correlate SessionId with UserAgent, behavior patterns, and user confirmation
| Evidence Type | Finding | Observation |
|---|---|---|
| Interactive MFA | Location A only | User performed SMS authentication |
| Location B Auth Methods | "Previously satisfied" only | Token reuse (normal OAuth flow) |
| SessionId | Same across both locations | Session continuity maintained |
| Time Gap | 18 hours | Within typical refresh token lifetime (24-90 days) |
| User Agent | Same | Consistent device fingerprint |
| Applications | Consistent across locations | Consistent workflow pattern |
The same SessionId requires careful analysis because:
Possible Scenarios Requiring Investigation:
| Scenario | Description | Action Required |
|---|---|---|
| Legitimate VPN Connection | User switched VPN exit nodes (same device, different apparent location) | Requires user confirmation |
| Legitimate User Travel | User traveled between locations with sufficient time gap (tokens remained valid) | Requires user confirmation |
| Multi-Device User | User has laptop + phone active simultaneously (different IPs, concurrent activity) | Check UserAgent for mobile vs desktop - Requires user confirmation |
| Stolen Token Replay | Attacker obtained refresh token (SessionId stays same, may show different UserAgent) | Cannot be ruled out by SessionId alone |
| Mobile Carrier Routing | Carrier routes traffic through regional gateways (device in one location, exits another) | Check IP enrichment for ISP org |
ip_enrichment array to verify:is_vpn, is_proxy, is_tor)abuse_confidence_score, total_reports)threat_detected, threat_description)last_auth_result_detail, signin_count, success_count, failure_count)first_seen, last_seen - transient vs established pattern)Use IP enrichment data from investigation JSON to strengthen your analysis, then confirm with user:
is_vpn: true)Only after user confirmation can you conclude VPN usage or travel is legitimate. Same SessionId + IP enrichment data together provide strong evidence, but user confirmation is still required.
| Authentication Method | RequestSeq > 0 Meaning | RequestSeq = 0 Meaning |
|---|---|---|
| Passkey (device-bound) | User physically approved with biometric/PIN | Passkey used in prior session, token reused |
| Phone sign-in | User approved notification on phone | Phone approval in prior session, token reused |
| SMS verification | User entered SMS code | SMS verification in prior session, token reused |
| Microsoft Authenticator app | User approved push notification | Authenticator used in prior session, token reused |
| Previously satisfied | N/A - never has RequestSeq > 0 | Always indicates token/claim reuse |
CRITICAL: Always check IP enrichment data before making risk determination!
threat_detected: true in IP enrichmentabuse_confidence_score >= 75, is_tor: true, or malicious threat_descriptionabuse_confidence_score >= 25, is_vpn: true without user confirmation, or total_reports > 0abuse_confidence_score: 0, residential ISP org (TELUS, Comcast, etc.), is_vpn: false, high signin_count with consistent success rateip_enrichment array for VPN, abuse scores, threat intelThis skill requires:
RunAdvancedHuntingQuery for EntraIdSignInEventsmcp_sentinel-data_query_lake for SigninLogs + AADNonInteractiveUserSignInLogs unionAuthenticationDetails per-step granularity is neededip_enrichment array (from user-investigation skill)temp/investigation_<upn_prefix>_<timestamp>.jsonfile_search or list_dir to locate existing investigationsAuthentication tracing is typically performed as a follow-up analysis after running a user investigation:
ip_enrichment arrayKey Integration Points: