.github/skills/authentication-tracing/SKILL.md
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. Triggers on keywords like "trace authentication", "trace back to interactive MFA", "SessionId analysis", "token reuse", "geographic anomaly", "impossible travel", or when investigating suspicious sign-in locations. This skill provides forensic analysis of Entra ID authentication chains to distinguish legitimate activity from credential/token theft.
npx skillsauth add scstelz/security-investigator authentication-tracingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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:
development
Use this skill when asked to investigate a computer, device, endpoint, or machine for security issues, suspicious activity, malware, or compliance review. Triggers on keywords like "investigate computer", "investigate device", "investigate endpoint", "check machine", "device security", "endpoint investigation", or when a device name/hostname is mentioned with investigation context. This skill provides comprehensive device security analysis including Defender alerts, sign-in patterns, logged-on users, vulnerabilities, software inventory, compliance status, network activity, and automated investigation tracking for Entra Joined, Hybrid Joined, and Entra Registered devices.
development
Recommended starting point for new users and daily SOC operations. Quick 15-minute security posture scan across 7 domains: active incidents, identity (human + NonHuman), endpoint, email threats, admin & cloud ops, and exposure. 12 queries executed in parallel batches, producing a prioritized Threat Pulse Dashboard with color-coded verdicts (🔴 Escalate / 🟠 Investigate / 🟡 Monitor / ✅ Clear) and drill-down recommendations pointing to specialized skills. Trigger on getting-started questions like "what can you do", "where do I start", "help me investigate". Supports inline chat and markdown file output
development
Use this skill when asked to investigate a user account for security issues, suspicious activity, or compliance review. Triggers on keywords like "investigate user", "security investigation", "user investigation", "check user activity", "analyze sign-ins", or when a UPN/email is mentioned with investigation context. This skill provides comprehensive Entra ID user security analysis including sign-in anomalies, MFA status, device compliance, audit logs, security incidents, Identity Protection risk, and automated reports (HTML, markdown file, or inline chat).
development
Use this skill when asked to generate SVG data visualization dashboards from investigation data or skill reports. Triggers on keywords like "generate SVG dashboard", "create a visual dashboard", "visualize this report", "SVG from the report", "visualize results", "create SVG chart", "SVG from this data". Supports two modes: manifest-driven structured dashboards (from skill reports with svg-widgets.yaml) and freeform adaptive visualizations from ad-hoc investigation data. Component library includes KPI cards, score cards, bar charts, line charts, donut charts, waterfall charts, tables, recommendation cards, assessment banners. SharePoint Dark Theme default palette.