Security Hardening Guide: SANS / Threat Intel — Even MOAR Powershell, looking at Entra

Share
Security Hardening Guide: SANS / Threat Intel — Even MOAR Powershell, looking at Entra
Modern Endpoint · Security Insights

Security Hardening Guide: SANS / Threat Intel — Even MOAR Powershell, looking at Entra

I was on a call last year with a security team that had just passed their SOC 2 audit. They were proud. Their Conditional Access policies were tight, their MFA was enforced, and their Entra ID logs were flowing into Sentinel. Then someone asked: "What does your persistent access token monitoring look like?" Silence. Nobody had written a query for that. Nobody owned it. The tokens were 24 hours long and nobody knew when they were issued, to whom, or whether they had ever been revoked. They had great controls. They had almost no visibility.

16 min read ArticleModernEndpoint

🔍 Why Entra ID Is the Threat Intel Focal Point

Entra ID is not just an identity provider. It is the trust broker for your entire Microsoft 365 and Azure estate. Every authentication event, every token issuance, every service principal action generates a signal. The problem is that most organizations are collecting those signals and not reading them.

SANS threat intelligence guidance consistently identifies identity as the primary initial access vector. Credential abuse, token theft, adversary-in-the-middle (AiTM) phishing, and OAuth application consent abuse all manifest first in Entra ID telemetry. Before they show up in your endpoint logs. Before they show up in email alerts. Entra sees it first.

⚡ Assumption Challenge
Most organizations believe: "We have MFA enabled, so our identity layer is secure."
Reality: MFA stops password spray. It does not stop token theft, AiTM phishing, session cookie hijacking, or OAuth consent abuse. Each of these bypasses MFA at the authentication layer and operates on the downstream token. Entra logs contain the evidence — but only if someone is reading them."

The Microsoft Entra sign-in logs and audit logs are the raw material. What turns them into threat intelligence is query discipline, automation, and a defined response process. That is what this guide builds.

---

🧩 The SANS Framework Mapping to Entra Signals

SANS threat intelligence tradecraft focuses on indicators of compromise, attacker TTPs, and detection coverage across the MITRE ATT&CK framework. When you map that to Entra ID, three signal categories dominate:

SANS/MITRE TTPEntra SignalLog Source
Initial Access - Valid Accounts (T1078)Sign-in from new location, impossible travelSigninLogs
Credential Access - Token Theft (T1528)Refresh token usage without corresponding authSigninLogs, AADServicePrincipalSignInLogs
Persistence - OAuth App Consent (T1550.001)New delegated permission grantsAuditLogs
Privilege Escalation - Account Manipulation (T1098)Role assignment changesAuditLogs
Defense Evasion - Disable MFA (T1562)Authentication method changesAuditLogs
Discovery - Account Discovery (T1087)Bulk read operations via GraphAADServicePrincipalSignInLogs

This table is not academic. Each row represents an attacker behavior you should have an active KQL query monitoring. If you do not have a query for it, you have a detection gap — and SANS will tell you that detection gaps are what threat actors document before they move.

⚡ Assumption Challenge
Most organizations believe: "Our SIEM covers identity threats because we ingest sign-in logs."
Reality: Ingesting logs is not detection. Detection requires queries, thresholds, baselines, and alert ownership. I have reviewed Sentinel workspaces with 90 days of Entra logs and zero active scheduled analytics rules against them."

---

💡 PowerShell Foundations: Connecting to Microsoft Graph for Entra Hardening

Before any query runs, you need a clean, auditable connection to the Microsoft Graph. I always push teams toward the Microsoft Graph PowerShell SDK over the legacy MSOL and AzureAD modules. Those are deprecated. Any script using them is accumulating operational debt.

powershell
Install-Module Microsoft.Graph -Scope CurrentUser -Force

Connect-MgGraph -Scopes `
    "User.Read.All", `
    "AuditLog.Read.All", `
    "Directory.Read.All", `
    "Policy.Read.All", `
    "RoleManagement.Read.Directory", `
    "IdentityRiskEvent.Read.All"

Get-MgContext
Note

The Microsoft Graph PowerShell SDK replaced the deprecated AzureAD and MSOnline modules. Microsoft's official migration guide is at https://learn.microsoft.com/en-us/powershell/microsoftgraph/migration-steps. Do not mix old and new modules in the same script — the behavior is unpredictable when both are loaded.

