<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alan Conlon</title>
    <description>The latest articles on DEV Community by Alan Conlon (@alanconlon).</description>
    <link>https://dev.to/alanconlon</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4159913%2F856229d7-8812-4968-adcc-41f79dfb0595.png</url>
      <title>DEV Community: Alan Conlon</title>
      <link>https://dev.to/alanconlon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alanconlon"/>
    <language>en</language>
    <item>
      <title>Hunting MFA Fatigue in Sentinel: Building the Detection One Line at a Time</title>
      <dc:creator>Alan Conlon</dc:creator>
      <pubDate>Tue, 25 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/alanconlon/hunting-mfa-fatigue-in-sentinel-building-the-detection-one-line-at-a-time-2cg2</link>
      <guid>https://dev.to/alanconlon/hunting-mfa-fatigue-in-sentinel-building-the-detection-one-line-at-a-time-2cg2</guid>
      <description>&lt;p&gt;MFA fatigue is the attack that works because people are tired. The attacker already has the password, usually from a phishing kit or a credential dump, and MFA is the only thing left in the way. So they hammer it. Push notification after push notification, sometimes at two in the morning, until the person on the other end approves one just to make their phone shut up. That single tap is the whole breach. It’s how some of the most publicised compromises of the last few years started, and there’s nothing exotic about it. It’s a denial-of-patience attack.&lt;/p&gt;

&lt;p&gt;The good news is that it’s loud. An attacker bombing someone with MFA prompts leaves a very recognisable shape in your sign-in logs, and you can find it with the same handful of KQL operators from the primer. This post builds that detection from nothing, one line at a time. If you haven’t read &lt;a href="https://alanconlon.com/posts/sentinel-kql-primer/" rel="noopener noreferrer"&gt;the primer&lt;/a&gt;, it’s the place to start; this one assumes you know what a pipe does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the attack looks like in the data
&lt;/h2&gt;

&lt;p&gt;Every MFA prompt that gets denied or times out lands in &lt;code&gt;SigninLogs&lt;/code&gt; with a result code. The one that matters here is &lt;code&gt;500121&lt;/code&gt;, which is Entra’s code for an MFA challenge that failed: the user declined it, ignored it, or it timed out. One of those on its own is nothing. Someone fat-fingered a prompt or left their phone in the kitchen.&lt;/p&gt;

&lt;p&gt;The attack shape is different: a burst of them, against one account, in a short window. Ten denied prompts in fifteen minutes isn’t someone making tea. That clustering is the entire detection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start by looking, not detecting
&lt;/h2&gt;

&lt;p&gt;Same discipline as always. Before writing anything clever, look at what the failures actually look like in your own tenant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SigninLogs
| where TimeGenerated &amp;gt; ago(7d)
| where ResultType == "500121"
| take 20

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that and read the rows. You’ll see who’s failing MFA in an ordinary week, which apps trigger it, and roughly how often it happens when nothing is wrong. That baseline matters, because the difference between noise and an attack is volume against time, and you can’t judge volume until you’ve seen normal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now add the shape
&lt;/h2&gt;

&lt;p&gt;The attack is a cluster, so we count failures per user inside small time windows. &lt;code&gt;bin()&lt;/code&gt; chops time into buckets, and &lt;code&gt;summarize&lt;/code&gt; counts inside them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SigninLogs
| where TimeGenerated &amp;gt; ago(1d)
| where ResultType == "500121"
| summarize FailedPrompts = count() by UserPrincipalName, bin(TimeGenerated, 15m)
| where FailedPrompts &amp;gt;= 8
| sort by FailedPrompts desc

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it as a sentence. Take the last day of sign-ins, keep only failed MFA challenges, count them per user per fifteen-minute window, and show me anyone with eight or more in a single window. That’s the whole detection, and it’s five lines.&lt;/p&gt;

&lt;p&gt;The threshold is yours to tune. Eight in fifteen minutes is a reasonable starting point, but your baseline from the first query is the real guide. Set it low enough to catch a determined attacker, high enough that the person with a flaky Authenticator setup doesn’t page you every morning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The refinement that makes it serious
&lt;/h2&gt;

