From Phishing to NIS2: Detecting and Responding to Modern Email Compromise
Threat
You’ve probably heard about the large phishing campaign in Norway recently. While phishing campaigns are nothing new, this one stood out for a few important reasons.
First, parts of the attack leveraged Norwegian infrastructure. This meant that both the phishing emails and the subsequent logins appeared to originate from within Norway. Second, the campaign was highly automated, allowing attackers to operate at significant speed and scale. As a result, several users in large organizations were successfully compromised, which led to rapid spread of the attack.
With that said, there was nothing particularly from a technical perspective — the emails contained SharePoint invitations with links carrying AiTM (Adversary-in-the-Middle) payloads.
This changes some common assumptions:
A phishing email may now come from a trusted or previously known sender—the attacker could be using a legitimately compromised account.
Authentication attempts may originate from trusted geographic locations. If a login comes from Norway, it’s no longer safe to assume it is legitimate.
In other words:
Known sender ≠ trusted identity
IP from trusted geolocation ≠ safe login
Traditional trust signals are increasingly unreliable
Modern phishing attacks exploit trust, not just technical vulnerabilities.
Prevention
In highly organized phishing campaigns, only a few measures truly make a difference: phishing-resistant authentication and user training.
Phishing-resistant authentication should be the default today. The technology is already in place, with multiple options available, and in the vast majority of cases there are no real downsides for end users.
User training. Yes — no technical person enjoys dealing with it. And delivering effective, regular training isn’t easy. There are solutions that can automate this process—imperfect and not cheap, but they work. The goal is simple: create awareness and encourage internal discussions about phishing so users stay alert.
Combining strong authentication with ongoing awareness training gives organizations the best chance to stop attacks before damage occurs.
Detection
Here’s the fun (technical) part: in a Microsoft XDR environment, we can trigger a detection rule to capture the user’s IP when a SharePoint link is clicked, and then correlate it with the IPs used for subsequent logins. In our case, we look ahead up to 30 minutes, as defined in the KQL query.
All KQL queries in this article are very basic and should be tested and adjusted to your tenant environment before being used for detection and response. Otherwise, you should expect some false positives.
Here is a KQL (Sentinel) query example:
// Description: Correlate SharePoint email link clicks with subsequent Azure AD sign-ins from a different IP address to detect possible phishing, token theft, or adversary-in-the-middle activity
// Category: Identity
// Tags: Phishing, AiTM, SuspiciousSignIn, UrlClickCorrelation, SharePoint, TokenTheft
let WhitelistIP = dynamic(["0.0.0.0"]);
let LookBack = 30m;
let Clicks = UrlClickEvents
| where TimeGenerated >= ago(LookBack)
| where Workload == "Email"
| where Url has "sharepoint.com"
| project ClickTime = TimeGenerated, AccountUpn, Url, UrlChain, ClickIP = IPAddress;
let SignIns = union isfuzzy=true
(SigninLogs
| where TimeGenerated >= ago(LookBack)
| project
SignInTime = TimeGenerated,
UserPrincipalName,
SignInIP = tostring(IPAddress),
Timestamp = TimeGenerated,
ReportId = tostring(Id),
ResourceDisplayName,
IncomingTokenType,
IsInteractive,
SignInSource = "SigninLogs"),
(AADNonInteractiveUserSignInLogs
| where TimeGenerated >= ago(LookBack)
| project
SignInTime = TimeGenerated,
UserPrincipalName,
SignInIP = tostring(IPAddress),
Timestamp = TimeGenerated,
ReportId = tostring(Id),
ResourceDisplayName,
IncomingTokenType,
IsInteractive,
SignInEventTypes,
SignInSource = "AADNonInteractiveUserSignInLogs")
| where isnotempty(SignInIP)
| where not(set_has_element(WhitelistIP, SignInIP));
Clicks
| join kind=inner SignIns on $left.AccountUpn == $right.UserPrincipalName
| where isnotempty(ClickIP) and isnotempty(SignInIP)
| where SignInTime > ClickTime
| where ClickIP != SignInIP
| extend DelayMinutes = datetime_diff("minute", SignInTime, ClickTime)
| summarize arg_min(DelayMinutes, *) by AccountUpn, ClickTime, Url, ClickIP
| project
Timestamp,
ReportId,
AccountUpn,
ClickTime,
SignInTime,
DelayMinutes,
ClickIP,
SignInIP,
SignInSource,
IsInteractive,
IncomingTokenType,
SignInEventTypes,
ResourceDisplayName,
Url,
UrlChainSadly, this is still a detection, not truly real-time. Even when using Sentinel NRT (near real-time detection rules), which run once per minute, logging delays mean that “near real-time” often becomes closer to five minutes. In practice, anything that relies on logs in Sentinel or Microsoft XDR has a minimum delay of around five minutes. As a result, any fully automated response based on this data is typically at least four minutes too late once a machine-speed attack is already in progress.
Another key control is limiting outgoing emails, either per user or per user group. This helps reduce the blast radius if an account is compromised — and can even stop the spread once recipient limits are reached.
To establish a baseline, you can use the following KQL queries:
Top emails per day:
// Description: Identify outbound email senders with the highest single-day sending volume over the last 60 days
// Category: Email
// Tags: OutboundEmail, VolumeAnalysis, SenderProfiling, AnomalyDetection
EmailEvents
| where TimeGenerated > ago(60d)
| where EmailDirection == "Outbound"
| summarize EmailCount = count() by SenderFromAddress, Day = bin(TimeGenerated, 1d)
| summarize TopDailyCount = max(EmailCount) by SenderFromAddress
| sort by TopDailyCount descTop emails per hour:
// Description: Identify outbound email senders with the highest single-hour sending volume over the last 60 days
// Category: Email
// Tags: OutboundEmail, VolumeAnalysis, SenderProfiling, AnomalyDetection
EmailEvents
| where TimeGenerated > ago(60d)
| where EmailDirection == "Outbound"
| summarize EmailCount = count() by SenderFromAddress, Hour = bin(TimeGenerated, 1h)
| summarize TopHourlyCount = max(EmailCount) by SenderFromAddress
| sort by TopHourlyCount descA practical approach is to add around 20% on top of these baselines, then adjust—or create new—anti-spam policies accordingly in the Microsoft 365 Defender portal - https://security.microsoft.com/antispam
Response
In the best-case scenario, an automated response based on detections in Sentinel or XDR will only stop a manual attack. So, if a user has been compromised, whether you’re dealing with an automated or manual response, you should:
Change the user’s password
Revoke active sessions
Deactivate the user account
Check for newly added authentication methods
Review logs for actions taken after the compromise (apps accessed, Graph API endpoints, Office 365 activity, mailbox configuration changes such as forwarding rules, changes in Azure (if the user had access there)—anything you can find about the user in Sentinel/XDR)
NIS2: Reporting Obligations
NIS2 has not yet been fully implemented in Norwegian law, but it is clearly on the way. Norway’s digitalsikkerhetslov entered into force on 1 October 2025 and introduced the original NIS framework, while NIS2 is expected later. Preparing now is still highly relevant, because the overall direction and core requirements are unlikely to become less demanding.
Under NIS2, reporting obligations apply to significant incidents affecting services covered by the regulation, as well as incidents that cause or may cause severe financial loss or considerable damage to others. In this context, the compromised user’s level of access is an important factor, because it helps determine the potential impact of the incident.
Remember: an email compromise does not automatically mean that only the mailbox is affected. The real concern is access to SharePoint, OneDrive, and other related applications.
Log analysis is essential here—you need to investigate what actions occurred, which systems and data were accessed and from which IP addresses. Access to highly sensitive information is an important factor in assessing the impact of the incident, because it may increase the likelihood that the incident meets the reporting threshold.
So, back to reporting:
Early warning: usually within 24 hours of becoming aware of a significant incident
Incident notification: within 72 hours, with additional context, impact assessment, and mitigation steps
Final report: typically within one month after the incident notification
NIS2: Must-Haves & Evidence
When things don’t go as expected and you face a supervisory review, inspection, or audit, you need to demonstrate due diligence. That means collecting and retaining evidence:
Detection and investigation logs: URL clicks, sign-ins, alerts, and other relevant telemetry
Timeline of events: from detection to containment, remediation, and recovery
Scope of compromise: what the user had access to, which systems and data were accessed, and the classification or criticality of the information involved
Security and authentication documentation: relevant policies, access-control settings, and user training records
Response records and procedures: session revocations, password resets, containment actions, and related response steps
Takeaways
Logging is critical: without logs, your SOC and automation are blind. Make sure you collect enough data to support effective detection and response.
Not everything is out of the box: some response actions require scripting and API integration—but they are achievable.
Detection and response are not enough: you must document everything.
Evidence matters: it should be timestamped, verifiable, and correlated across logs.
High-value data matters most: access often determines whether an incident is reportable, and data classification is often the hardest part.