---

🔐 Hardening Query 1: Privileged Role Assignments Without PIM

This is the first check I run in every new tenant engagement. Permanent privileged role assignments outside of Privileged Identity Management (PIM) are standing attack surface. Every global admin who is permanently assigned rather than activated through PIM is a credential that an attacker can abuse indefinitely without triggering an activation alert.

powershell
$permanentAdmins = Get-MgDirectoryRoleAssignment -All | ForEach-Object {
    $role = Get-MgDirectoryRole -DirectoryRoleId $_.RoleDefinitionId
    $principal = Get-MgDirectoryObject -DirectoryObjectId $_.PrincipalId
    [PSCustomObject]@{
        RoleName      = $role.DisplayName
        PrincipalId   = $_.PrincipalId
        PrincipalType = $principal.AdditionalProperties["@odata.type"]
        PrincipalName = $principal.AdditionalProperties["displayName"]
    }
} | Where-Object { $_.RoleName -match "Admin|Global|Security|Privileged" }

$permanentAdmins | Sort-Object RoleName | Format-Table -AutoSize

Export this to CSV and count the permanent assignments against your expected PIM-activated role list. Every gap is a finding.

powershell
$permanentAdmins | Export-Csv -Path ".\PermanentRoleAssignments_$(Get-Date -Format yyyyMMdd).csv" -NoTypeInformation
Warning

Service accounts and break-glass emergency access accounts are the common exceptions to PIM enforcement. Both must be documented, monitored via alert rules, and reviewed quarterly. Do not exclude them silently — exclusions that are not documented become the attack path.

🔍 Reality Check
What most organizations believe: PIM is enabled and covering privileged roles because the license includes it.
What actually happens in production: PIM requires explicit onboarding of each role. Organizations with E5 licenses frequently have PIM available but not configured, while dozens of permanent role assignments persist from the original tenant setup. The license does not activate the governance."

---

📊 KQL: Detecting Role Assignment Changes Outside Business Hours

Role assignment changes should happen through a defined change management process. When they happen outside business hours, they warrant immediate investigation. This query looks for role assignment events between 10 PM and 6 AM in your local timezone, adjusted for UTC.

kql
AuditLogs
| where TimeGenerated > ago(30d)
| where OperationName has_any ("Add member to role", "Remove member from role", "Add eligible member to role")
| extend InitiatedBy = tostring(InitiatedBy.user.userPrincipalName)
| extend TargetUser = tostring(TargetResources[0].userPrincipalName)
| extend RoleName = tostring(TargetResources[0].displayName)
| extend HourUTC = datetime_part("Hour", TimeGenerated)
// Flag events outside 06:00-22:00 UTC
| where HourUTC < 6 or HourUTC > 22
| project TimeGenerated, OperationName, InitiatedBy, TargetUser, RoleName, Result
| order by TimeGenerated desc

This query feeds directly into a Sentinel scheduled analytics rule. Set it to run every hour with a lookback of 1 hour. Alert on any result.

---

🔐 Hardening Query 2: Legacy Authentication Sign-Ins Still Active

Legacy authentication protocols (Basic Auth, SMTP AUTH, IMAP, POP3) do not support modern MFA challenges. Any sign-in using these protocols bypasses your Conditional Access policies entirely. SANS identifies this as a primary credential abuse enabler.

powershell
$filter = "createdDateTime ge " + (Get-Date).AddDays(-7).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") + " and clientAppUsed ne 'Browser' and clientAppUsed ne 'Mobile Apps and Desktop clients'"

$legacySignIns = Get-MgAuditLogSignIn -Filter $filter -All | Where-Object {
    $_.ClientAppUsed -match "IMAP|POP3|SMTP|Basic Auth|Exchange ActiveSync|Other clients"
} | Select-Object CreatedDateTime, UserPrincipalName, ClientAppUsed, AppDisplayName, IpAddress, Location

$legacySignIns | Group-Object ClientAppUsed | Select-Object Name, Count | Sort-Object Count -Descending

If this returns results, your Conditional Access block policy for legacy authentication is either missing or has a scope gap. Block it. There is no legitimate enterprise scenario for basic auth in 2025 that justifies the risk.