&lt;p&gt;Here’s the version worth promoting to an analytics rule, and the thinking behind it. A burst of denied prompts is suspicious. A burst of denied prompts followed by a success is the actual disaster, because it means the bombing worked. Someone got tired and tapped approve.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let Failures = SigninLogs
    | where TimeGenerated &amp;gt; ago(1d)
    | where ResultType == "500121"
    | summarize FailedPrompts = count(), LastFail = max(TimeGenerated)
        by UserPrincipalName
    | where FailedPrompts &amp;gt;= 8;
SigninLogs
| where TimeGenerated &amp;gt; ago(1d)
| where ResultType == "0"
| join kind=inner Failures on UserPrincipalName
| where TimeGenerated &amp;gt; LastFail
| project UserPrincipalName, FailedPrompts, LastFail,
    SuccessAt = TimeGenerated, IPAddress, Location, AppDisplayName

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two new ideas, both small. &lt;code&gt;let&lt;/code&gt; names a query so you can reuse it, here the set of accounts that got bombed. Then &lt;code&gt;join&lt;/code&gt; matches those accounts against successful sign-ins (&lt;code&gt;ResultType == "0"&lt;/code&gt;) that happened after the last failure. What comes out is the list you actually care about: accounts that were hammered and then let someone in, with the IP, location and app of the sign-in that got through.&lt;/p&gt;

&lt;p&gt;If that query ever returns a row, it’s not a hunt anymore. It’s an incident, and that account needs its sessions revoked and its credentials reset before you finish your coffee.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it misses, because every detection misses something
&lt;/h2&gt;

&lt;p&gt;Honesty about the gaps is what separates a detection you trust from one you hope about. This one won’t see an attacker who paces themselves below your threshold, and it won’t catch number-matching bypasses or token theft, which don’t generate denied prompts at all. It also assumes push MFA; if you’ve already moved to phishing-resistant methods for privileged accounts, whole classes of this attack stop applying to those users, which is rather the point of moving.&lt;/p&gt;

&lt;p&gt;None of that makes the query less worth running. It means this is one detection, not a strategy. The strategy is number matching turned on, phishing-resistant MFA for the accounts that matter most, and this query watching for the accounts still on push.&lt;/p&gt;

&lt;h2&gt;
  
  
  Promote it slowly, same as always
&lt;/h2&gt;

&lt;p&gt;Run it in the logs for a week or two first. Watch what it catches, tune the threshold and the window to your tenant, and only then wire it into a scheduled analytics rule with an automation that flags the account. The rule you understand at 2am is the rule that was built slowly at 2pm. That was true in &lt;a href="https://alanconlon.com/posts/sentinel-kql-primer/" rel="noopener noreferrer"&gt;the primer&lt;/a&gt; and it doesn’t stop being true when the attack gets a scarier name.&lt;/p&gt;

&lt;p&gt;The wider lesson is the one worth keeping. Good detections are rarely clever. This one is a count, a threshold and a join, and it catches an attack that has embarrassed some very large organisations. The gap was never the difficulty of the query. It was that nobody had opened the logs and asked.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://alanconlon.com/posts/hunting-mfa-fatigue-kql/" rel="noopener noreferrer"&gt;alanconlon.com&lt;/a&gt;, where I post practical security, Azure and data write-ups every other Tuesday.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kql</category>
      <category>sentinel</category>
      <category>security</category>
      <category>azure</category>
    </item>
    <item>
      <title>Setting Up Microsoft Security Copilot: Capacity, Roles, and the Defaults That Bite</title>
      <dc:creator>Alan Conlon</dc:creator>
      <pubDate>Tue, 28 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/alanconlon/setting-up-microsoft-security-copilot-capacity-roles-and-the-defaults-that-bite-38b5</link>
      <guid>https://dev.to/alanconlon/setting-up-microsoft-security-copilot-capacity-roles-and-the-defaults-that-bite-38b5</guid>
      <description>&lt;p&gt;Before you do anything with Security Copilot, check whether it’s already running. If your organisation is on Microsoft 365 E5 or E7, Microsoft may well have provisioned it for you, quietly, as part of the licence. It turns up with a monthly allocation of compute and a set of default roles already attached, and most people don’t notice until someone opens the portal and starts poking around. That “it’s already on” moment is exactly when the setup matters most, because the defaults are not what you’d choose if you’d sat down and thought about it.&lt;/p&gt;

