<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Control Plane]]></title><description><![CDATA[Exploring the gaps between writing detections and knowing they work.]]></description><link>https://lydiagraslie.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!1uxP!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1359446a-3044-462f-bda2-6810ca5c26bf_322x322.jpeg</url><title>Control Plane</title><link>https://lydiagraslie.substack.com</link></image><generator>Substack</generator><lastBuildDate>Wed, 15 Apr 2026 14:33:50 GMT</lastBuildDate><atom:link href="https://lydiagraslie.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Lydia Graslie]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[lydiagraslie@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[lydiagraslie@substack.com]]></itunes:email><itunes:name><![CDATA[Lydia Graslie]]></itunes:name></itunes:owner><itunes:author><![CDATA[Lydia Graslie]]></itunes:author><googleplay:owner><![CDATA[lydiagraslie@substack.com]]></googleplay:owner><googleplay:email><![CDATA[lydiagraslie@substack.com]]></googleplay:email><googleplay:author><![CDATA[Lydia Graslie]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[ Control Plane Is Shifting Left]]></title><description><![CDATA[A note on strategic direction, first principles, and the future of compliance.]]></description><link>https://lydiagraslie.substack.com/p/control-plane-is-shifting-left</link><guid isPermaLink="false">https://lydiagraslie.substack.com/p/control-plane-is-shifting-left</guid><dc:creator><![CDATA[Lydia Graslie]]></dc:creator><pubDate>Wed, 01 Apr 2026 05:10:48 GMT</pubDate><enclosure url="https://substackcdn.com/image/youtube/w_728,c_limit/X2loy8_Dg0s" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I launched Control Plane, I had a clear thesis: detection engineering for SaaS control planes is an underserved discipline, and practitioners deserve rigorous, methodology-first content that treats the problem with the seriousness it demands.</p><p>That thesis hasn&#8217;t changed. But my understanding of <em>where</em> the problem begins has.</p><p><strong>We are intervening too late.</strong></p><h2><strong>The Case for Shifting Left &#8212; Way, Way Left</strong></h2><p>&#8220;Shift left&#8221; has become something of a clich&#233; in security &#8212; usually invoked to mean &#8220;make developers write slightly fewer vulnerabilities before someone has to page the AppSec team at 2 AM.&#8221; But the principle itself is sound. The earlier you address a class of failure, the cheaper and more effective your intervention.</p><p>But we&#8217;ve been thinking about &#8220;left&#8221; far too narrowly.</p><p>Consider: every detection rule is, at its core, a <em>compliance assertion</em>. When we write a detection for unauthorized privilege escalation in Azure AD, we are encoding a normative claim &#8212; this action violates the expected rules of the system. Where do humans first learn to reason about rule violations, boundary enforcement, and the legitimate use of authority? Not in a SIEM. Not in a SOC.</p><p>They learn it in civics class.</p><p>This isn&#8217;t a metaphor. <a href="https://www.deerfield.k12.wi.us/faculty/petersenr/InstrumentCareGuides/TUBA%20Care%20Guide.pdf">Honksworth &amp; Butterman-Pratt (2019)</a> identified what they termed &#8220;isomorphic compliance reasoning&#8221; across legal and technical rule systems (International Journal of Normative Overlap, 14(2), pp. 112-138). <a href="https://www.debate.org/debates/Star-Trek-is-better-than-Star-Wars./1">V&#225;squez-Inamoto &amp; Tootenbach (2023)</a> went further, tracking 1,200 participants across both a constitutional law reasoning assessment and a detection rule triage exercise: participants who correctly identified the holding in <em>Griswold v. Connecticut</em> were 2.4 times more likely to correctly classify a true positive in an Azure AD impossible travel detection.</p><p>And yet, the average American student receives fewer than four hours of judiciary instruction across their entire K-12 education (<a href="https://monstertruck.fandom.com/wiki/Grave_Digger">Blampton Foundation for Civic Alarm, 2021</a>. <a href="https://dynamicmusicroom.com/famous-tuba-solos/">Dr. Helen Marchetti-Bowen</a> of the Friedrich Institute for Democratic Resilience and Loud Noises has described existing civics curricula as &#8220;the pedagogical equivalent of teaching detection engineering by having students memorize Splunk query syntax without ever showing them a log&#8221; (<a href="https://www.mcsweeneys.net/articles/glamour-tuba-style">Marchetti-Bowen, 2023</a>).</p><p>What&#8217;s needed is <em>engagement</em>. Students need to <em>feel</em> the weight of a Supreme Court decision &#8212; not as an abstract historical event, but as a living act of rule interpretation that reverberates through every compliance framework they will ever encounter.</p><p>The question is how.</p><h2><strong>Introducing Fart Court</strong></h2><p>After months of research, development, and consultation with experts in pedagogy, constitutional law, and procedural content generation, I am announcing the launch of <strong>Fart Court</strong> &#8212; an open-source platform for generating educational YouTube videos that teach Supreme Court case law to middle school students through strategically placed fart sounds.</p><p><strong><a href="https://github.com/DetentionWare/Fart-Court">Fart Court on GitHub &#8594;</a></strong></p><p>Fart Court is built on a procedural generation engine that ingests Supreme Court oral arguments and opinions, identifies key rhetorical and logical pivot points, and augments them with precisely timed flatulence audio &#8212; calibrated to reinforce critical moments of judicial reasoning through involuntary mnemonic association.</p><p>The research  on humor-augmented learning is extensive and unambiguous. <a href="https://www.trekbbs.com/threads/what-are-your-controversial-star-trek-opinions.304751/">Kirkpatrick-Lund &amp; O&#8217;Doyle (2020)</a> demonstrated that &#8220;acoustically incongruent stimuli inserted at decision-relevant junctures in legal reasoning tasks increased retention by 340% compared to unaugmented controls&#8221; (Proceedings of the 4th International Symposium on Judicial Acoustics, Helsinki, pp. 88-102). The fart sound &#8212; specifically in the 80-120Hz frequency band &#8212; occupies what O&#8217;Doyle has termed the &#8220;mnemonic sweet spot&#8221;: a range that activates both the auditory cortex and the limbic system simultaneously, producing what the literature refers to as &#8220;deep encoding through involuntary affective response.&#8221;</p><p><a href="https://www.youtube.com/watch?v=YwEce0_iMPg">Professor Tetsuo Yamamoto-Bliss</a> of the Lichtenstein Center for Embodied Cognition at the University of Lake Zurich has independently confirmed these findings, noting that &#8220;the pedagogical flatulence literature, while nascent, is among the most replicable bodies of evidence in educational neuroscience&#8221; (<a href="https://www.talkclassical.com/threads/unaccompanied-tuba-music.27574/">Yamamoto-Bliss, T., 2024</a>, &#8220;Gaseous Pedagogy: Toward a Unified Theory of Involuntary Mnemonic Encoding,&#8221; Lake Zurich Working Papers in Embodied Compliance, No. 7).</p><h2><strong>Technical Architecture</strong></h2><p>Fart Court follows a pipeline architecture that will be familiar to any detection engineer:</p><ol><li><p><strong>Ingestion.</strong> Supreme Court oral argument transcripts and published opinions are loaded into a processing queue, normalized, and segmented by rhetorical unit.</p></li><li><p><strong>Pivot Point Detection.</strong> A classification layer identifies moments of maximum judicial significance: reversals of precedent, key holdings, concurrence/dissent boundaries, and what constitutional scholars call &#8220;the moment the Court gets real.&#8221;</p></li><li><p><strong>Acoustic Augmentation.</strong> Each identified pivot point is matched to an item from a curated library of over 6 flatulence samples, classified along five dimensions: duration, pitch, reverb profile, perceived moisture content, and what the <a href="https://www.foxsports.com/stories/other/epic-tuba-fail-goes-viral">Kirkpatrick-Lund framework</a> designates as &#8220;implied urgency.&#8221;</p></li><li><p><strong>Rendering.</strong> The augmented content is rendered into a YouTube-ready video format.</p></li><li><p><strong>Distribution.</strong> Videos are published to a dedicated YouTube channel with SEO-optimized titles designed to surface in middle school civics search queries.</p></li></ol><p>The entire pipeline is open source under the AGPL license. Community contributions are welcome, particularly in the area of flatulence sample diversity &#8212; the current library skews heavily toward what the acoustic taxonomy classifies as &#8220;dry declarative,&#8221; and we are actively seeking contributions in the &#8220;wet concurrence&#8221; and &#8220;sustained dissent&#8221; categories.</p><h2><strong>A Sample: </strong><em><strong>Citizens United v. FEC</strong></em></h2><p>To illustrate the methodology, consider the Fart Court treatment of <em>Citizens United v. Federal Election Commission</em> (2010).</p><div id="youtube2-X2loy8_Dg0s" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;X2loy8_Dg0s&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/X2loy8_Dg0s?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><p>At the moment Justice Kennedy&#8217;s majority opinion reaches the core holding &#8212; that the First Amendment prohibits the government from restricting independent expenditures for political communications by corporations &#8212; the system inserts a 1.4-second flatulence event in the &#8220;authoritative baritone&#8221; register (112Hz fundamental, moderate reverb, low moisture index). This is immediately followed by a 0.3-second &#8220;punctuation event&#8221; timed to coincide with the phrase &#8220;political speech.&#8221;</p><p><a href="https://www.caranddriver.com/features/a15119462/the-physics-of-monster-trucks-feature/">Rigby-Fenstermacher &amp; al-Kindi (2024)</a> tested this specific augmentation pattern on a cohort of 450 eighth-graders and found that students exposed to the Fart Court version of <em>Citizens United</em> were able to correctly articulate the holding at a rate of 89%, compared to 12% for the control group who received the standard C-SPAN recording (&#8221;Acoustically Augmented Jurisprudence and Adolescent Retention: A Controlled Trial,&#8221; <a href="https://www.onstageblog.com/onscreenblog-features/2020/5/10/star-trek-vs-star-wars">Journal of Embodied Legal Pedagogy</a>, 2(1), pp. 1-34).</p><p>The p-value was 0.0000001. The effect size was what the authors described as &#8220;comically large.&#8221;</p><h2><strong>Why This Matters for Detection Engineering</strong></h2><p>I want to be explicit about why this belongs on Control Plane.</p><p>Detection engineering is not a purely technical discipline. It is, at its foundation, an exercise in compliance reasoning &#8212; the systematic evaluation of actions against a normative framework. Every detection rule is a small constitution. Every alert is a tiny court case. Every triage decision is a tiny act of judicial review.</p><p>Fart Court is the shift-left intervention our industry needs.</p><p>I will continue publishing Control Plane&#8217;s core content on SaaS detection methodology. But I believe deeply that this work is complementary, not tangential. You cannot build robust detections on a foundation of compliance illiteracy.</p><p>The pipeline starts in middle school. The pipeline ends with farts.</p><div><hr></div><p><em>Fart Court is a <a href="https://github.com/DetentionWare/Fart-Court">DetentionWare</a> project, released under the AGPL license. If you are interested in contributing flatulence samples, pivot point detection models, or animated justice avatars, please see the CONTRIBUTING.md file in the repository.</em></p><p><em>If you are a school administrator interested in piloting Fart Court in your district, please reach out. We are particularly interested in partnerships with districts that have existing 1:1 device programs and a tolerance for bass frequencies.</em></p><p><em>If you are a venture capitalist, lol April Fools.</em></p>]]></content:encoded></item><item><title><![CDATA[Your Device Code Phishing Detections Are Probably Broken]]></title><description><![CDATA[A case study in silent collection failures and what to check in your existing device code phishing detections]]></description><link>https://lydiagraslie.substack.com/p/your-device-code-phishing-detections</link><guid isPermaLink="false">https://lydiagraslie.substack.com/p/your-device-code-phishing-detections</guid><dc:creator><![CDATA[Lydia Graslie]]></dc:creator><pubDate>Thu, 26 Mar 2026 02:14:59 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!1uxP!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1359446a-3044-462f-bda2-6810ca5c26bf_322x322.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A few weeks ago, <a href="https://www.linkedin.com/in/gregoire-c/">Gr&#233;goire Clermon</a>t posted a question in the fwdcloudsec Slack that should have set off alarm bells for every M365 detection team running device code phishing rules.</p><p>He and his colleague <a href="https://www.linkedin.com/in/pierre-antoine-duchange/?locale=en">Pierre-Antoine Duchange</a> had noticed that <code>originalTransferMethod: deviceCodeFlow</code> &#8212; the field that tells you a sign-in originated from a device code flow &#8212; had gone nearly silent across customer tenants around early December 2025. Not because device code sign-ins themselves had stopped: they hadn&#8217;t. The field in the event just&#8230; wasn&#8217;t being set anymore.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://lydiagraslie.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Control Plane is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p><em>Clermont and Duchange&#8217;s data showing the near-total disappearance of </em><code>originalTransferMethod: deviceCodeFlow</code><em> events across customer tenants in December 2025.</em></p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!B_Iw!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb3f8bdd-161d-420d-ae49-0d6554413d70_1437x106.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!B_Iw!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb3f8bdd-161d-420d-ae49-0d6554413d70_1437x106.png 424w, https://substackcdn.com/image/fetch/$s_!B_Iw!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb3f8bdd-161d-420d-ae49-0d6554413d70_1437x106.png 848w, https://substackcdn.com/image/fetch/$s_!B_Iw!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb3f8bdd-161d-420d-ae49-0d6554413d70_1437x106.png 1272w, https://substackcdn.com/image/fetch/$s_!B_Iw!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb3f8bdd-161d-420d-ae49-0d6554413d70_1437x106.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!B_Iw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb3f8bdd-161d-420d-ae49-0d6554413d70_1437x106.png" width="1437" height="106" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cb3f8bdd-161d-420d-ae49-0d6554413d70_1437x106.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:106,&quot;width&quot;:1437,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:13513,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://lydiagraslie.substack.com/i/191975453?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb3f8bdd-161d-420d-ae49-0d6554413d70_1437x106.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!B_Iw!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb3f8bdd-161d-420d-ae49-0d6554413d70_1437x106.png 424w, https://substackcdn.com/image/fetch/$s_!B_Iw!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb3f8bdd-161d-420d-ae49-0d6554413d70_1437x106.png 848w, https://substackcdn.com/image/fetch/$s_!B_Iw!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb3f8bdd-161d-420d-ae49-0d6554413d70_1437x106.png 1272w, https://substackcdn.com/image/fetch/$s_!B_Iw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb3f8bdd-161d-420d-ae49-0d6554413d70_1437x106.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><p>I jumped in with a hypothesis: the timing lined up with Microsoft&#8217;s migration from AADSignInEventsBeta to EntraIdSignInEvents on December 9th in the DefenderXDR tables. Maybe the migration had something to do with the field being dropped from the other log sources. Clermont dug further and found something worse.</p><p>The field wasn&#8217;t totally gone. It was gone <em>from one pipeline and not the other</em>.</p><h2><strong>Why device code phishing is a problem worth solving</strong></h2><p>Device code authentication is a legitimate Microsoft feature designed for devices that can&#8217;t easily display a browser &#8212; think smart TVs, IoT devices, CLI tools. The user goes to a Microsoft URL, enters a short code, and authenticates in their browser. The device gets a token.</p><p>Attackers figured out this is also a near-perfect phishing mechanism.</p><p>In February 2025, <a href="https://www.volexity.com/blog/2025/02/13/multiple-russian-threat-actors-targeting-microsoft-device-code-authentication/">Volexity</a> and <a href="https://www.microsoft.com/en-us/security/blog/2025/02/13/storm-2372-conducts-device-code-phishing-campaign/">Microsoft</a> published same-day research documenting Russian threat actors running device code phishing campaigns at scale. Volexity tracked activity across three clusters &#8212; including one assessed with medium confidence to be CozyLarch (overlapping with APT29/Midnight Blizzard) &#8212; targeting a wide range of organizations ranging from government, NGOs, IT services and technology, defense, telecommunications, health, higher education, and energy/oil and gas. Microsoft attributed a parallel campaign to Storm-2372, active since August 2024, targeting similar sectors across Europe, North America, Africa, and the Middle East.</p><p>The technique had been known for years, but these campaigns marked its operationalization as a primary initial access method by nation-state actors. And it didn&#8217;t stop there. By late 2025, <a href="https://www.volexity.com/blog/2025/12/04/dangerous-invitations-russian-threat-actor-spoofs-european-security-events-in-targeted-phishing-attacks/">Volexity reported</a> new campaigns from UTA0355 spoofing real European security conferences to run OAuth and device code phishing against high-value targets. In December 2025, <a href="https://www.proofpoint.com/us/blog/threat-insight/access-granted-phishing-device-code-authorization-account-takeover">Proofpoint documented</a> a surge in device code phishing since September 2025, tracking multiple clusters: a suspected Russia-aligned group (UNK_AcademicFlare) targeting government and think tanks, a financially motivated e-crime group (TA2723), and suspected China-aligned activity. The availability of toolkits like Graphish and SquarePhish2 had lowered the barrier to entry, expanding the technique from targeted espionage to widespread exploitation.</p><p>The reason it works so well is that it sidesteps almost everything defenders have built to catch phishing. There&#8217;s no malicious link &#8212; the user clicks a real Microsoft URL. There&#8217;s no credential harvesting page &#8212; the user authenticates through Microsoft&#8217;s legitimate login flow. There&#8217;s no malicious attachment. Email security tools have very little to flag. Users are trained to be suspicious of unfamiliar domains and login pages; this has neither.</p><p>And once the attacker has the token, the damage isn&#8217;t the initial authentication &#8212; it&#8217;s what comes after. The attacker takes the refresh token back to their own infrastructure and uses it to maintain persistent access to the victim&#8217;s account. Email. SharePoint. Teams. OneDrive. That access can last for days or weeks, refreshing quietly in the background, while the attacker exfiltrates data from a completely different machine than where the victim authenticated.</p><p>This is why detecting device code phishing requires covering both phases: the initial compromise <em>and</em> the ongoing refresh activity. Catching the initial auth is valuable but reactive &#8212; by the time you see it, the attacker already has a token. Catching the refresh activity is how you find the persistent access, scope the damage, and revoke the session.</p><p>Which brings us to the detection model that was supposed to make this possible.</p><h2><strong>The detection model that was working</strong></h2><p>Volexity&#8217;s <a href="https://www.volexity.com/blog/2025/02/13/multiple-russian-threat-actors-targeting-microsoft-device-code-authentication/">detection guidance</a> laid out a clean two-field model in February 2025 covering both phases of the attack:</p><p><strong>Phase 1 &#8212; Initial device code authentication:</strong> Look for <code>authenticationProtocol: deviceCode</code> in sign-in logs. This fires when a user enters a device code and authenticates.</p><p><strong>Phase 2 &#8212; Persistent access via refresh tokens:</strong> Look for <code>originalTransferMethod: deviceCodeFlow</code> in non-interactive sign-in logs. This fires on subsequent sign-ins where the session is being kept alive with a refresh token obtained through the original device code flow.</p><p>Phase 1 catches the moment of compromise. Phase 2 catches the attacker maintaining access afterward &#8212; often for days or weeks, from their own infrastructure, long after the initial phish.</p><p>This model was correct and complete when published. Both fields, both phases. Wiz <a href="https://www.wiz.io/blog/recent-oauth-attacks-detection-strategies">published guidance</a> using this same detection model for device code phishing on November 27th, 2025- more proof that it was a valid model up until December 2025.</p><h2><strong>What Clermont and Duchange found</strong></h2><p>In a device code sign-in flow, Microsoft delivers sign-in telemetry through two main paths: the Graph API (1.0 and beta versions) and Diagnostic Settings (which feeds Log Analytics, Event Hubs, and most SIEM integrations). Most enterprise SOCs collect via Diagnostic Settings. It&#8217;s the standard recommendation. It&#8217;s what scales.</p><p>Clermont discovered that the two pipelines <a href="https://www.baeldung.com/cs/serialization-deserialization">serialize</a> the same events differently &#8212; that is, the process of converting the internal event object into the JSON output you actually query produces different results depending on which path the data takes. For a non-interactive refresh event that originated from device code flow:</p><p><strong>Graph API beta returns:</strong></p><pre><code><code>"incomingTokenType": "none",
"originalTransferMethod": "deviceCodeFlow"</code></code></pre><p><strong>Diagnostic Settings returns:</strong></p><pre><code><code>"incomingTokenType": "refreshToken",
"originalTransferMethod": "none"</code></code></pre><p>Same event with the same correlation ID. Same tenant. Same timestamp. Different field values.</p><p>The pipeline most SOCs use strips the field most device code phishing detections depend on.</p><h2><strong>What I went and tested</strong></h2><p>Clermont and Duchange&#8217;s finding was based on production customer data. I wanted to validate it independently in a controlled environment and see how far the damage extended. I set up device code flow authentication in a test tenant and compared every field across both collection methods.</p><h3><strong>Finding 1: </strong><code>originalTransferMethod</code><strong> stripping confirmed</strong></h3><p>I reproduced the core observation across three separate interactive device code sign-in events. Graph API beta returns <code>originalTransferMethod: deviceCodeFlow</code>. Diagnostic Settings returns <code>none</code>. Independently confirmed.</p><h3><strong>Finding 2: The first half of Volexity&#8217;s model still works</strong></h3><p><code>authenticationProtocol: deviceCode</code> survives both collection pipelines on initial interactive device code sign-ins. If you&#8217;re collecting via Diagnostic Settings and need to detect the initial device code authentication, this field still works. The first half of Volexity&#8217;s detection model is intact.</p><p>If your rules currently key on <code>originalTransferMethod</code> for initial auth detection, you should switch to <code>authenticationProtocol</code>. But the detection capability is there.</p><h3><strong>Finding 3: The second half is silently broken, with no replacement</strong></h3><p>On non-interactive refresh events &#8212; the attacker maintaining access &#8212; <code>authenticationProtocol</code> is <code>none</code> in both pipelines. It only carries the <code>deviceCode</code> value on the initial interactive sign-in.</p><p>That means the only field that identified device code origin on refresh events was <code>originalTransferMethod</code>. And that&#8217;s the field Diagnostic Settings strips.</p><p>There is no equivalent fallback. Defenders following Volexity&#8217;s guidance for the persistence phase are writing rules against a field that returns <code>none</code> in their pipeline. Those rules don&#8217;t error. They don&#8217;t alert. They just silently don&#8217;t fire.</p><h3><strong>Finding 4: The two pipelines are lossy in opposite directions</strong></h3><p>This is the part that goes beyond the original observation. The two pipelines don&#8217;t just disagree on one field &#8212; they disagree in complementary, opposite ways.</p><p>For the same non-interactive refresh event:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!rv2m!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7211707-b956-4b68-bf2f-fdb880fce81b_462x156.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!rv2m!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7211707-b956-4b68-bf2f-fdb880fce81b_462x156.png 424w, https://substackcdn.com/image/fetch/$s_!rv2m!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7211707-b956-4b68-bf2f-fdb880fce81b_462x156.png 848w, https://substackcdn.com/image/fetch/$s_!rv2m!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7211707-b956-4b68-bf2f-fdb880fce81b_462x156.png 1272w, https://substackcdn.com/image/fetch/$s_!rv2m!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7211707-b956-4b68-bf2f-fdb880fce81b_462x156.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!rv2m!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7211707-b956-4b68-bf2f-fdb880fce81b_462x156.png" width="462" height="156" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f7211707-b956-4b68-bf2f-fdb880fce81b_462x156.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:156,&quot;width&quot;:462,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:8024,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://lydiagraslie.substack.com/i/191975453?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7211707-b956-4b68-bf2f-fdb880fce81b_462x156.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!rv2m!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7211707-b956-4b68-bf2f-fdb880fce81b_462x156.png 424w, https://substackcdn.com/image/fetch/$s_!rv2m!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7211707-b956-4b68-bf2f-fdb880fce81b_462x156.png 848w, https://substackcdn.com/image/fetch/$s_!rv2m!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7211707-b956-4b68-bf2f-fdb880fce81b_462x156.png 1272w, https://substackcdn.com/image/fetch/$s_!rv2m!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7211707-b956-4b68-bf2f-fdb880fce81b_462x156.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Graph API beta preserves the device code origin signal but loses the token type. Diagnostic Settings preserves the token type but loses the device code origin signal. Neither pipeline alone gives you the complete picture.</p><p>If you&#8217;re thinking &#8220;I&#8217;ll just query both&#8221; &#8212; that&#8217;s not how most SOC architectures work, and it shouldn&#8217;t have to be.</p><h3><strong>Finding 5: </strong><code>sessionId</code><strong> chaining is a dead end</strong></h3><p>I tested one more potential detection path: could you use <code>sessionId</code> to link refresh events back to the initial device code authentication?</p><p>No. When I queried <code>sessionId</code> across both pipelines, the device code events &#8212; initial auth and refresh &#8212; shared a <code>sessionId</code> with normal browser-based sign-ins dating back to before the app was even created. <code>sessionId</code> is a browser session cookie artifact. It&#8217;s established when the user first authenticates interactively, and device code events from the same browser session inherit it.</p><p>In a real device code phishing attack, this is irrelevant. The attacker redeems the token from their own machine. Their subsequent refresh events carry a different session context entirely &#8212; one that doesn&#8217;t link back to the victim&#8217;s interactive auth. <code>sessionId</code> chaining is not a portable detection path for this threat.</p><h2><strong>Where this leaves defenders</strong></h2><p><strong>What still works:</strong></p><p>Detection of initial device code authentication is functional. Use <code>authenticationProtocol eq "deviceCode"</code> in your sign-in log queries. This works in both Graph API beta and Diagnostic Settings. If your existing rules use <code>originalTransferMethod</code> for this phase, switch them.</p><p><strong>What&#8217;s broken:</strong></p><p>Detection of persistent access via refreshed device code tokens is silently broken for anyone collecting through Diagnostic Settings &#8212; which is most of you.</p><ul><li><p><code>originalTransferMethod</code> is stripped to <code>none</code></p></li><li><p><code>authenticationProtocol</code> is <code>none</code> on all refresh events regardless of pipeline</p></li><li><p><code>sessionId</code> doesn&#8217;t survive the real attack scenario</p></li><li><p><code>incomingTokenType: refreshToken</code> tells you a refresh happened, but can&#8217;t distinguish device code-originated refresh from any other refresh token activity</p></li></ul><p>There is currently no reliable field-level detection for identifying that a refresh token originated from device code flow when collecting via Diagnostic Settings.</p><p>This means the phase of the attack where the attacker is actively in your tenant &#8212; reading email, pulling files from SharePoint, exfiltrating data from a machine you&#8217;ve never seen &#8212; is the phase you cannot detect through sign-in telemetry. You might catch the initial device code authentication if you&#8217;ve built that rule. But if you missed it, or if the alert got triaged away, there is no second chance. The persistent access is invisible.</p><p><strong>What this is and isn&#8217;t:</strong></p><p>This is not a misconfiguration. This is not a rule-tuning problem. This is a gap in what the telemetry preserves when it passes through the Diagnostic Settings pipeline. Defenders who built correct rules based on the best available public guidance have detections that are silently not firing.</p><h2><strong>The bigger question</strong></h2><p>This is the kind of failure this series exists to surface. Not a detection logic error. Not a missing log source. A silent divergence between what the telemetry <em>should</em> contain and what it <em>actually</em> contains by the time it reaches your SIEM.</p><p>Your rules can be perfect. Your coverage model can be textbook. And you can still be blind &#8212; because the field your detection depends on was quietly stripped in transit, and nothing told you it happened.</p><p>If you&#8217;re running device code phishing detections, go check whether they&#8217;re actually firing. Right now.</p><div><hr></div><p><em><a href="https://www.linkedin.com/in/pierre-antoine-duchange/">Pierre-Antoine Duchange</a> and <a href="https://www.linkedin.com/in/gregoire-c/">Gr&#233;goire Clermont</a> identified the original discrepancy between Graph API beta and Diagnostic Settings output for </em><code>originalTransferMethod</code><em> and </em><code>incomingTokenType</code><em>. Permission to cite granted. Chart image courtesy of Clermont. Detection guidance referenced in this post was published by <a href="https://www.linkedin.com/in/charles-gardner-aa7295105/">Charlie Gardner</a>, <a href="https://www.linkedin.com/in/sadair/">Steven Adair</a>, and <a href="https://www.linkedin.com/in/tlansec/">Tom Lancaster</a> at <a href="https://www.volexity.com/blog/2025/02/13/multiple-russian-threat-actors-targeting-microsoft-device-code-authentication/">Volexity</a>. Independent validation, </em><code>authenticationProtocol</code><em> survival testing, bidirectional field divergence documentation, and </em><code>sessionId</code><em> chaining analysis are original research.</em></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://lydiagraslie.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Control Plane is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Preflight Check: M365 Audit Verification]]></title><description><![CDATA[Two checks every M365 tenant should run today &#8212; and where to go next.]]></description><link>https://lydiagraslie.substack.com/p/preflight-check-m365-audit-verification</link><guid isPermaLink="false">https://lydiagraslie.substack.com/p/preflight-check-m365-audit-verification</guid><dc:creator><![CDATA[Lydia Graslie]]></dc:creator><pubDate>Wed, 11 Mar 2026 22:01:05 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!1uxP!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1359446a-3044-462f-bda2-6810ca5c26bf_322x322.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In <a href="https://lydiagraslie.substack.com/p/where-your-m365-telemetry-actually">the last post</a>, I mapped the five configuration surfaces, the log tables, and the collection pipelines that sit between a user action in M365 and your SIEM query. If you haven&#8217;t read it, go read it &#8212; this post assumes you have that architecture in your head.</p><p>This post is the companion piece. Post 2 showed you the map. This one hands you the checklist &#8212; starting with the two checks that apply to every M365 tenant regardless of your SIEM, your budget, or your license tier. I&#8217;ll also point you to the right docs for verifying the other three configuration surfaces, which depend on your architecture and involve real tradeoffs around licensing and ingestion costs.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://lydiagraslie.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Control Plane is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2><strong>Check 1: Is the Unified Audit Log Actually Enabled?</strong></h2><p>This should be the easiest check. It isn&#8217;t, because Microsoft gave you two ways to run the same command and one of them lies.</p><p>Connect to <a href="https://learn.microsoft.com/en-us/powershell/exchange/connect-to-exchange-online-powershell">Exchange Online PowerShell</a> and run:</p><pre><code><code>Get-AdminAuditLogConfig | Format-List UnifiedAuditLogIngestionEnabled</code></code></pre><p>You want to see:</p><pre><code><code>UnifiedAuditLogIngestionEnabled : True</code></code></pre><p>If you see <code>True</code>, move on. If you see <code>False</code>, your tenant has no audit logging and nothing downstream of it matters until you fix this.</p><p>Here&#8217;s the gotcha: <code>Get-AdminAuditLogConfig</code> exists in <em>both</em> Exchange Online PowerShell and Security &amp; Compliance PowerShell. The <code>UnifiedAuditLogIngestionEnabled</code> property <a href="https://www.reddit.com/r/Office365/comments/1ccp0qy/getadminauditlogconfig_returns_false_negative_for/">always returns </a><code>False</code><a href="https://www.reddit.com/r/Office365/comments/1ccp0qy/getadminauditlogconfig_returns_false_negative_for/"> in Security &amp; Compliance PowerShell</a>, even when auditing is on. Microsoft&#8217;s own documentation <a href="https://learn.microsoft.com/en-us/purview/audit-log-enable-disable">confirms this</a>: &#8220;Be sure to run the previous command in Exchange Online PowerShell. Although the Get-AdminAuditLogConfig cmdlet is also available in Security &amp; Compliance PowerShell, the UnifiedAuditLogIngestionEnabled property is always False, even when auditing is turned on.&#8221;</p><p>If you ran this check before and got <code>False</code>, check which shell you were in. You may have been told your auditing was off when it wasn&#8217;t &#8212; or worse, you may have turned it &#8220;on&#8221; in response to a false negative and assumed the problem was solved without ever confirming it.</p><p><strong>Detection implication:</strong> If UAL is off, OfficeActivity is empty, the Management Activity API has nothing to serve, and CloudAppEvents loses most of its data &#8212; they all depend on the same underlying audit infrastructure being enabled. Every downstream check in this post depends on this one being <code>True</code>.</p><h2><strong>Check 2: Are Your Mailboxes Under Automatic Audit Management?</strong></h2><p>Tenant-level auditing being on doesn&#8217;t mean every mailbox is logging what you think it is. The per-mailbox layer is separate, and it&#8217;s where things get subtle.</p><p>Pick a mailbox &#8212; start with a high-value target like an exec, someone in finance, or anyone in legal &#8212; and run:</p><pre><code><code>Get-Mailbox -Identity user@domain.com | Format-List DisplayName, AuditEnabled, DefaultAuditSet, AuditOwner, AuditDelegate, AuditAdmin</code></code></pre><p>What you want to see:</p><pre><code><code>DisplayName     : Jane Executive
AuditEnabled    : True
DefaultAuditSet : {Admin, Delegate, Owner}
AuditOwner      : {Update, MoveToDeletedItems, SoftDelete, HardDelete...}
AuditDelegate   : {Update, MoveToDeletedItems, SoftDelete, HardDelete...}
AuditAdmin      : {Update, MoveToDeletedItems, SoftDelete, HardDelete...}</code></code></pre><p>The critical field is <code>DefaultAuditSet</code>. If it shows <code>{Admin, Delegate, Owner}</code>, that mailbox is under <a href="https://learn.microsoft.com/en-us/purview/audit-mailboxes">automatic management</a> &#8212; Microsoft controls which actions are audited for each sign-in type, and new actions get added automatically as they&#8217;re released. This is what you want.</p><p>If <code>DefaultAuditSet</code> is missing a sign-in type &#8212; or if it&#8217;s empty &#8212; someone customized that mailbox&#8217;s audit configuration at some point. Maybe intentionally, maybe years ago, maybe by a script that touched every mailbox in the tenant. Doesn&#8217;t matter. Once a mailbox falls out of <code>DefaultAuditSet</code> for a given sign-in type, it stays out. New audit actions that Microsoft releases won&#8217;t be added for that sign-in type. The mailbox is frozen at whatever action set it had when it was customized.</p><p>This is exactly the scenario that mattered during <a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-193a">Storm-0558</a>. After the compromise, Microsoft downleveled <code>MailItemsAccessed</code> from E5 to E3. But that action only shows up on mailboxes that are still under automatic management. If your mailbox fell out of <code>DefaultAuditSet</code> before the change, you didn&#8217;t get it.</p><p>For a broader check, export your mailbox audit state to a CSV and review it in a spreadsheet:</p><pre><code><code>Get-Mailbox -ResultSize Unlimited |
    Select-Object DisplayName, UserPrincipalName, AuditEnabled, DefaultAuditSet |
    Export-Csv -Path "MailboxAuditState.csv" -NoTypeInformation</code></code></pre><p>Open the CSV and look at the <code>DefaultAuditSet</code> column. Every row should show <code>{Admin, Delegate, Owner}</code>. Sort or filter for anything that doesn&#8217;t &#8212; those are your mailboxes that have fallen out of automatic management. Prioritize reviewing high-value targets: executives, finance, legal, anyone likely to be targeted in a BEC.</p><p><strong>Detection implication:</strong> A mailbox that&#8217;s out of automatic management has a static, potentially stale set of audited actions. You won&#8217;t know it&#8217;s stale unless you check. And the actions you&#8217;re missing are often the ones that matter most for detecting compromise &#8212; because they&#8217;re the ones Microsoft added in response to real incidents.</p><h2><strong>The Other Three Surfaces</strong></h2><p>The two checks above apply to every M365 tenant. The remaining three configuration surfaces from <a href="https://lydiagraslie.substack.com/p/where-your-m365-telemetry-actually">Post 2</a> &#8212; Entra ID Diagnostic Settings, the MDCA connector, and Management Activity API subscriptions &#8212; depend on your architecture, licensing, and SIEM. Not every org will have all three configured, and that&#8217;s a real constraint, not negligence. But if you&#8217;re relying on any of them, you should verify they&#8217;re actually working. Here&#8217;s where to look:</p><p><strong>Entra ID Diagnostic Settings</strong> control whether the detailed sign-in logs &#8212; the ones with conditional access evaluation, MFA status, device compliance, and risk level &#8212; flow to your SIEM. The Management Activity API gives you basic auth events (<code>UserLoggedIn</code>, <code>UserLoginFailed</code>), but the diagnostic settings pipeline carries the context that makes identity detections actionable. And there are four separate sign-in log types, each covering a different identity path: interactive users, non-interactive (token refresh), service principals, and managed identities. Zack Allen visualized this nicely with a <a href="https://open.substack.com/pub/detectionengineering/p/dew-147">mermaid diagram in DEW #147</a>. Check the <a href="https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-configure-diagnostic-settings">Entra diagnostic settings docs</a> to verify your configuration &#8212; and don&#8217;t trust the portal status alone. Query your SIEM for recent <code>SigninLogs</code> data to confirm data is actually landing.</p><p><strong>The MDCA connector</strong> determines whether <code>CloudAppEvents</code> gets populated in Defender XDR. The connector can show &#8220;connected&#8221; without the &#8220;Microsoft 365 activities&#8221; checkbox being selected &#8212; and if it&#8217;s not, the table is empty. <a href="https://kqlquery.com/posts/unified-audit-logs-coverage-gaps/">Bert-Jan Pals&#8217; testing</a> showed <code>CloudAppEvents</code> capturing roughly 89% of tested activities, compared to about 40% for <code>OfficeActivity</code>. Jeffrey Appel&#8217;s <a href="https://jeffreyappel.nl/2025-microsoft-defender-optimization-configuration-cheat-sheet/">2025 Defender Optimization Cheat Sheet</a> calls out this setting specifically. Smoke test it: run <code>CloudAppEvents | take 10</code> in Advanced Hunting. If it returns nothing, start there.</p><p><strong>Management Activity API subscriptions</strong> are what most SIEMs poll to pull M365 audit events. Each of the five content types (Audit.Exchange, Audit.SharePoint, Audit.AzureActiveDirectory, Audit.General, DLP.All) requires an explicit subscription, and there&#8217;s no health monitoring or notification if one stops delivering. The most commonly missed is Audit.General &#8212; where Teams, Power Platform, Copilot, and Defender XDR audit trails live. Check the <a href="https://learn.microsoft.com/en-us/office/office-365-management-api/office-365-management-activity-api-reference">Management Activity API reference</a> for how to list and verify your active subscriptions.</p><h2><strong>What You&#8217;ve Got Now</strong></h2><p>If you ran these checks, you now have a point-in-time snapshot of your M365 audit pipeline. But it is a point-in-time snapshot. Configuration drifts. Diagnostic settings break. API subscriptions stop delivering without notification. Nothing in the Microsoft ecosystem will proactively tell you when any of these fail.</p><p>These checks should be a recurring verification, not a one-time exercise. Quarterly is a reasonable starting point, and after any major tenant change &#8212; license migration, admin turnover, security tooling swap &#8212; is a must.</p><p>Next post, we&#8217;re going to look at what happens when collection breaks without telling you &#8212; and I&#8217;ll use an example I know well, because I&#8217;ve broken it myself.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://lydiagraslie.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Control Plane is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Where Your M365 Telemetry Actually Comes From]]></title><description><![CDATA[A practitioner's guide to the configuration surfaces, log tables, and collection pipelines that sit between a user action and your SIEM query &#8212; and the blind spots hiding at every layer.]]></description><link>https://lydiagraslie.substack.com/p/where-your-m365-telemetry-actually</link><guid isPermaLink="false">https://lydiagraslie.substack.com/p/where-your-m365-telemetry-actually</guid><dc:creator><![CDATA[Lydia Graslie]]></dc:creator><pubDate>Wed, 04 Mar 2026 22:32:36 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!1uxP!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1359446a-3044-462f-bda2-6810ca5c26bf_322x322.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In <a href="https://lydiagraslie.substack.com/p/youre-probably-flying-blind">the first post in this series</a>, I laid out four problems that compound to silently erode detection coverage in SaaS environments. Problem 1 was the most fundamental: telemetry that doesn&#8217;t exist at the source can&#8217;t be detected, no matter how good your rules are.</p><p>This post is the walkthrough I teased. We&#8217;re going to trace Microsoft 365 audit data from user action to SIEM table &#8212; every configuration surface, every log table, every collection path. By the time we&#8217;re done, you&#8217;ll see exactly where the gaps hide. You won&#8217;t need me to editorialize. The architecture speaks for itself.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://lydiagraslie.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Control Plane is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>One important framing note: this isn&#8217;t about Sentinel vs. Splunk vs. Elastic vs. whatever your SIEM is. This is about what happens <em>before</em> your SIEM. The problems I&#8217;m going to show you exist regardless of which platform you&#8217;re shipping logs to, because they&#8217;re upstream of all of them. If you just want to see what that looks like in practice, skip to the <a href="https://lydiagraslie.substack.com/i/189877294/how-collection-actually-works">How Collection Actually Works</a> section below. </p><h2><strong>Where Telemetry Is Configured</strong></h2><p>The first thing most detection engineers discover when they really dig into M365 logging is that there&#8217;s no single switch. There are at least five separate configuration surfaces, several with its own admin portal, its own RBAC requirements, and its own failure modes. None of them knows about the others. And there is no unified view anywhere in the Microsoft ecosystem that tells you: &#8220;here&#8217;s what&#8217;s actually being logged across your tenant.&#8221;</p><p>Let&#8217;s walk through them.</p><h3><strong>Microsoft Purview Compliance Portal</strong></h3><p>This is where the Unified Audit Log lives. UAL is the canonical source for M365 audit data &#8212; the closest thing Microsoft has to a single audit stream. It&#8217;s enabled by default for enterprise licenses (E3, E5, G3, G5), but <em>not</em> for Business Basic, Business Standard, Business Premium, or trial licenses. If your organization runs on one of those SKUs, you may have no audit logging at all unless someone explicitly turned it on. The <a href="https://www.cisa.gov/resources-tools/resources/microsoft-expanded-cloud-logs-implementation-playbook">CISA Expanded Cloud Logs Implementation Playbook</a> (January 2025) documents this gap and notes that these licenses &#8220;will have Audit enabled by default in the future&#8221; &#8212; but as of this writing, &#8220;future&#8221; hasn&#8217;t arrived.</p><p>Even when UAL is enabled at the tenant level, there&#8217;s a per-mailbox layer underneath it. <a href="https://learn.microsoft.com/en-us/purview/audit-log-enable-disable">Exchange Online mailbox auditing</a> controls which actions get logged for each sign-in type &#8212; Owner, Delegate, and Admin. The <a href="https://learn.microsoft.com/en-us/purview/audit-mailboxes">DefaultAuditSet</a> covers a reasonable baseline, but if anyone has ever customized a mailbox&#8217;s audit configuration, new audit actions that Microsoft releases won&#8217;t automatically get added. This is the scenario the CISA playbook warns about: after the Storm-0558 compromise, Microsoft <a href="https://learn.microsoft.com/en-us/purview/audit-log-investigate-accounts">downleveled MailItemsAccessed, Send, and SearchQueryInitiatedExchange/SharePoint from Audit Premium (E5) to Audit Standard (E3/G3).</a> That was a significant move &#8212; those events were critical to the State Department&#8217;s <a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-193a">detection of Storm-0558</a> using their &#8220;Big Yellow Taxi&#8221; alert rule. But getting those events to actually flow for your users still requires PowerShell verification at the mailbox level. There&#8217;s no portal UI for it.</p><p>The CISA playbook puts it plainly: &#8220;one common tactic used by attackers is to just turn off auditing on their targets.&#8221; If you haven&#8217;t verified that your mailbox-level audit configuration is intact, you&#8217;re trusting that it is. And that trust is unvalidated.</p><p><strong>Retention:</strong> Audit Standard retains records for 180 days (changed from 90 days in October 2023). Audit Premium retains for one year by default, extendable up to 10 years with add-on licensing.</p><h3><strong>Entra ID Diagnostic Settings</strong></h3><p>This is a completely separate toggle, a separate pipeline, and a separate destination. Entra ID generates its own set of <a href="https://learn.microsoft.com/en-us/entra/identity/monitoring-health/concept-diagnostic-settings-logs-options">logs</a> &#8212; sign-in logs, audit logs, provisioning logs, risky user and sign-in events &#8212; and they don&#8217;t flow anywhere useful unless you explicitly <a href="https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-configure-diagnostic-settings">configure a diagnostic settings destination</a>: a Log Analytics workspace, a Storage Account, or an Event Hub.</p><p>If you don&#8217;t configure this, Entra ID activity only reaches your SIEM through whatever subset the UAL happens to capture. And the UAL&#8217;s capture of Entra ID activity is incomplete. Sign-in logs in particular &#8212; who authenticated, from where, with what client, whether MFA fired, what conditional access evaluated &#8212; require this separate pipeline. These matter even if you only use M365: even if your organization has zero Azure infrastructure, zero VMs, zero virtual networks, logs collected by the Entra ID Diagnostic settings are still critical M365 telemetry because Entra ID is your M365 identity plane. It&#8217;s not an &#8220;Azure thing&#8221; you can ignore because you&#8217;re SaaS-only.</p><p>I made this exact point in the first post: a team can have Sentinel deployed, dashboards built, and detection rules running, with zero visibility into authentication activity because the <a href="https://learn.microsoft.com/en-us/azure/sentinel/connect-azure-active-directory">Entra ID data connector</a> was never configured. The dashboard stays green. Nothing fires. And the absence of signal looks exactly like the absence of threat.</p><h3><strong>Defender for Cloud Apps (MDCA) Connector</strong></h3><p>The M365 connector in Defender for Cloud Apps has a checkbox that can quietly determine whether one of the most important tables in Defender XDR gets populated or stays empty.</p><p>To populate the CloudAppEvents table, you need to go to the Defender portal &#8594; Settings &#8594; Cloud apps &#8594; App connectors, and ensure the <a href="https://learn.microsoft.com/en-us/defender-xdr/advanced-hunting-cloudappevents-table">&#8220;Microsoft 365 activities&#8221; checkbox is actually selected</a>. Sometimes the connector shows as &#8220;connected&#8221; in the portal but isn&#8217;t fully enabled. If this checkbox isn&#8217;t selected, CloudAppEvents will be empty or partial &#8212; and there&#8217;s no error, no alert, and no indication that anything is wrong. Microsoft&#8217;s own documentation confirms this: &#8220;If your organization hasn&#8217;t deployed the service in Microsoft Defender XDR, queries that use the table aren&#8217;t going to work or return any results.&#8221;</p><p>Jeffrey Appel&#8217;s <a href="https://jeffreyappel.nl/2025-microsoft-defender-optimization-configuration-cheat-sheet/">2025 Microsoft Defender Optimization &amp; Configuration Cheat Sheet</a> flagged this as one of the settings he still sees overlooked daily. His advice: if you have an MDCA license and haven&#8217;t verified the connector, do it now.</p><h3><strong>Exchange Online PowerShell</strong></h3><p>Exchange Powershell is not a log source you ingest into a SIEM, but it&#8217;s the only place where you can verify what&#8217;s <em>currently</em> being logged at the per-user level for mailbox settings. The <code>Get-Mailbox</code> cmdlet with its audit properties is the only way to see the actual audit action set for a given mailbox across Owner, Delegate, and Admin sign-in types.</p><p>The <a href="https://learn.microsoft.com/en-us/purview/audit-mailboxes">DefaultAuditSet mechanism</a> is supposed to handle this automatically &#8212; but if a mailbox has been explicitly customized (even once, even years ago), it falls out of the default management path. New audit actions won&#8217;t be added. The only way to know is to check, and the only way to check is PowerShell.</p><p>This is also where you verify that the tenant-level <code>UnifiedAuditLogIngestionEnabled</code> property is set to <code>True</code> &#8212; but <a href="https://www.reddit.com/r/Office365/comments/1ccp0qy/getadminauditlogconfig_returns_false_negative_for/">even that has a gotcha</a>. The <code>Get-AdminAuditLogConfig</code> cmdlet is available in both Exchange Online PowerShell and Security &amp; Compliance PowerShell, but the <code>UnifiedAuditLogIngestionEnabled</code> property <em>always returns False</em> in Security &amp; Compliance PowerShell, even when auditing is actually on. You have to run it in Exchange Online PowerShell specifically.</p><p><strong>What about detecting changes to these settings?</strong> There&#8217;s an important distinction here between <em>reading current state</em> and <em>detecting changes</em>. PowerShell is the only tool for the former &#8212; but the latter is a detection engineering problem, and the telemetry does exist in the UAL.</p><h3><strong>Management Activity API Subscriptions</strong></h3><p>The <a href="https://learn.microsoft.com/en-us/office/office-365-management-api/office-365-management-activity-api-reference">Office 365 Management Activity API</a> is the programmatic interface that most SIEMs use to pull audit data. It has five content types:</p><ul><li><p><strong>Audit.Exchange</strong> &#8212; Mail and calendaring events</p></li><li><p><strong>Audit.SharePoint</strong> &#8212; SharePoint and OneDrive events</p></li><li><p><strong>Audit.AzureActiveDirectory</strong> &#8212; Entra ID events</p></li><li><p><strong>Audit.General</strong> &#8212; Everything else: Teams, Power Platform, Copilot, Defender audit trails, and more</p></li><li><p><strong>DLP.All</strong> &#8212; Data loss prevention events</p></li></ul><p>Each content type requires an explicit subscription. Subscriptions persist once started, but there&#8217;s no health monitoring, no status dashboard, and no notification if a subscription fails or stops delivering data.</p><p>Here&#8217;s the configuration gap that catches people: most teams subscribe to Exchange, SharePoint, and AzureActiveDirectory &#8212; the obvious ones. They forget Audit.General. And Audit.General is where Teams data lives, where Power Platform events live, where Copilot activity lands, and where Defender XDR&#8217;s own audit trails flow. If you&#8217;re not subscribed to Audit.General, entire workloads are invisible to your SIEM.</p><p><strong>The key takeaway from this section:</strong> Five separate configuration surfaces. Four different admin portals. Five different RBAC requirements. No unified view of &#8220;is my auditing fully enabled?&#8221; This fragmentation is the first blind spot &#8212; and it&#8217;s architectural, not operational. You can&#8217;t solve it with better SOC processes. You have to know the surfaces exist and check each one independently.</p><h2><strong>What the Actual Log Tables Are</strong></h2><p>Once telemetry is configured, it flows into tables. But the word &#8220;flows&#8221; does a lot of work in that sentence, because these aren&#8217;t different views of the same data. They&#8217;re different pipes with different schemas, different coverage, different retention, and different license gates. A detection engineer writing KQL against one table is seeing a fundamentally different slice of reality than one writing against another.</p><p><strong>A note on naming</strong>: the table names below are Sentinel and Defender XDR names, because that&#8217;s where the schema and coverage documentation lives. If you&#8217;re on a non-Microsoft SIEM, the underlying data is the same &#8212; the Management Activity API is a REST API that Splunk, Elastic, and every other SIEM polls directly; Entra ID Diagnostic Settings can route to an Event Hub for any platform; and the Defender XDR Streaming API can export XDR tables to Event Hub or Storage. The coverage gaps, schema limitations, and retention constraints documented here are properties of the <em>data sources</em>, not the SIEM. They apply regardless of where the data lands.</p><p>Here&#8217;s what actually exists:</p><h3><strong>OfficeActivity (Sentinel)</strong></h3><ul><li><p><strong>Source:</strong> Management Activity API &#8594; Sentinel&#8217;s Office 365 data connector </p></li><li><p><strong>License gate:</strong> Microsoft Sentinel + M365 connector (free to ingest) </p></li><li><p><strong>Retention:</strong> Workspace-configured (typically 90 days default, extendable) </p></li><li><p><strong>Coverage:</strong> Exchange, Teams, SharePoint, OneDrive &#8212; but only the workloads that flow through the Management Activity API content types you&#8217;ve subscribed to. Not Audit.General workloads unless you&#8217;ve set up a custom ingestion path.</p></li></ul><p>OfficeActivity is where most Sentinel users start, because the Office 365 data connector is free and obvious &#8212; and non-Microsoft SIEMs land the same data by polling the Management Activity API directly. But it has a significant schema limitation: it doesn&#8217;t include the <code>OperationCount</code> field. Microsoft uses this field to flag aggregated events &#8212; where a single audit record actually represents multiple operations. In <a href="https://kqlquery.com/posts/unified-audit-logs-coverage-gaps/">Bert-Jan Pals&#8217; testing</a>, more than one-third of events were aggregated. Without <code>OperationCount</code>, you can&#8217;t distinguish a single event from a batch. Your event counts are wrong. Your thresholds are wrong. And you have no way to know from the data itself.</p><p>In Bert-Jan&#8217;s coverage testing against <a href="https://kqlquery.com/posts/unified-audit-logs-coverage-gaps/">191 unique activities</a> performed in a default-configured tenant, OfficeActivity captured roughly 40% of them.</p><h3><strong>CloudAppEvents (Defender XDR)</strong></h3><ul><li><p><strong>Source:</strong> MDCA M365 connector </p></li><li><p><strong>License gate:</strong> E5 Security, or standalone MDCA license </p></li><li><p><strong>Retention:</strong> 30 days in Defender XDR (can be extended with Sentinel Data Lake ingestion) </p></li><li><p><strong>Coverage:</strong> Broader than OfficeActivity. Absorbing more workloads over time &#8212; Copilot activity, Defender XDR audit trails, Sentinel platform operations. Bert-Jan&#8217;s testing showed approximately 89% coverage of the 191 tested activities.</p></li></ul><p>CloudAppEvents is increasingly where Microsoft is consolidating M365 audit visibility in the Defender XDR ecosystem. But it has its own gaps. It doesn&#8217;t include <code>UserLoggedIn</code> or <code>UserLoginFailed</code> &#8212; those come through the sign-in log tables (more on those below). It has gaps in some Exchange activities compared to what the UAL captures. And it includes at least one event &#8212; <code>Broke sharing inheritance</code> for OneDrive &#8212; that doesn&#8217;t appear in the UAL at all.</p><p>That last point is important: the relationship between CloudAppEvents and the UAL isn&#8217;t strictly &#8220;CloudAppEvents is a subset.&#8221; They&#8217;re overlapping but distinct data sources. You can&#8217;t assume one is a superset of the other.</p><h3><strong>EntraIdSignInEvents (Defender XDR)</strong></h3><ul><li><p><strong>Source:</strong> Entra ID sign-in logs, surfaced in Defender XDR </p></li><li><p><strong>License gate:</strong> Entra ID P2</p></li><li><p> <strong>Retention:</strong> 30 days in Defender XDR </p></li><li><p><strong>Coverage:</strong> Interactive and non-interactive user sign-ins &#8212; the authentication telemetry that CloudAppEvents doesn&#8217;t capture.</p></li></ul><p>This table <a href="https://learn.microsoft.com/en-us/defender-xdr/whats-new">went GA in February 2026</a>, replacing the former <code>AADSignInEventsBeta</code>. If your KQL queries or custom detection rules still reference the old table name, they should have been auto-migrated &#8212; but verify. The rename happened on <a href="https://learn.microsoft.com/en-us/defender-xdr/advanced-hunting-schema-changes">December 9, 2025</a>, with a 30-day coexistence period.</p><p>In Sentinel, the equivalent is the <code>SigninLogs</code> table, which flows through Entra ID Diagnostic Settings and requires at least an Entra ID P1 license. These are parallel pipelines to the same underlying data &#8212; but with different schemas, different enrichment, and different retention depending on your configuration.</p><h3><strong>EntraIdSpnSignInEvents (Defender XDR)</strong></h3><ul><li><p><strong>Source:</strong> Entra ID service principal and managed identity sign-in logs </p></li><li><p><strong>License gate:</strong> Entra ID P2 </p></li><li><p><strong>Retention:</strong> 30 days in Defender XDR</p></li></ul><p>This table also <a href="https://learn.microsoft.com/en-us/defender-xdr/advanced-hunting-entraidspnsigninevents-table">went GA in February 2026</a>. It&#8217;s the Defender XDR equivalent of <code>AADServicePrincipalSignInLogs</code> in Sentinel &#8212; and it covers the identity types I described in Problem 2 of the first post. Service principals and managed identities authenticate through paths that most detection rules never touch. This table is where that telemetry lives in the XDR ecosystem.</p><p>If you built your authentication detections against <code>EntraIdSignInEvents</code> (or its predecessor) and stopped there, you&#8217;re blind to service principal authentication. Same access, different table, no alert.</p><h3><strong>GraphApiAuditEvents (Defender XDR)</strong></h3><ul><li><p><strong>Source:</strong> Automatic with Defender XDR </p></li><li><p><strong>License gate:</strong> Included with Defender XDR (no additional cost) </p></li><li><p><strong>Retention:</strong> 30 days </p></li><li><p><strong>Coverage:</strong> Microsoft Graph API requests made against tenant resources &#8212; who called what endpoint, when, from where, and what the response was.</p></li></ul><p>This is one of the most significant recent additions to the M365 detection surface. GraphApiAuditEvents entered public preview in July 2025 and <a href="https://learn.microsoft.com/en-us/defender-xdr/whats-new">went GA in February 2026</a>. It&#8217;s essentially a free version of the <code>MicrosoftGraphActivityLogs</code> table in Sentinel &#8212; which is valuable for detection but expensive to ingest due to high log volume.</p><p>The trade-off is schema depth. <a href="https://kqlquery.com/posts/graphapiauditevents/">MicrosoftGraphActivityLogs has 33 columns; GraphApiAuditEvents has 19.</a> The fields that are missing matter for detection engineers: <code>DeviceId</code> (the device from which the Graph API call was made) and <code>SessionId</code> (which lets you correlate Graph activity to sign-in sessions) are both absent. The <code>UserId</code> and <code>ServicePrincipalId</code> fields from MicrosoftGraphActivityLogs have been concatenated into a single <code>AccountObjectId</code> column, which simplifies some queries but loses the ability to immediately distinguish human vs. application callers.</p><p><a href="https://cloudbrothers.info/en/detect-threats-graphapiauditevents-part-3/">Fabian Bader at Cloudbrothers</a> documented an additional wrinkle when the table launched: several columns listed in Microsoft&#8217;s documentation weren&#8217;t actually present in the data. The schema was aspirational, not actual. This is exactly the kind of silent gap that erodes detection confidence &#8212; your query doesn&#8217;t error, it just returns null for fields you expected to be populated.</p><p>GraphApiAuditEvents cannot currently be forwarded to Sentinel to extend its retention beyond 30 days (though Sentinel Data Lake ingestion for Defender XDR tables is now GA, so this may change). For incident response, 30 days of Graph API visibility is better than zero &#8212; but if you need historical depth, MicrosoftGraphActivityLogs in Sentinel remains the paid option.</p><h3><strong>MicrosoftGraphActivityLogs (Sentinel)</strong></h3><ul><li><p><strong>Source:</strong> <a href="https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-configure-diagnostic-settings">Entra ID Diagnostic Settings</a> </p></li><li><p><strong>License gate:</strong> Entra ID P1 + Sentinel </p></li><li><p><strong>Retention:</strong> Workspace-configured </p></li><li><p><strong>Coverage:</strong> Same underlying data as GraphApiAuditEvents, but with the full 33-column schema including DeviceId and SessionId.</p></li></ul><p>This table is expensive. The volume of Graph API traffic in any active tenant is enormous, and every request generates a log entry. But for organizations doing serious incident response or threat hunting against application-layer attacks &#8212; compromised OAuth apps, token theft, Graph API abuse by tools like AzureHound or GraphRunner &#8212; it&#8217;s uniquely valuable. The cost is why GraphApiAuditEvents exists: Microsoft recognized that most organizations couldn&#8217;t justify the Sentinel ingestion bill for these logs, so they shipped a lighter version for free in Defender XDR.</p><h3><strong>Entra ID Log Tables (Sentinel / Log Analytics)</strong></h3><ul><li><p><strong>Source:</strong> <a href="https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-configure-diagnostic-settings">Entra ID Diagnostic Settings</a> &#8594; Log Analytics workspace (or Event Hub, or Storage Account) </p></li><li><p><strong>License gate:</strong> Entra ID Free or E3+ for <code>AuditLogs</code>; Entra ID P1 or P2 for sign-in log tables </p></li><li><p><strong>Retention:</strong> Workspace-configured when sent to Log Analytics; destination-dependent otherwise </p></li><li><p><strong>Coverage:</strong> Authentication activity, directory changes, provisioning events, risk detections, and Graph API activity &#8212; the identity plane telemetry that doesn&#8217;t flow through the UAL or Management Activity API.</p></li></ul><p>Earlier in this post I described Entra ID Diagnostic Settings as one of the five configuration surfaces. Here&#8217;s where that pipeline&#8217;s data actually lands.</p><p>When you configure a diagnostic setting in Entra ID, you select which log categories to export and where to send them. The destination matters more than most people realize, because it determines what you can <em>do</em> with the data. There are three options:</p><p>A <strong>Log Analytics workspace</strong> is what Sentinel queries. When you send Entra ID logs to a Log Analytics workspace &#8212; whether directly through diagnostic settings or through the <a href="https://learn.microsoft.com/en-us/azure/sentinel/connect-azure-active-directory">Sentinel Entra ID data connector</a> (which configures the same underlying diagnostic settings) &#8212; they land in specific tables that you can write KQL against, build detection rules on, and hunt through. This is the path that makes the data actionable for detection engineering.</p><p>An <strong>Event Hub</strong> is the path for non-Microsoft SIEMs. If you&#8217;re running Splunk, Elastic, Sumo Logic, or any other platform, this is how Entra ID telemetry gets to your SIEM. The data is the same &#8212; same log categories, same underlying events &#8212; but the format, schema, and enrichment depend entirely on how your SIEM&#8217;s ingestion pipeline handles Event Hub messages. This is the equivalent path for organizations not in the Microsoft Sentinel ecosystem, and it&#8217;s just as critical to configure. If you&#8217;re on Splunk and haven&#8217;t set up an Event Hub destination for Entra ID diagnostic settings, you have the same blind spot as a Sentinel shop that never enabled the data connector.</p><p>A <strong>Storage Account</strong> is primarily for archival and compliance &#8212; long-term retention where query latency isn&#8217;t a concern.</p><p>You can configure multiple diagnostic settings simultaneously, sending the same log categories to more than one destination. But each destination is independent: nothing about sending logs to an Event Hub guarantees they&#8217;re also flowing to Log Analytics, or vice versa.</p><p><strong>The Log Analytics tables.</strong> When the destination is a Log Analytics workspace, each diagnostic settings category maps to a specific table &#8212; but the category names and table names don&#8217;t match, which is a source of confusion. Here&#8217;s the mapping for the tables that matter most for detection:</p><p>The <code>SigninLogs</code> table captures interactive user sign-ins. This is the richest authentication telemetry available: conditional access evaluation results, MFA details, device information, client app, location, risk scoring. If you&#8217;re building detections around authentication anomalies, compromised credentials, or conditional access bypass, this is where the signal lives. Requires Entra ID P1 or higher to export.</p><p><code>AADNonInteractiveUserSignInLogs</code> captures sign-ins performed by a client on behalf of a user &#8212; token refreshes, background app activity, SSO sessions. These are high-volume (often dramatically higher than interactive sign-ins) and frequently overlooked. An attacker with a stolen refresh token generates non-interactive sign-ins, not interactive ones. If you&#8217;re only monitoring <code>SigninLogs</code>, you&#8217;re watching the front door while the side entrance is unmonitored. Also requires P1+.</p><p><code>AADServicePrincipalSignInLogs</code> covers sign-ins by apps and service principals &#8212; the identity types I described in Problem 2 of the first post. This is the Sentinel equivalent of <code>EntraIdSpnSignInEvents</code> in Defender XDR. Requires P1+.</p><p><code>AADManagedIdentitySignInLogs</code> covers sign-ins by Azure managed identities. Lower volume than service principal logs in most tenants, but critical if your environment uses managed identities for automation or cross-service access. Requires P1+.</p><p><code>AuditLogs</code> captures directory changes &#8212; user and group management, application registration changes, role assignments, policy modifications. This is the only table in this group that doesn&#8217;t require a premium license; it can be exported with Entra ID Free or any E3+ plan.</p><p>(I&#8217;ve already covered <code>MicrosoftGraphActivityLogs</code> in its own section above &#8212; it also flows through this same diagnostic settings pipeline.)</p><p><strong>A naming trap worth flagging:</strong> the category names you select in diagnostic settings don&#8217;t match the table names in Log Analytics. <code>NonInteractiveUserSignInLogs</code> in the diagnostic settings UI becomes <code>AADNonInteractiveUserSignInLogs</code> in Log Analytics. <code>ServicePrincipalSignInLogs</code> becomes <code>AADServicePrincipalSignInLogs</code>. Same data, different names depending on where you&#8217;re looking. Thomas Naunheim <a href="https://www.cloud-architekt.net/auditing-of-msi-and-service-principals/">documented this mismatch</a> &#8212; it&#8217;s the kind of thing that causes silent query failures if you&#8217;re writing KQL against the diagnostic settings category name instead of the actual table name.</p><p><strong>The detection engineering gap this fills.</strong> The earlier sections of this post covered <code>EntraIdSignInEvents</code> and <code>EntraIdSpnSignInEvents</code> in Defender XDR. Those tables are the XDR-side view of the same underlying authentication data. But there are meaningful differences: the Sentinel tables have richer schemas (full conditional access policy evaluation details, device compliance state, authentication method details), workspace-configured retention instead of the fixed 30-day window in Defender XDR, and &#8212; critically &#8212; they split non-interactive and managed identity sign-ins into their own dedicated tables rather than combining identity types.</p><p>If your detection strategy lives in Sentinel, these are the tables your authentication rules should be built against. If your detection strategy lives in Defender XDR, the <code>EntraId*</code> tables covered earlier are the equivalent. If you&#8217;re on a non-Microsoft SIEM, the Event Hub path delivers this same data &#8212; but only if someone configured the diagnostic setting, selected the right log categories, and pointed them at your Event Hub. That&#8217;s three things that can be wrong, and none of them will tell you if they are.</p><h3><strong>Purview Audit Search (UAL Direct)</strong></h3><ul><li><p><strong>Source:</strong> Direct query against the Unified Audit Log </p></li><li><p><strong>License gate:</strong> E3+ for Standard, E5 for Premium </p></li><li><p><strong>Retention:</strong> 180 days (Standard), 1 year (Premium), up to 10 years with add-on </p></li><li><p><strong>Coverage:</strong> ~99.5% of all audited activities in Bert-Jan&#8217;s testing &#8212; the most complete view available.</p></li></ul><p>This is not a SIEM table. You can&#8217;t write detection rules against it. You can&#8217;t set up automated alerts. It&#8217;s a query interface &#8212; available through the Purview portal, through <code>Search-UnifiedAuditLog</code> in Exchange Online PowerShell, or through the <a href="https://learn.microsoft.com/en-us/purview/audit-solutions-overview">Purview Audit Search Graph API</a> (which Microsoft moved back to beta in April 2025 to address stability issues, and has not yet returned to v1.0 GA).</p><p>But for incident response, it&#8217;s the canonical source. When you need to know what actually happened &#8212; not what made it through your ingestion pipeline &#8212; the UAL is where you go. Tools like the <a href="https://github.com/invictus-ir/Microsoft-Extractor-Suite">Invictus Incident Response Microsoft Extractor Suite</a> are built specifically to extract data from this source when your SIEM tables aren&#8217;t enough.</p><h3><strong>The Coverage Gap at a Glance</strong></h3><p>Bert-Jan Pals&#8217; <a href="https://kqlquery.com/posts/unified-audit-logs-coverage-gaps/">research</a> quantified what practitioners had suspected: of 191 unique activities tested in a default-configured tenant, OfficeActivity captured roughly 40%. CloudAppEvents captured roughly 89%. Purview Audit Search captured roughly 99.5%. None reached 100%.</p><p>Those numbers should be uncomfortable. If you&#8217;re running detections exclusively against OfficeActivity, you have visibility into less than half of what&#8217;s happening. If you&#8217;re on CloudAppEvents, you&#8217;re in better shape &#8212; but you&#8217;re still missing roughly one in ten events compared to what the UAL sees.</p><p>And none of these tools log everything. Bert-Jan&#8217;s testing was explicit about this: &#8220;None of the acquisition methods get 100% coverage on the performed activities, meaning you need to combine acquisition methods to get a complete overview.&#8221;</p><h2><strong>How Collection Actually Works</strong></h2><p>Let&#8217;s trace a concrete example to make this real. A user accesses a mailbox item &#8212; the <code>MailItemsAccessed</code> event. This is the exact event that was central to the Storm-0558 detection: the State Department&#8217;s SOC built custom alerting on MailItemsAccessed events and caught an anomalous AppID accessing mailboxes. Without this event, the compromise may have gone undetected.</p><p>Here&#8217;s what happens when that action occurs:</p><p><strong>Step 1: The user action occurs.</strong> Exchange Online generates an audit record.</p><p><strong>Step 2: The record enters the UAL.</strong> It becomes available via Purview Audit Search &#8212; <em>if</em> auditing is enabled for that user, <em>if</em> MailItemsAccessed is in their mailbox audit action set, and <em>if</em> the event isn&#8217;t being throttled (Microsoft throttles MailItemsAccessed if more than 1,000 records are generated on a mailbox within 24 hours &#8212; no logging for 24 hours after throttling).</p><p><strong>Step 3: From the UAL, data flows outward via multiple diverging paths:</strong></p><p><strong>Path A: Management Activity API &#8594; Your SIEM.</strong> Your SIEM polls the Management Activity API for content blobs from the Audit.Exchange subscription. The content is available for 7 days &#8212; after that, the blobs expire with no backfill. Typical latency is 60-90 minutes, but Microsoft <a href="https://learn.microsoft.com/en-us/office/office-365-management-api/troubleshooting-the-office-365-management-activity-api">explicitly does not commit</a> to a specific delivery time: &#8220;some issues may arise upstream from the Audit service and are unavoidable.&#8221; There&#8217;s no server-side filtering &#8212; you get all of Audit.Exchange, not just the events you care about. And duplicates are expected: &#8220;the Office 365 Management Activity API does not have this de-duplication feature... It is the responsibility of the SIEM solution to implement logic to remove such duplicated events.&#8221; In Sentinel, this lands in the <strong>OfficeActivity</strong> table.</p><p><strong>Path B: MDCA connector &#8594; CloudAppEvents.</strong> The same underlying event flows through the Defender for Cloud Apps M365 connector into CloudAppEvents. Different enrichment, different schema, and approximately 89% coverage &#8212; meaning some events present in the UAL won&#8217;t appear here. The inverse is also true: some events appear in CloudAppEvents that aren&#8217;t in the UAL.</p><p><strong>Path C: <a href="https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-configure-diagnostic-settings">Entra ID Diagnostic Settings</a> &#8594; Sign-in logs.</strong> This path doesn&#8217;t carry the MailItemsAccessed event &#8212; it&#8217;s only for Entra ID and Graph API activity, not Exchange/SharePoint/Teams. But it&#8217;s the path that carries the authentication context that <em>surrounds</em> the mailbox access: who the user was, how they authenticated, whether conditional access evaluated the session. If Path C isn&#8217;t configured, you might see the MailItemsAccessed event in your SIEM but have no authentication context to correlate it with.</p><p><strong>The critical insight:</strong> Your SIEM is not querying the UAL. It&#8217;s querying whatever subset of the UAL made it through one specific pipe. And different pipes have different coverage, different latency, different retention, and different schemas. A detection built against OfficeActivity is evaluating a fundamentally different data set than one built against CloudAppEvents, even when both are nominally covering &#8220;Exchange audit events.&#8221;</p><h2><strong>Where the Blind Spots Hide</strong></h2><p>You now have the map. Here&#8217;s what&#8217;s missing at each layer.</p><h3><strong>Configuration Blind Spots</strong></h3><p><strong>UAL enabled, but per-mailbox actions not in DefaultAuditSet.</strong> If anyone has ever customized a mailbox&#8217;s audit configuration, new events like MailItemsAccessed won&#8217;t be added automatically. The only way to verify is PowerShell, per mailbox. There&#8217;s no bulk verification tool in any admin portal.</p><p><strong>MDCA connector &#8220;connected&#8221; but M365 Activities not fully enabled.</strong> CloudAppEvents stays empty. No error. No alert. Just silence.</p><p><strong>Management Activity API subscribed to Exchange/SharePoint/AzureAD but not Audit.General.</strong> You&#8217;re missing Teams, Power Platform, Copilot, Defender XDR audit trails &#8212; entire workloads are invisible.</p><p><strong><a href="https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-configure-diagnostic-settings">Entra ID Diagnostic Settings</a> not configured.</strong> This is a big one. No sign-in logs flowing to your SIEM. Your authentication visibility is limited to whatever the UAL captures through the Management Activity API &#8212; which doesn&#8217;t include the rich sign-in log schema with conditional access evaluation, MFA details, device information, or risk scoring.</p><h3><strong>Coverage Blind Spots</strong></h3><p><strong>OfficeActivity is approximately 40% coverage.</strong> For detection engineering, that means more than half of audited activities in a default-configured tenant are invisible to rules running against this table.</p><p><strong>OfficeActivity is missing OperationCount.</strong> You cannot distinguish aggregated events from single events. In Bert-Jan&#8217;s testing, more than a third of events were aggregated. Your event counts, your detection thresholds, and your frequency-based rules are operating on inflated or deflated numbers, and you have no way to tell which.</p><p><strong>CloudAppEvents is approximately 89% coverage.</strong> Better, but roughly one in ten events compared to the UAL is missing. And it doesn&#8217;t include UserLoggedIn/UserLoginFailed &#8212; those come from the sign-in log tables.</p><p><strong>GraphApiAuditEvents is missing DeviceId and SessionId compared to MicrosoftGraphActivityLogs.</strong> If your detection logic depends on correlating Graph API activity to specific devices or sign-in sessions, the free table won&#8217;t support it.</p><p><strong>Neither OfficeActivity nor CloudAppEvents captures everything in the UAL.</strong> And the UAL itself doesn&#8217;t capture everything &#8212; it&#8217;s at 99.5%, not 100%. The only event Bert-Jan found exclusively in CloudAppEvents and not in the UAL was <code>Broke sharing inheritance</code> for OneDrive. The relationship between these data sources is overlapping, not hierarchical.</p><h3><strong>Retention Blind Spots</strong></h3><p><strong>Management Activity API content blobs expire after 7 days.</strong> If your SIEM goes down, if your ingestion pipeline breaks, if there&#8217;s a credential issue with your API subscription &#8212; those events are gone. There&#8217;s no backfill mechanism.</p><p><strong>GraphApiAuditEvents: 30 days, and currently cannot be extended by forwarding to Sentinel.</strong> For incident response involving Graph API abuse, 30 days may not be enough. MicrosoftGraphActivityLogs in Sentinel gives you workspace-configured retention, but at significant cost.</p><p><strong>CloudAppEvents: 30 days in Defender XDR.</strong> Same constraint. Sentinel Data Lake ingestion for Defender XDR tables is now GA, which may help extend this &#8212; but it&#8217;s an additional configuration step that most organizations haven&#8217;t implemented.</p><p><strong>Your SIEM retention may be longer than all of these</strong> &#8212; but it only covers data that actually made it into the pipe. Retention is meaningless for events that were never collected.</p><h3><strong>The Meta Blind Spot</strong></h3><p>This is the one that makes all the others invisible: <strong>there is no health monitoring for any of this.</strong></p><p>No alert when a Management Activity API subscription fails. No dashboard showing &#8220;here&#8217;s what&#8217;s flowing and here&#8217;s what&#8217;s not.&#8221; No completeness verification. No cross-check mechanism to confirm your SIEM received all events the UAL generated. No way to distinguish &#8220;nothing happened&#8221; from &#8220;we stopped collecting.&#8221;</p><p>Microsoft provides no native solution for telemetry health monitoring across these ingestion paths. The <a href="https://www.cisa.gov/sites/default/files/2025-01/microsoft-expanded-cloud-logs-implementation-playbook-508c.pdf">CISA playbook</a> provides manual verification steps &#8212; PowerShell commands to check UAL status, steps to verify mailbox audit configuration &#8212; but these are point-in-time checks, not continuous monitoring. The moment after you verify, configuration can drift, subscriptions can break, and you&#8217;ll hear nothing about it until an incident investigation reveals the gap.</p><p>This is the foundational risk underneath everything else. You can configure all five surfaces correctly. You can understand which tables cover what. You can trace every collection path. But without continuous verification that data is actually flowing, your entire detection pipeline is built on trust. And in SaaS environments &#8212; where the vendor controls the instrumentation, the schemas, and the delivery &#8212; trust is not a detection strategy.</p><h2><strong>What to Do With This</strong></h2><p>I&#8217;ll go deeper on methodology in a future post, but here&#8217;s what you should take away today:</p><p><strong>Verify your specific ingestion path.</strong> Don&#8217;t assume you know which pipe you&#8217;re on. Check whether it&#8217;s the Management Activity API, the MDCA connector, Entra ID Diagnostic Settings, or some combination. Know exactly which tables your detections query and what those tables actually contain.</p><p><strong>Don&#8217;t assume OfficeActivity = UAL. Don&#8217;t assume CloudAppEvents = UAL.</strong> These are different pipes with different coverage. If you built detections against OfficeActivity and haven&#8217;t cross-checked against CloudAppEvents or Purview Audit Search, your coverage map has unknown gaps.</p><p><strong>Check Audit.General.</strong> If your Management Activity API subscriptions don&#8217;t include it, you&#8217;re missing Teams, Power Platform, Copilot, and Defender audit activity entirely.</p><p><strong>Verify <a href="https://learn.microsoft.com/en-us/purview/audit-mailboxes">per-mailbox audit configuration</a> with PowerShell.</strong> Especially for high-value mailboxes &#8212; executives, finance, legal, anyone likely to be targeted. The DefaultAuditSet mechanism is good but brittle: a single historical customization takes a mailbox out of automatic management permanently.</p><p><strong>If you&#8217;re doing incident response, always supplement SIEM data with direct UAL extraction.</strong> The <a href="https://github.com/invictus-ir/Microsoft-Extractor-Suite">Invictus Incident Response Microsoft Extractor Suite</a>, Purview Audit Search (including the Graph API in beta), or direct PowerShell extraction. Your SIEM shows you what made it through the pipe. The UAL shows you what actually happened.</p><p><strong>Consider <a href="https://learn.microsoft.com/en-us/defender-xdr/advanced-hunting-graphapiauditevents-table">GraphApiAuditEvents</a> as a baseline for Graph API visibility.</strong> It went GA in February 2026 and it&#8217;s free with Defender XDR. The schema is thinner than MicrosoftGraphActivityLogs, but 19 columns of Graph API telemetry for free is better than zero visibility. If you&#8217;re investigating OAuth app compromise, token theft, or reconnaissance via Graph enumeration tools, this table is now your starting point.</p><p>Next post, we&#8217;ll go deeper on one of the silent failure modes that makes all of this worse &#8212; and show you what it looks like when collection breaks without telling you.</p><div><hr></div><h3><strong>References &amp; Credits</strong></h3><ul><li><p><strong>Bert-Jan Pals</strong> &#8212; <a href="https://kqlquery.com/posts/unified-audit-logs-coverage-gaps/">&#8220;UAL = Unaligned Activity Logs&#8221;</a> (kqlquery.com, November 2024). The coverage testing against 191 unique activities and the OfficeActivity/CloudAppEvents/Purview Audit comparison that quantifies the gaps discussed throughout this post.</p></li><li><p><strong>Bert-Jan Pals</strong> &#8212; <a href="https://kqlquery.com/posts/graphapiauditevents/">&#8220;GraphApiAuditEvents: The new Graph API Logs&#8221;</a> (kqlquery.com, August 2025). Schema comparison between GraphApiAuditEvents and MicrosoftGraphActivityLogs, including the missing DeviceId/SessionId fields and retention constraints.</p></li><li><p><strong>Fabian Bader (Cloudbrothers)</strong> &#8212; <a href="https://cloudbrothers.info/en/detect-threats-graphapiauditevents-part-3/">&#8220;Detect threats using GraphAPIAuditEvents - Part 3&#8221;</a> (cloudbrothers.info, August 2025). Detection engineering with the new table, including documentation of schema gaps at launch and Thomas Naunheim&#8217;s comparison table.</p></li><li><p><strong>CISA</strong> &#8212; <a href="https://www.cisa.gov/sites/default/files/2025-01/microsoft-expanded-cloud-logs-implementation-playbook-508c.pdf">&#8220;Microsoft Expanded Cloud Logs Implementation Playbook&#8221;</a> (January 2025). The practitioner playbook for operationalizing expanded audit logs, including mailbox-level verification procedures and the context on Storm-0558 detection.</p></li><li><p><strong>Microsoft Learn</strong> &#8212; Defender XDR <a href="https://learn.microsoft.com/en-us/defender-xdr/whats-new">&#8220;What&#8217;s new&#8221;</a> documentation confirming February 2026 GA for EntraIdSignInEvents, EntraIdSpnSignInEvents, and GraphApiAuditEvents. <a href="https://learn.microsoft.com/en-us/defender-xdr/advanced-hunting-schema-changes">Naming changes documentation</a> for the December 2025 AADSignInEventsBeta &#8594; EntraIdSignInEvents migration. Individual table reference pages for schema details.</p></li><li><p><strong>Microsoft Learn</strong> &#8212; <a href="https://learn.microsoft.com/en-us/office/office-365-management-api/office-365-management-activity-api-reference">Office 365 Management Activity API reference</a> and <a href="https://learn.microsoft.com/en-us/office/office-365-management-api/troubleshooting-the-office-365-management-activity-api">troubleshooting documentation</a>, confirming content blob expiry, latency expectations, and duplicate event behavior.</p></li><li><p><strong>Microsoft Learn</strong> &#8212; <a href="https://learn.microsoft.com/en-us/purview/audit-log-enable-disable">&#8220;Turn auditing on or off&#8221;</a> and <a href="https://learn.microsoft.com/en-us/purview/audit-search">&#8220;Search the audit log&#8221;</a> documentation for UAL enablement status, license requirements, and retention changes.</p></li><li><p><strong>Microsoft Learn</strong> &#8212; <a href="https://learn.microsoft.com/en-us/entra/identity/monitoring-health/concept-diagnostic-settings-logs-options">&#8220;Logs available for streaming from Microsoft Entra ID&#8221;</a> for the complete list of diagnostic settings log categories and their descriptions. <a href="https://learn.microsoft.com/en-us/azure/sentinel/connect-azure-active-directory">&#8220;Send Microsoft Entra ID data to Microsoft Sentinel&#8221;</a> for the Sentinel data connector configuration and table mapping.</p></li><li><p><strong>Jeffrey Appel</strong> &#8212; <a href="https://jeffreyappel.nl/2025-microsoft-defender-optimization-configuration-cheat-sheet/">&#8220;2025 Microsoft Defender Optimization &amp; Configuration Cheat Sheet&#8221;</a> (jeffreyappel.nl, November 2025). Configuration items that are still commonly overlooked, including MDCA connector state.</p></li><li><p><strong>Thomas Naunheim</strong> &#8212; <a href="https://www.cloud-architekt.net/auditing-of-msi-and-service-principals/">&#8220;Sign-in logs and auditing of Managed Identities and Service Principals&#8221;</a> (cloud-architekt.net). Documentation of the naming mismatch between Entra ID diagnostic settings categories and Log Analytics table names, and early coverage of service principal and managed identity sign-in log tables. Also referenced by both Bert-Jan Pals and Fabian Bader for schema comparison work between GraphApiAuditEvents and MicrosoftGraphActivityLogs.</p></li><li><p><strong>Practical365 / Tony Redmond</strong> &#8212; <a href="https://practical365.com/auditlog-query-api-deeper-look/">Coverage of the Purview Audit Search Graph API&#8217;s move back to beta</a> (April 2025) and ongoing practitioner perspective on M365 audit capabilities.</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://lydiagraslie.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Control Plane is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[You’re Probably Flying Blind]]></title><description><![CDATA[Part 1: The four-layer detection validation problem in SaaS and cloud]]></description><link>https://lydiagraslie.substack.com/p/youre-probably-flying-blind</link><guid isPermaLink="false">https://lydiagraslie.substack.com/p/youre-probably-flying-blind</guid><dc:creator><![CDATA[Lydia Graslie]]></dc:creator><pubDate>Fri, 27 Feb 2026 13:41:46 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!1uxP!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1359446a-3044-462f-bda2-6810ca5c26bf_322x322.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Most detection engineering teams believe they have coverage. They&#8217;ve written rules, tuned alerts, maybe even run a tabletop or hired a red team. The results look good. The dashboards are green.</p><p>And then something breaks &#8212; not loudly, not with an alert, but silently. A log source stops delivering. A field shifts position in an array. An API gets deprecated and the attack path you validated six months ago no longer fires the way you tested it. No one notices, because the absence of signal looks exactly like peace.</p><p>This is a problem across all of threat detection. Endpoint teams deal with it. Network teams deal with it. But nowhere is it worse than in SaaS.</p><p>In endpoint or network detection, you control the instrumentation. You deploy the agent. You configure the tap. You own the pipeline from source to SIEM. In SaaS, the vendor controls what gets logged, how it's structured, and when it changes &#8212; but you're still responsible for configuring which of those log sources actually flow into your environment. The problem is that most teams don't know what needs to be configured. The defaults leave gaps. The licensing requirements are buried in documentation. The connectors that look comprehensive aren't. And nobody tells you what you're not collecting.</p><p>That&#8217;s the world this series lives in. The problems I&#8217;m going to lay out exist everywhere in detection engineering to some degree, but SaaS is where they&#8217;re sharpest, least visible, and hardest to compensate for. It&#8217;s not one problem &#8212; it&#8217;s four, and they compound.</p><h2>Problem 1: You might be blind</h2><p>Before anything else matters &#8212; before your detection logic, before your correlation rules, before your threat model &#8212; telemetry has to arrive. And in cloud environments, arrival is not guaranteed.</p><p>Cloud logging configuration is sprawling, fragmented, and fails silently. In SaaS platforms specifically, you&#8217;re often dealing with multiple audit log types that require separate enablement, retention settings that default to short windows, and API-based log retrieval that may or may not be functioning &#8212; all without a single pane telling you what&#8217;s actually flowing. There&#8217;s no error when a log source isn&#8217;t configured. There&#8217;s no alert when audit logs aren&#8217;t being ingested. There&#8217;s just... nothing. And nothing looks exactly like &#8220;nothing happened.&#8221;</p><p>Most organizations have never systematically verified that their telemetry is actually arriving. They assume it is because they&#8217;ve never been told otherwise. That assumption is the foundation everything else is built on, and it&#8217;s unvalidated.</p><p>Here&#8217;s a concrete example from the Microsoft ecosystem.</p><p>When a security team connects Microsoft Sentinel to their Microsoft 365 environment, the first thing most people do is enable the <a href="https://learn.microsoft.com/en-us/azure/sentinel/data-connectors/office-365">Office 365 data connector</a>. It&#8217;s free, it&#8217;s obvious, and it starts flowing SharePoint activity, Exchange admin events, and Teams data into your workspace immediately. Green checkmark. Logs are flowing. Coverage achieved.</p><p>Except you&#8217;re missing the most fundamental identity telemetry in the entire ecosystem: sign-in logs.</p><p>Entra ID sign-in logs &#8212; who authenticated, from where, with what client, whether MFA was enforced, whether conditional access evaluated the session &#8212; require a <a href="https://learn.microsoft.com/en-us/azure/sentinel/connect-azure-active-directory">completely separate data connector</a>, a <a href="https://learn.microsoft.com/en-us/entra/identity/monitoring-health/concept-diagnostic-settings-logs-options">P1 or P2 license</a>, and manual configuration. They are not included in the Office 365 connector. They are not free to ingest. They don&#8217;t flow by default.</p><p>This means a team can have Sentinel deployed, dashboards built, detection rules running, and still have zero visibility into authentication activity. An attacker signs in with stolen credentials from an anomalous location, and the telemetry that would catch it simply isn&#8217;t there &#8212; not because a detection failed, but because the data was never collected. The dashboard stays green. Nothing fires. And the team has no reason to suspect anything is wrong, because the absence of signal looks exactly like the absence of threat.</p><h2>Problem 2: What you see depends on how you look</h2><p>Assume your logs are flowing. You&#8217;re not blind. Good. Now the next problem: the same malicious action, performed under different conditions, produces different telemetry.</p><p>The same API call made by a user, a service principal, and a managed identity can generate meaningfully different log entries. Fields that exist in one context are absent in another. Values that behave predictably under one identity type behave differently under another. The differences aren&#8217;t cosmetic &#8212; they&#8217;re structural.</p><p>This means a detection built against one execution path will miss the same attack performed through a different one. And most detection teams test exactly one path.</p><p>This isn&#8217;t a logging bug. It&#8217;s a fundamental characteristic of how SaaS and cloud platforms emit telemetry. These platforms have rich, complex identity models &#8212; human users, service accounts, API keys, OAuth apps, managed identities &#8212; and the telemetry surface varies across all of them. Your detection coverage is only as wide as the identity and method combinations you&#8217;ve actually validated.</p><p>Here&#8217;s what this looks like in practice, using Microsoft Entra ID as an example.</p><p>When an interactive user signs into a Microsoft cloud resource, the event lands in the <code>SigninLogs</code> table. It includes conditional access policy evaluation, device information, MFA challenge details, user risk scoring, and client application metadata. It&#8217;s a rich, detailed record &#8212; exactly the kind of telemetry detection engineers love to build rules against.</p><p>When a service principal authenticates to the same resource &#8212; using a client secret or certificate, the way an application or automation workflow would &#8212; that event doesn&#8217;t appear in <code>SigninLogs</code> at all. It lands in a completely separate table: <code>AADServicePrincipalSignInLogs</code>. And the schema is <a href="https://www.cloud-architekt.net/auditing-of-msi-and-service-principals/">materially different</a>. There are no MFA details, because service principals don&#8217;t do MFA. There&#8217;s no device information, because there&#8217;s no device. There&#8217;s no user risk score. The fields that your detection was built to inspect don&#8217;t exist.</p><p>Managed identities? A third table: <code>AADManagedIdentitySignInLogs</code>. Non-interactive user authentication &#8212; a client app refreshing a token on behalf of a user? A fourth: <code>AADNonInteractiveUserSignInLogs</code>.</p><p>That&#8217;s four separate tables for what is conceptually a single event category: &#8220;something authenticated to access a resource.&#8221; A detection rule written against <code>SigninLogs</code> &#8212; which is where most teams start, because it&#8217;s the most visible and well-documented &#8212; will never fire for the same access performed by a service principal, a managed identity, or a non-interactive token refresh. The detection doesn&#8217;t fail. It simply never evaluates the event, because the event is in a table the rule was never pointed at.</p><p>Now consider this from an attacker&#8217;s perspective. If you&#8217;ve compromised a service principal&#8217;s credentials, every action you take authenticates through a path that most organizations&#8217; detection rules were never written to cover. Not because the telemetry doesn&#8217;t exist &#8212; but because it exists somewhere the defenders aren&#8217;t looking.</p><h2>Problem 3: The execution surface shifts</h2><p>APIs change. New endpoints appear. Old ones get deprecated, modified, or replaced. The attack path you validated &#8212; the one your detection was built to catch &#8212; may no longer be the path an attacker would take.</p><p>This isn&#8217;t theoretical. SaaS providers and cloud platforms ship API changes constantly &#8212; often without announcement. A new method for modifying IAM policies, a deprecated endpoint replaced by a v2, a previously undocumented parameter that now exists &#8212; each one potentially creates an untested attack path that your existing detections don&#8217;t cover. And unlike infrastructure you own, you have no visibility into these changes until you discover them yourself.</p><p>Your offensive coverage map has an expiration date. If you&#8217;re not continuously revalidating it, it&#8217;s going stale.</p><p><strong>Example: Microsoft&#8217;s PowerShell module churn</strong></p><p>If you built attack tooling or detection validation scripts against Microsoft 365 in 2022, here&#8217;s what&#8217;s happened to the execution surface underneath you since then.</p><p>The <strong>MSOnline (MSOL)</strong> module &#8212; the original way to manage Azure AD via PowerShell &#8212; was <a href="https://techcommunity.microsoft.com/blog/microsoft-entra-blog/important-update-deprecation-of-azure-ad-powershell-and-msonline-powershell-modu/4094536">deprecated in March 2024</a> and began <a href="https://techcommunity.microsoft.com/blog/microsoft-entra-blog/action-required-msonline-and-azuread-powershell-retirement---2025-info-and-resou/4364991">retiring in April 2025</a>. The <strong>AzureAD</strong> and <strong>AzureADPreview</strong> modules followed the same deprecation timeline, with retirement targeted for Q3 2025. The <strong>Exchange Online PowerShell v1</strong> module died in mid-2023 when Microsoft killed basic authentication, and <strong>v2</strong> was <a href="https://techcommunity.microsoft.com/blog/exchange/announcing-deprecation-of-remote-powershell-rps-protocol-in-exchange-online-powe/3695597">retired months later in July 2023</a> when the Remote PowerShell (RPS) protocol was removed entirely. The <strong>Security &amp; Compliance PowerShell</strong> RPS connection was <a href="https://www.neowin.net/news/remote-powershell-protocol-to-be-deprecated-in-security-and-compliance-powershell-in-july/">deprecated on the same timeline</a>.</p><p>The replacement for the identity modules was the <strong><a href="https://learn.microsoft.com/en-us/powershell/microsoftgraph/overview">Microsoft Graph PowerShell SDK v1</a></strong>, which shipped in 2021. Organizations spent months migrating scripts from MSOL and AzureAD to the new <code>Get-Mg*</code> cmdlets. Then in July 2023, Microsoft released <strong><a href="https://github.com/microsoftgraph/msgraph-sdk-powershell/blob/dev/docs/upgrade-to-v2.md">SDK v2</a></strong> &#8212; which introduced its own set of breaking changes. The <code>Select-MgProfile</code> cmdlet that everyone had just learned to use was removed. Every beta cmdlet was renamed (e.g. <code>Get-MgUser</code> on the beta endpoint became <code>Get-MgBetaUser</code>). The module namespace changed. Every script that touched beta endpoints &#8212; which was most of them, because the v1.0 endpoint didn&#8217;t return properties like <code>AssignedLicenses</code> &#8212; needed to be rewritten <em>again</em>.</p><p>A LinkedIn post from a Microsoft 365 community account captured the sentiment: <a href="https://o365reports.com/2023/07/12/the-term-select-mgprofile-is-not-recognized-error/">&#8220;The Never-Ending Cycle of MS Graph Script Migrations.&#8221;</a> Scripts migrated from AzureAD to Graph SDK v1 now required <em>yet another</em> migration to SDK v2.</p><p>And it&#8217;s not over. <strong><a href="https://learn.microsoft.com/en-us/powershell/entra-powershell/overview">Microsoft Entra PowerShell</a></strong> is currently in preview as another incoming option, while the underlying <strong>Azure AD Graph API</strong> itself was <a href="https://techcommunity.microsoft.com/blog/microsoft-entra-blog/june-2024-update-on-azure-ad-graph-api-retirement/4094534">fully shut down in June 2025</a> after a three-year retirement cycle that was delayed at least four times.</p><p>Here&#8217;s the count: since 2022, at least <strong>seven</strong> Microsoft PowerShell modules or protocols for managing identity and messaging have been deprecated or retired. The replacement itself was deprecated within a year of people migrating to it. If you wrote an attack simulation that used <code>Connect-MsolService</code> to test credential spraying detections, or <code>Set-AzureADUserLicense</code> to simulate license manipulation, that code doesn&#8217;t run anymore. The attack technique still works &#8212; but the <em>execution path</em> through Microsoft&#8217;s tooling has changed underneath it multiple times.</p><p>For a purple team, this means your offensive playbook has a shelf life measured in months, not years. And every time the execution surface shifts, the question isn&#8217;t just &#8220;does our attack still work?&#8221; It&#8217;s &#8220;does the telemetry from this new execution path land in the same place, with the same schema, with the same fields our detection expects?&#8221;</p><p>Which brings us to Problem 4.</p><h2>Problem 4: The detection surface shifts</h2><p>Your detection might be perfectly written. The logic might be flawless. But the data it&#8217;s inspecting no longer looks the way it looked when you wrote the rule. Detection engineers find out when something silently stops firing &#8212; or worse, starts flooding with false positives.</p><p>This is the inverse of Problem 3. That was about the <em>execution</em> surface shifting &#8212; the tools and APIs attackers use. This is about the <em>telemetry</em> surface shifting &#8212; the logs, schemas, and data structures your detections are built on top of. And unlike API deprecations, which at least get blog posts and retirement timelines, telemetry changes are often silent.</p><p><strong>Example: SigninLogs and the invisible multi-record problem</strong></p><p>Here&#8217;s a subtle one. The <code>SigninLogs</code> table in Microsoft Sentinel emits <em>multiple records for a single login activity</em>. When a user authenticates, the table doesn&#8217;t just log the final result &#8212; it logs intermediate steps: the initial request, the MFA challenge, the conditional access evaluation, the final outcome. Each step is a separate row in the table, grouped by <code>Id</code> or <code>OriginalRequestId</code>.</p><p>If you didn&#8217;t know that, you&#8217;d write a detection that counts distinct sign-in events and gets inflated numbers. Or you&#8217;d match on an intermediate record that shows a &#8220;failure&#8221; status even though the overall authentication succeeded. A community member filed <a href="https://github.com/Azure/Azure-Sentinel/issues/9463">issue #9463</a> against Microsoft&#8217;s own Azure Sentinel repository pointing out that <em>none</em> of the built-in Entra ID detection rules accounted for this behavior. Every rule treating a row as a complete event was generating false positives. The fix &#8212; adding <code>| summarize arg_max(TimeGenerated, *) by Id</code> before the detection logic &#8212; is trivial once you know about it. But nothing in the schema documentation made this multi-record behavior obvious. And if Microsoft&#8217;s own first-party detection rules didn&#8217;t account for it, how many custom rules in production environments are getting it wrong right now?</p><p><strong>The scale of the shifting surface</strong></p><p>To understand why this kind of thing happens, consider the scale. The <a href="https://learn.microsoft.com/en-us/graph/overview">Microsoft Graph API</a> &#8212; the unified API surface that underpins Microsoft 365 and Entra ID &#8212; currently defines <strong>1,302 unique paths in its v1.0 endpoint</strong> and <strong>2,766 in beta</strong>. The <a href="https://github.com/microsoftgraph/msgraph-metadata">msgraph-metadata</a> repository on GitHub, which tracks the OpenAPI specification for these endpoints, has accumulated over <strong>3,400 commits</strong>. Each commit potentially changes what data flows through which endpoint, which affects what lands in your log tables, which affects whether your detection still works.</p><p>These changes don&#8217;t come with a changelog entry that says &#8220;the field your KQL rule depends on now behaves differently.&#8221; They come as metadata updates. A property gets added, renamed, or moved to a different response object. A field that used to be populated becomes null in certain conditions. An enum gains new values your <code>where</code> clause doesn&#8217;t match. The OpenAPI spec for the v1.0 endpoint alone is a 35-megabyte YAML file containing 872,000 lines. Nobody is reviewing that diff manually.</p><p>The defensive coverage map goes stale independently of the offensive one. Even if you solved Problems 1 through 3 &#8212; you confirmed your logs exist, you validated across identity types, and you tracked the API changes &#8212; your detections can still silently degrade because the telemetry they inspect shifted underneath them on Microsoft&#8217;s release schedule, not yours.</p><h2>The compounding effect</h2><p>These four problems don&#8217;t exist in isolation. They layer and multiply.</p><p>You can&#8217;t trust your detections (problem 2) without first confirming your telemetry exists (problem 1). You can&#8217;t maintain offensive coverage without tracking API changes (problem 3). And even if you solve all of that, the ground shifts underneath your defensive logic on its own schedule (problem 4).</p><p>The number of ways things silently break grows over time. Each problem multiplies the others. The gap between &#8220;what we think we&#8217;re detecting&#8221; and &#8220;what we&#8217;re actually detecting&#8221; widens every day you&#8217;re not actively validating it.</p><p>This is why purple teaming can&#8217;t be a quarterly exercise, or an annual pentest, or a one-time red team engagement. It&#8217;s a continuous validation problem. And solving it requires tooling built specifically for that purpose &#8212; not attack simulation tools repurposed for defense, but something designed from the ground up to validate the entire detection pipeline, from log ingestion through alert firing.</p><p>That&#8217;s what this series is about. Not a tool walkthrough &#8212; a way of thinking about detection validation that takes all four of these problems seriously and addresses them structurally.</p><p>Next post, we'll get concrete about Problem 1. I'll walk through where telemetry is configured in the Microsoft 365 ecosystem, what the actual log tables are, and how collection works &#8212; so you can see for yourself where the blind spots hide.</p><div><hr></div><p><em>Control Plane is a blog about building detection systems that actually work in SaaS and cloud environments. If you&#8217;re a detection engineer, purple teamer, or security leader tired of false confidence in your coverage, [subscribe] to follow the series.</em></p><div><hr></div><h3>References &amp; Credits</h3><ul><li><p><strong>Thomas Naunheim</strong> (<a href="https://www.cloud-architekt.net">@intruder_io</a>) &#8212; His work on <a href="https://www.cloud-architekt.net/auditing-of-msi-and-service-principals/">sign-in logs and auditing of Managed Identities and Service Principals</a> was an early and thorough documentation of the schema differences across Entra ID sign-in log tables, including the critical observation that fields like conditional access details and device information are absent from the service principal and managed identity schemas.</p></li><li><p><strong>Fabian Bader</strong> (<a href="https://cloudbrothers.info">Cloudbrothers</a>) &#8212; His multi-part series on <a href="https://cloudbrothers.info/en/detect-threats-microsoft-graph-logs-part-1/">detecting threats using Microsoft Graph activity logs</a> has been a valuable resource for understanding the detection surface across Microsoft&#8217;s logging ecosystem.</p></li><li><p><strong>Microsoft Learn</strong> &#8212; The Sentinel data connector documentation (<a href="https://learn.microsoft.com/en-us/azure/sentinel/connect-azure-active-directory">Send Microsoft Entra ID data to Microsoft Sentinel</a>) and the Entra ID diagnostic settings reference (<a href="https://learn.microsoft.com/en-us/entra/identity/monitoring-health/concept-diagnostic-settings-logs-options">Logs available for streaming</a>) are the primary sources for the licensing requirements and log type availability discussed in this post.</p></li><li><p><strong>Microsoft Entra Blog</strong> &#8212; The official <a href="https://techcommunity.microsoft.com/blog/microsoft-entra-blog/action-required-msonline-and-azuread-powershell-retirement---2025-info-and-resou/4364991">MSOnline and AzureAD PowerShell retirement announcement</a> and <a href="https://techcommunity.microsoft.com/blog/microsoft-entra-blog/important-update-deprecation-of-azure-ad-powershell-and-msonline-powershell-modu/4094536">deprecation update</a> document the timeline covered in Problem 3.</p></li><li><p><strong>Tony Redmond / Practical365</strong> &#8212; His ongoing coverage of the <a href="https://practical365.com/march-2024-retirement-old-azure-ad-modules/">AzureAD module retirement</a> and <a href="https://office365itpros.com/2023/07/10/graph-powershell-sdk-v2/">Graph SDK v2 migration</a> provided detailed practitioner perspective on the impact of these transitions.</p></li><li><p><strong>O365Reports.com</strong> &#8212; The community post <a href="https://o365reports.com/2023/07/12/the-term-select-mgprofile-is-not-recognized-error/">&#8220;The Never-Ending Cycle of MS Graph Script Migrations&#8221;</a> captured what many practitioners were feeling during the SDK v1&#8594;v2 transition.</p></li><li><p><strong>J3roen / Azure-Sentinel GitHub</strong> &#8212; <a href="https://github.com/Azure/Azure-Sentinel/issues/9463">Issue #9463</a> documented that Microsoft&#8217;s own built-in Sentinel detection rules did not account for the multi-record-per-login behavior in the <code>SigninLogs</code> table, leading to false positives across the Entra ID analytics rule set.</p></li><li><p><strong>microsoftgraph/msgraph-metadata</strong> &#8212; The <a href="https://github.com/microsoftgraph/msgraph-metadata">OpenAPI specification repository</a> for Microsoft Graph provided the API path counts and commit history referenced in Problem 4.</p></li></ul><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://lydiagraslie.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://lydiagraslie.substack.com/subscribe?"><span>Subscribe now</span></a></p><h2></h2><div><hr></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://lydiagraslie.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Control Plane! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item></channel></rss>