kql
// KQL equivalent for Sentinel monitoring
SigninLogs
| where TimeGenerated > ago(7d)
| where ClientAppUsed has_any ("IMAP", "POP3", "SMTP Auth", "Exchange ActiveSync", "Other clients")
| summarize SignInCount = count(), 
            UniqueUsers = dcount(UserPrincipalName),
            UniqueIPs = dcount(IPAddress)
            by ClientAppUsed, bin(TimeGenerated, 1d)
| order by SignInCount desc
Danger

If you find active legacy auth sign-ins from service accounts or shared mailboxes, do NOT block them without investigating first. Blocking legacy auth without a full inventory causes immediate operational disruption to automated workflows, on-premises relay connectors, and legacy line-of-business applications. Build the inventory before you build the block policy.

---

🚨 Hardening Query 3: Risky Sign-In Detection with Identity Protection

Microsoft Entra ID Protection generates risk signals for every sign-in and every user. These signals are the output of Microsoft's global threat intelligence — billions of signals processed daily. If you have E5 or Entra ID P2, you have access to this data. If you are not querying it, you are leaving your best identity threat intelligence sitting unread.

powershell
$riskFilter = "riskLevelDuringSignIn eq 'high' or riskLevelDuringSignIn eq 'medium'"
$riskySignIns = Get-MgAuditLogSignIn -Filter $riskFilter -All | 
    Select-Object CreatedDateTime, UserPrincipalName, RiskLevelDuringSignIn, 
                  RiskDetail, RiskState, IpAddress, Location, AppDisplayName |
    Sort-Object CreatedDateTime -Descending

Write-Host "Risky Sign-Ins Last 48h: $($riskySignIns.Count)"
$riskySignIns | Export-Csv -Path ".\RiskySignIns_$(Get-Date -Format yyyyMMdd).csv" -NoTypeInformation
kql
// KQL: Risky users who successfully signed in despite risk
AADRiskyUsers
| where RiskLevel in ("high", "medium")
| where RiskState == "atRisk"
| join kind=inner (
    SigninLogs
    | where TimeGenerated > ago(24h)
    | where ResultType == 0 // Successful sign-in
    | project SignInTime = TimeGenerated, UserPrincipalName, AppDisplayName, IPAddress
) on UserPrincipalName
| project RiskLevel, RiskState, UserPrincipalName, RiskDetail, SignInTime, AppDisplayName, IPAddress
| order by SignInTime desc

This query surfaces the most dangerous condition: a user flagged as risky who is still successfully authenticating. That means your Conditional Access risk-based policies are either not configured, not scoped correctly, or have an exclusion that is covering this user.

"A risky user who can still sign in is not a detection — it is a gap in your enforcement chain."

---

⚠️ Hardening Query 5: Stale Guest Accounts and External Identities

Guest account lifecycle residue is an underestimated attack surface. Former partners, contractors, and project collaborators accumulate in your tenant long after their engagement ends. Each stale guest is a potential reentry point if their parent organization is compromised.

powershell
$cutoffDate = (Get-Date).AddDays(-90).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")

$staleGuests = Get-MgUser -Filter "userType eq 'Guest'" -All -Property DisplayName, UserPrincipalName, SignInActivity, CreatedDateTime |
    Where-Object {
        $_.SignInActivity.LastSignInDateTime -lt (Get-Date).AddDays(-90) -or
        $_.SignInActivity.LastSignInDateTime -eq $null
    } |
    Select-Object DisplayName, UserPrincipalName, 
                  @{N="LastSignIn"; E={$_.SignInActivity.LastSignInDateTime}},
                  CreatedDateTime

Write-Host "Stale Guest Accounts: $($staleGuests.Count)"
$staleGuests | Export-Csv -Path ".\StaleGuests_$(Get-Date -Format yyyyMMdd).csv" -NoTypeInformation
⚡ Assumption Challenge
Most organizations believe: "Guest accounts are low risk because guests have limited permissions."
Reality: Guests inherit team memberships, SharePoint site access, and any group-based access granted during their engagement. If those permissions were not removed when the project ended, the guest account is sitting on live data access. I have found guest accounts with three-year-old last sign-ins that still had contributor access to Azure subscriptions."

---

🚫 What This Technology Does NOT Solve

PowerShell against Graph and KQL in Sentinel are detection and visibility tools. They surface signals. They do not remediate.

Identity Protection risk signals do not auto-remediate unless you configure risk-based Conditional Access policies. Seeing a risky user in a query and acting on it manually does not scale. You need automated enforcement tied to the risk signal.