&lt;p&gt;I’ll leave the incident-investigation side for another day. The bit nobody walks you through is the enablement and the permissions, and that’s the bit that actually decides whether this thing is safe to have switched on.&lt;/p&gt;

&lt;h2&gt;
  
  
  It might already be provisioned
&lt;/h2&gt;

&lt;p&gt;The licensing splits into two camps, and which one you’re in changes everything about setup.&lt;/p&gt;

&lt;p&gt;If you’re on Microsoft 365 E5 or E7, Security Copilot is included and auto-provisioned. You get an allocation of Security Compute Units, the compute that powers it, scaled to your licence count: 400 units a month for every 1,000 paid user licences, up to a ceiling of 10,000. That allocation resets every month and doesn’t roll over, so unused capacity just evaporates. Nothing to buy, nothing to stand up. It’s on.&lt;/p&gt;

&lt;p&gt;If you’re not on E5 or E7, none of that applies. You provision the compute yourself, which means an Azure subscription and someone with the right roles to create the capacity. Different starting line entirely, so confirm your licence position before you follow any guide, including this one. Half the confusion online comes from people following E5 steps without an E5 licence, or the reverse.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you’re actually paying for: compute units
&lt;/h2&gt;

&lt;p&gt;Security Copilot runs on Security Compute Units, or SCUs, and they come in two flavours. Provisioned capacity is always-on. You decide how many units to keep warm, and you pay for them by the hour whether Copilot does a single thing or sits idle all weekend. Overage is the opposite: on-demand units that kick in when you exhaust the provisioned ones, billed only when used.&lt;/p&gt;

&lt;p&gt;The gotcha is the provisioned side. It’s billed hourly on what you’ve reserved, not on what you consume, so an oversized always-on allocation is money quietly leaving the building. If you’re just standing it up to have a look, Microsoft’s own recommendation is modest: three provisioned units with overage set to unlimited. Worth knowing that the standalone portal will even let you set provisioned to zero and run entirely on overage, which is effectively pay-as-you-go and a sensible way to trial it without committing to warm capacity you might not touch.&lt;/p&gt;

&lt;p&gt;Provisioning the capacity needs more than a credit card. You’ll want Azure Contributor or Owner on the subscription or resource group, plus Security Administrator or higher in the tenant. The usage dashboard keeps ninety days of history, and it’s where you go to see what’s actually being consumed before you decide how much to reserve. Look at real usage first, size the capacity second.&lt;/p&gt;

&lt;h2&gt;
  
  
  The default depends on when you deployed
&lt;/h2&gt;

&lt;p&gt;Copilot Contributor isn’t a Microsoft Entra role. Security Copilot has its own two platform roles, Owner and Contributor, that live inside Copilot and only govern access to the platform itself. Who gets Contributor out of the box is the part that has changed, and it’s worth knowing which era your tenant belongs to.&lt;/p&gt;

&lt;p&gt;New instances now default to the Recommended Microsoft Security roles group: a bundle that grants Contributor access only to users who already hold security-relevant Entra roles. That’s a sensible middle ground, and if you’re standing Copilot up fresh, it’s what you’ll get.&lt;/p&gt;

&lt;p&gt;Older instances are a different story. Earlier deployments defaulted to granting Copilot Contributor to every user in the tenant through the built-in Everyone group, and existing customers who have that assignment keep it until someone removes it. So the day-one check is simple: open Role assignment and look at what’s actually there. If the Everyone group is still assigned, remove it and replace it with either the recommended roles bundle or a scoped group of the people who should genuinely have access. One wrinkle to know before you act: once the Everyone group is removed, it can’t be assigned again, so this is a one-way door (a good one, but a door).&lt;/p&gt;

&lt;p&gt;If you do use your own groups, note that Security Copilot only supports role-assignable groups, and assign roles to groups rather than individuals. It’s less to manage and far easier to review six months later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Copilot inherits permissions, it doesn’t grant them
&lt;/h2&gt;

&lt;p&gt;This is the part that reassures people once they understand it, and trips them up before they do. Security Copilot uses on-behalf-of authentication, which means it can never see more than the signed-in user can already see. It has no privileges of its own. It borrows yours.&lt;/p&gt;

