Detecting Agentic Browser Exfiltration in the M365 Audit Stream
The interesting thing about agentic browser assistants — the Copilot sidebar that will now happily click links for you, the Atlas-style agents, the half-dozen Chrome extensions that wrap an LLM around the DOM — is that they make the classic DLP question obsolete. There is no payload to inspect on egress. The user (or an attacker who prompt-injected the page the agent just read) tells the agent in natural language to summarize the contents of a SharePoint document and paste the summary into a Gmail draft, or a Teams chat to an external tenant, or a comment field on a third-party SaaS app. The bytes that leave the endpoint are an LLM-paraphrased version of the source. No regex on the proxy will catch that. Your CASB’s content classifier won’t either, because the agent has already rewritten the sensitive sentence into something that no longer trips the dictionary.
This is a detection-engineering problem, not a control problem. The control side is mostly already lost — you provisioned the agent, the user has legitimate access to the document, and the agent inherited that access via OBO tokens. What’s left is figuring out, from telemetry, when an agent did something a human wouldn’t have, or did it on behalf of an instruction that didn’t come from the human.
Where the signal actually lives
For M365 Copilot specifically, the useful telemetry is in the Unified Audit Log under Operation = "CopilotInteraction". The event includes an AISystemPlugin array (which connectors the agent invoked), an AppHost (Word, Outlook, BizChat, Edge sidebar, Teams), a Contexts array describing the documents pulled into the prompt, and an AccessedResources list of the file URIs the agent touched in producing its response. Crucially it does not include the prompt text or the response text by default — Purview’s audit retention strips both unless you’ve turned on the Premium tier with Communication Compliance hooked up, and even then the storage cost makes most shops sample.
That missing prompt text is the entire problem. You can see that Copilot touched a document, which document, from where, and roughly when. You cannot see what it was asked or what it produced. So the detection has to be built on access shape, not content.
The shape that matters: an agent session that pulls from a sensitive label or site, followed inside the same correlation window by an outbound action from the same user — an Outlook Send, a Teams external chat message, an OAuth-granted third-party app reading the user’s mail, a paste event captured by your EDR’s browser sensor. The window is shorter than you think. Five minutes is too long. Ninety seconds is closer.
What the rule actually looks like
In Splunk-shaped pseudocode, with field names from the M365 connector that ships with the Microsoft 365 TA (assuming you’re on TA 4.5 or later; the older one flattens AccessedResources into a comma-joined string and you’ll spend a week regexing it back out):
index=o365 sourcetype=o365:management:activity Operation=CopilotInteraction
| eval sensitive=if(match(AccessedResources, "sites/(legal|finance|hr-confidential|m-a)"), 1, 0)
| where sensitive=1
| eval window_start=_time, window_end=_time+90
| join UserId type=inner [
search index=o365 (Operation=Send OR Operation=MessageSent)
| eval send_time=_time
| fields UserId send_time Recipients Subject
]
| where send_time>=window_start AND send_time<=window_end
| where match(Recipients, "@(?!yourdomain\.com|yoursubsidiary\.com)")
That is the skeleton. It is not the rule you deploy. It is the rule you deploy on day one to find out what your false-positive surface looks like, which is the only honest way to size the real one.
Expected volume on first run, in a 5,000-seat tenant with Copilot broadly licensed: hundreds of hits a day. Maybe a thousand. The first week is mostly the executive assistant pattern — EA reads a board-deck draft in Copilot, summarizes it, emails the summary to the exec’s external personal address because that’s how the exec reads documents on the train. Legitimate. Repeats daily. Carve it out by user and recipient pair, not globally.
The second-largest bucket, usually, is sales engineers using Copilot to pull from internal product documentation and then emailing the cleaned-up version to a customer contact. Also legitimate, also high-volume, also the kind of thing where if you suppress it without a second signal you have blinded yourself to the actual attack. The second signal is the part most teams skip.
The second signal
What separates the EA pattern from real exfil is one of three things, and you need at least one of them in the rule before it’s worth paging on:
The recipient domain is novel for that user. Not novel for the tenant — novel for the user, looking back 30 days. The EA always emails the same exec personal address. Real exfil tends to involve a recipient nobody in the user’s history has talked to.
The document was opened by the agent without a corresponding human-driven FileAccessed event in the preceding hour. This is the prompt-injection tell. If the agent decided to open a document and the human didn’t navigate there first, something instructed the agent to do that, and it wasn’t the user’s keyboard. The UserAgent field will say Microsoft Office/... for human opens and something with Copilot or the BizChat string for agent opens; you can separate them.
The AppHost is Edge sidebar or BizChat and the originating page (you’ll need browser telemetry from your EDR for this — Defender for Endpoint exposes it as BrowserNavigationEvents in the advanced hunting schema, CrowdStrike’s equivalent is the BrowserHistory event but it’s not on by default) is not a Microsoft domain. The agent reading and summarizing an attacker-controlled page that contains injected instructions is the canonical 2025-era attack and it’s still the canonical 2026 one because nobody’s solved it at the model layer.
With the second-signal requirement, the daily volume on a 5K-seat tenant drops from the high hundreds to roughly 10–30. That’s tunable from the SOC side. Without it, you’ve built an alarm that the team will mute by end of week two, and the rule will sit in the detection repo with a # disabled — too noisy, revisit Q3 comment above it that nobody ever revisits.
What breaks this in practice
A few things, in rough order of how often they bite.
The AccessedResources field is not always populated. When Copilot uses a Graph connector to a third-party data source (Jira, ServiceNow, a custom connector your platform team built last spring), the resource URI lands in a different sub-field under AISystemPlugin and your regex misses it entirely. You will think those interactions are clean. They aren’t; you just can’t see them. There’s no clean fix — you have to enumerate your connectors and write a per-connector extractor, which is exactly the kind of work that doesn’t get done until after the incident.
Time skew between the M365 audit ingestion (which lags 15–60 minutes from event time, sometimes more, the docs claim near-real-time and the docs are wrong) and your EDR browser telemetry (near-real-time) means the join window has to be on _time from the event payload, not ingest time. If you join on ingest time you will silently miss correlations because the Copilot event hasn’t landed yet when the send event arrives. Splunk’s _indextime is not what you want here.
DLP labels are inconsistently applied. The rule above keys on site path, not on sensitivity label, for exactly this reason. If you key on SensitivityLabel, you’re trusting that someone labeled the file, and in most tenants outside of FedRAMP-bound workloads, label coverage on legacy SharePoint sites is somewhere between 40% and 70%. The path-based heuristic is uglier but it catches the unlabeled board deck that someone uploaded in 2019 and nobody ever touched again.
External recipients via Teams federation don’t show up in the same Operation as Outlook sends. You need Operation=MessageSent with a non-null CommunicationType of ExternalFederated, and the field name there has changed twice since 2023. Check the schema your tenant is actually emitting before you write the join; the published schema reference and what lands in the index are not always the same document.
Where this lands in 800-53
The detection itself is SI-4 (system monitoring) and AU-6 (audit review and analysis), with the agent-action attribution piece leaning on AU-2 event selection — you have to have explicitly added Copilot interaction events to your audit configuration, they are not in the default ingest for a lot of connectors. The control gap that makes the detection necessary is mostly AC-3 and AC-6: the agent inherits the user’s full access and there is no meaningful least-privilege boundary between “the user can read this file” and “the agent acting on the user’s behalf can read this file and paraphrase it into an outbound channel.” SC-7 boundary protection is effectively bypassed because the egress is policy-allowed Microsoft traffic. PT-2 and PT-3 come into scope if any of the documents the agent touched contained PII and the outbound recipient is outside the authorized processing boundary; that’s the conversation you do not want to have with your privacy office for the first time at 2am.
SR-3 is the one people forget. The agent is third-party software with privileged access to every document the user can see, and the supply-chain assurance you have on it is whatever the vendor’s SOC 2 says. That’s a risk register entry, not a detection, but the ISSO should be writing it down.
The honest version
This detection will not catch a careful attacker who paces their exfil and uses a recipient that already exists in the user’s mail history. Nothing built on access-shape correlation will. What it will catch is opportunistic abuse, prompt-injection-driven agent misuse from attacker-controlled web pages, and the insider who hasn’t thought hard about what telemetry exists. That’s most of what actually fires in this category, but it isn’t all of it, and any detection writeup that doesn’t say so out loud is selling something.
Build the rule. Expect it to be loud for two weeks. Tune on the second signal, not on the first. Don’t trust the schema docs — read your own index.