Graph PowerShell does not replace a SIEM. Running these scripts manually on a schedule is operationally fragile. These queries belong in Sentinel as scheduled analytics rules with defined incident creation, alert grouping, and playbook response. Scripts are the prototyping layer. Sentinel is the production layer.

Entra ID Protection only assesses sign-in and user risk. It does not assess configuration risk (your Conditional Access gaps), OAuth application risk (consent abuse in progress), or privileged identity risk (permanent assignments). Each of those requires separate query coverage.

Conditional Access does not classify data. I include this because teams frequently assume that if authentication is controlled, data access is controlled. CA governs who can authenticate and from where. It has no visibility into what data is accessed after authentication succeeds. That is a Purview problem, not an Entra problem.

⚖️ Trade-Off
Implementing scheduled Sentinel analytics rules for all of these queries increases your alert volume. That is the operational cost. If your SOC does not have a defined triage process for identity alerts, you will create alert fatigue before you create detection coverage. Build the triage playbook before you build the alert rules — otherwise the alert rules get muted within 30 days.

---

📊 Monitoring Architecture: From Signal to Incident

🏗 Entra Threat Signal Architecture
🔍
Signal Sources
Raw telemetry from authentication, authorization, and configuration events across the Entra plane.
SigninLogsAuditLogsAADRiskyUsersServicePrincipalSignInLogs
⚙️
Detection Layer
Scheduled KQL analytics rules in Sentinel with defined alert thresholds and incident creation logic.
Sentinel Analytics RulesThreat Intelligence MatchingAlert Correlation
🛡
Response Layer
Automated playbooks for high-confidence signals, manual triage for ambiguous signals, defined escalation paths.
Logic App PlaybooksSOC TriageIdentity Team Escalation
📋
Governance Layer
Weekly signal review, monthly detection coverage assessment, quarterly SANS framework mapping update.
Detection ReviewCoverage MappingPolicy Lifecycle

---

🏗️ Production Lifecycle

⏱ Production Lifecycle
Day 1
PowerShell scripts run manually to establish a baseline. Permanent role assignments documented. Stale guest inventory exported. Legacy auth sign-ins identified. KQL queries saved as workbook queries but not yet scheduled as analytics rules. Alert volume is unknown — do not create scheduled rules without first understanding the baseline noise level.
Month 6
Analytics rules are live in Sentinel with tuned thresholds. The legacy auth query is firing daily — probably because there are still service accounts on SMTP AUTH that nobody documented in Month 1. OAuth consent monitoring has surfaced two consent grants that required remediation. Stale guest review is running monthly via Logic App. The detection coverage gap is still the service principal sign-in logs — most teams deprioritize those in favor of user sign-in coverage.
Year 2
The queries that were written in Month 1 are still running, but the threat landscape has shifted. AiTM phishing is now the primary attack vector and the original sign-in queries are not catching token theft from successful AiTM sessions. A second detection coverage assessment against the current MITRE mapping is overdue. The guest lifecycle automation is working but guest access review governance is still manual — that is the operational debt that accumulated because nobody built the approval workflow in the first six months.

---

🔐 KQL: AiTM Phishing Detection via Token Anomalies

Adversary-in-the-Middle phishing is the current dominant attack pattern against MFA-protected tenants. The attacker proxies the authentication session, captures the session cookie, and replays it from a different IP address. The sign-in succeeds because MFA was satisfied during the proxy session. The anomaly is the IP address change between the authentication event and the session usage.

kql
// Detect sign-ins where session IP differs significantly from previous session
let timeWindow = 1h;
SigninLogs
| where TimeGenerated > ago(7d)
| where ResultType == 0
| where AuthenticationRequirement == "singleFactorAuthentication" or 
        AuthenticationRequirement == "multiFactorAuthentication"
| extend SessionId = tostring(SessionId)
| summarize 
    SignInIPs = make_set(IPAddress), 
    SignInCount = count(),
    FirstSignIn = min(TimeGenerated),
    LastSignIn = max(TimeGenerated)
    by UserPrincipalName, bin(TimeGenerated, timeWindow)
| where array_length(SignInIPs) > 2
| project TimeGenerated, UserPrincipalName, SignInIPs, SignInCount
| order by SignInCount desc