&lt;p&gt;So Contributor access gets someone onto the platform, but it gets them no data. To actually pull Sentinel incidents, the user still needs a Sentinel role like Microsoft Sentinel Reader. To see devices and policies through the Intune connection, they need an Intune role. Defender is the same. The real scoping of what Copilot can touch happens in the underlying products, through their normal RBAC, not in Copilot’s own settings. That’s a good thing. It means your existing least-privilege model carries straight through.&lt;/p&gt;

&lt;p&gt;It also means you should resist a tempting shortcut. Granting someone Security Administrator hands them Copilot Owner access automatically, but it also gives them a pile of tenant-wide security permissions that have nothing to do with Copilot. Microsoft says this plainly, and they’re right: don’t hand out a privileged role just to unlock Copilot. Put the person in the scoped group instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Owners, and why PIM belongs here
&lt;/h2&gt;

&lt;p&gt;A handful of Entra and Purview roles inherit Copilot Owner automatically: Global Administrator and Security Administrator as you’d expect, but also Billing Administrator, Intune Administrator and Compliance Administrator. A Billing Admin silently holding Copilot Owner is exactly the kind of default worth knowing about. Owner can manage capacity, plugins, workspaces and roles, so it’s worth knowing exactly who holds it by inheritance rather than by design.&lt;/p&gt;

&lt;p&gt;For those high-privilege roles, this is textbook Privileged Identity Management territory. Make them eligible rather than permanently assigned, require activation, and you shrink the standing pool of people who can reconfigure the platform down to nobody until somebody actually needs it. While you’re in the settings, turn audit logging on. It’s a Security Administrator action, it applies across all workspaces, and you’ll want it long before you think you need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workspaces and a sensible rollout
&lt;/h2&gt;

&lt;p&gt;If you’re running more than one team or region, workspaces let you carve capacity up and right-size it per team, each backed by its own compute. If you provisioned Copilot before workspaces existed, one was created for you in the background, so it’s already there whether you’ve looked at it or not.&lt;/p&gt;

&lt;p&gt;The rollout that works is the unexciting one. Start with a small provisioned allocation or none at all, a tightly scoped access group, owners locked behind PIM, audit logging on, and the underlying product RBAC doing the real gatekeeping. Watch the usage dashboard for a couple of weeks. Then, and only then, widen access and tune the capacity to what you’ve actually seen.&lt;/p&gt;

&lt;p&gt;The interesting work, the investigations and the promptbooks, comes after all this. But none of it is safe or sensible until the boring part is done properly. Get the capacity sized, get the Everyone group out of Contributor, and let your existing permissions model do its job. That’s the whole setup, and it’s almost entirely things you already know how to do.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://alanconlon.com/posts/security-copilot-setup/" rel="noopener noreferrer"&gt;alanconlon.com&lt;/a&gt;, where I post practical security, Azure and data write-ups every other Tuesday.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>security</category>
      <category>ai</category>
      <category>microsoft</category>
    </item>
    <item>
      <title>Getting Started with Sentinel Hunting: A KQL Primer for People Who Keep Putting It Off</title>
      <dc:creator>Alan Conlon</dc:creator>
      <pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/alanconlon/getting-started-with-sentinel-hunting-a-kql-primer-for-people-who-keep-putting-it-off-4jo5</link>
      <guid>https://dev.to/alanconlon/getting-started-with-sentinel-hunting-a-kql-primer-for-people-who-keep-putting-it-off-4jo5</guid>
      <description>&lt;p&gt;Most people who run Sentinel don’t really hunt in it. They wire up the connectors, switch on a pile of built-in analytics rules, and then treat the whole thing as a box that pings them when something’s wrong. Which is fine, until the day you actually need to go looking for something the built-in rules never thought to ask about. That’s when you open the logs, stare at the query bar, and realise you’ve been avoiding KQL for two years.&lt;/p&gt;

&lt;p&gt;I put it off myself for a long time. It looked like another query language to learn on top of everything else, and the examples online were either trivially basic or written by someone showing off. The truth is somewhere in between, and the useful middle is much smaller than it looks. You can get genuinely productive with about five operators. Everything else you pick up when you need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start by just looking at one table
&lt;/h2&gt;

&lt;p&gt;The mistake most people make is reaching for a detection before they’ve ever looked at the raw data. Don’t. Open the logs and run the most boring query there is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SigninLogs
| take 10

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it. &lt;code&gt;take 10&lt;/code&gt; grabs ten rows so you can see what’s actually in the table, the column names, what the values look like and where the useful stuff lives. Sign-in logs are huge and nested, and you’ll save yourself a lot of grief by seeing the shape of them before you try to be clever. Do the same with whatever table you care about. Look first, query second.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipe is the whole idea
&lt;/h2&gt;

&lt;p&gt;KQL reads top to bottom, and every line starts with a pipe. Each step takes the table that came out of the line above and does one thing to it. That’s the entire mental model. You’re passing a table down a chain and narrowing or reshaping it as you go.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SigninLogs
| where TimeGenerated &amp;gt; ago(24h)
| where ResultType != "0"
| take 20

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it as a sentence. Take the sign-in logs, keep the last 24 hours, keep only the failures, show me twenty. &lt;code&gt;ResultType != "0"&lt;/code&gt; is the failures, because zero means success. The quotes matter too, the column holds text even though the values look like numbers. One of those things nobody tells you and everybody just has to learn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filter early, always
&lt;/h2&gt;

&lt;p&gt;Here’s the one habit worth building from day one: cut the data down before you do anything expensive with it. Put your &lt;code&gt;where&lt;/code&gt; clauses high up, especially the time filter. Sentinel charges you for what you scan and makes you wait for it, so a query that filters to the last hour before it starts counting things will be faster and cheaper than one that counts everything and trims afterwards.&lt;/p&gt;

&lt;p&gt;It’s the difference between this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SigninLogs
| where TimeGenerated &amp;gt; ago(1h)
| summarize count() by UserPrincipalName

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the version that summarises the whole table first and then filters. Same answer. Wildly different cost. Get the time bound in early and it becomes second nature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summarize is where it gets useful
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;where&lt;/code&gt; finds rows. &lt;code&gt;summarize&lt;/code&gt; answers questions. The moment you want to know how many or how often rather than just show me, you’re reaching for summarize.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;SigninLogs&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;TimeGenerated&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ago&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;ResultType&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nv"&gt;"0"&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;summarize&lt;/span&gt; &lt;span class="n"&gt;FailedAttempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;UserPrincipalName&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;FailedAttempts&lt;/span&gt; &lt;span class="k"&gt;desc&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That counts failed sign-ins per user over the last day and puts the worst offenders at the top. It’s about four lines and it’s already more useful than half the dashboards I’ve seen. You can see where this goes: repeated failures clustered on one account in a short window is exactly the shape of someone being attacked, and you found it with five operators and no special tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don’t trust a detection you can’t read
&lt;/h2&gt;

&lt;p&gt;This is the part that matters more than the syntax. There’s a temptation, once you find a clever query online, to paste it into an analytics rule and move on. Resist it. A detection you don’t understand is worse than no detection, because it gives you confidence you haven’t earned. When it fires at 2am you need to know what it actually checked, and when it stays silent you need to know whether that means you’re safe or whether the query was quietly broken the whole time.&lt;/p&gt;

&lt;p&gt;So build them slowly. Get the query returning sensible rows in the logs first. Look at what it catches and what it misses. Only then promote it to a scheduled rule. The boring, incremental version is the one that works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Five operators get you a long way: &lt;code&gt;take&lt;/code&gt;, &lt;code&gt;where&lt;/code&gt;, &lt;code&gt;summarize&lt;/code&gt;, &lt;code&gt;sort&lt;/code&gt;, and &lt;code&gt;project&lt;/code&gt; (which trims the columns down to the ones you care about). Build a few queries by hand against the tables you already have: failed sign-ins, sign-ins from somewhere you don’t operate, accounts that suddenly got handed an admin role. The detections that catch real attacks are rarely exotic. They’re usually just someone who took the time to look.&lt;/p&gt;

&lt;p&gt;That’s the whole secret, honestly. The people who get value out of Sentinel aren’t the ones who memorised the language. They’re the ones who actually opened the logs and started asking questions.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://alanconlon.com/posts/sentinel-kql-primer/" rel="noopener noreferrer"&gt;alanconlon.com&lt;/a&gt;, where I post practical security, Azure and data write-ups every other Tuesday.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kql</category>
      <category>sentinel</category>
      <category>security</category>
      <category>azure</category>
    </item>
  </channel>
</rss>