This query is a starting point, not a final detection. AiTM detection at depth requires correlating the Defender for Office 365 phishing signals with the Entra sign-in anomaly. The full detection chain is documented in the Microsoft Sentinel threat hunting playbooks at https://learn.microsoft.com/en-us/azure/sentinel/hunting.

🔍 Reality Check
What most organizations believe: MFA blocks AiTM attacks because the attacker cannot satisfy the MFA challenge.
What actually happens in production: AiTM proxies the entire authentication session including the MFA challenge. The user completes MFA against the proxy, not the attacker. The attacker captures the post-MFA session token and replays it. Phishing-resistant MFA (FIDO2, Windows Hello for Business) is the only authentication method that structurally defeats this attack — because the credential is bound to the origin domain and cannot be proxied. Everything else is detective, not preventive."

---

🎯 Enterprise Decision Points

🎯 Enterprise Decision Point
Before deploying these queries as production Sentinel analytics rules, decide who owns the resulting incidents. Identity incidents in Sentinel that land in a generic SOC queue without a defined identity team escalation path will be triaged by analysts who do not have the Entra context to evaluate them correctly. Define the ownership model before the alert fires — not after. The decision: Does your SOC own identity incidents, or does your identity team own them with SOC escalation for high severity? Both models work. No model fails faster than a defined alert with undefined ownership.
🎯 Enterprise Decision Point
Decide whether you are building a detection library or a detection program. A detection library is a collection of KQL queries — it exists, it documents coverage, it does not operate by itself. A detection program has ownership, review cycles, coverage assessments against MITRE, alert tuning processes, and escalation playbooks. Most organizations build a library and call it a program. The test: if the analyst who wrote the queries leaves tomorrow, does the detection capability survive? If the answer is no, you have a library.

---

🎯 Final Architect Recommendation

Deploy the PowerShell baseline assessment scripts against every tenant you manage, immediately. The stale guest inventory and permanent role assignment audit will surface findings in every environment. That is not a prediction — I have run these scripts in more than forty enterprise tenants and found material findings in all of them. The queries are fast, non-invasive, and require only read permissions.

For the Sentinel analytics rules: do not enable them all at once. Start with the role assignment change detection and the OAuth consent monitoring. These two have the highest signal-to-noise ratio and the clearest remediation path. Tune them for two weeks before adding legacy auth monitoring and risky user sign-in correlation. Alert fatigue is real. A SOC that mutes identity alerts because they are too noisy is worse than a SOC with no alerts at all — because the muting creates a false confidence that nothing is happening.

Phishing-resistant MFA is the architectural recommendation I make to every organization running AiTM exposure analysis. FIDO2 security keys or Windows Hello for Business should be on the roadmap for any identity with privileged access or access to sensitive data. The detection queries I have provided will surface AiTM attempts. They will not prevent them. Only the authentication architecture prevents them.

Finally: make the detection coverage assessment a quarterly governance artifact, not a project deliverable. The threat landscape changes faster than most annual review cycles. Map your active analytics rules to the current MITRE ATT&CK framework every quarter. Identify gaps. Close the highest-priority gaps before the next review. That is a detection program.

"A detection gap documented and unresolved is not a known risk — it is a scheduled incident."

---

🎯 The Takeaway

  • If you have not run a permanent role assignment audit this quarter, run it before anything else. Every enterprise tenant I have assessed has had undocumented permanent privileged assignments. Each one is standing attack surface that Conditional Access does not mitigate.
    • Always validate that Entra ID Protection risk signals are connected to enforcing Conditional Access policies. Visibility without enforcement is documentation, not security. A risky user who can still authenticate has not been detected — the system has simply logged the problem.
      • If legacy authentication sign-ins are still active in your tenant, block them after building a full inventory. There is no modern enterprise use case for basic auth. The block policy should exist. The inventory must come first.
        • Always treat OAuth application consent grants as a privileged operation. Every non-Microsoft application granted delegated mail, file, or contact permissions is a persistent access foothold that survives password resets and MFA policy changes. Admin consent workflow is the control — deploy it.
          • If your detection queries live in a workbook but not in scheduled analytics rules with defined incident ownership, you have a library, not a detection program. Promote your highest-fidelity queries to production analytics rules this week. Name an owner for every alert type before it fires.
          • ---

            Reference Documentation:

            • Microsoft Graph PowerSh

Read more