<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator>
  <link href="https://rksolutions.nl/feed.xml" rel="self" type="application/atom+xml" />
  <link href="https://rksolutions.nl/" rel="alternate" type="text/html" />
  <updated>2026-06-10T07:27:37+00:00</updated>
  <id>https://rksolutions.nl/feed.xml</id>
  <icon>https://rksolutions.nl/favicon.png</icon>
  <logo>https://rksolutions.nl/favicon.png</logo>

  
    <title type="html">RK Solutions</title>
  

  
    <subtitle>Modern Workplace &amp; Automation — Roy Klooster</subtitle>
  

  
    <author>
        <name>Roy Klooster</name>
    </author>
  

  
  
  
  
    <entry>
      

      <title type="html">MacPPPC: Build and Deploy macOS PPPC Profiles to Intune from Your Browser</title>
      <link href="https://rksolutions.nl/posts/macpppc-deploy-macos-pppc-profiles-to-intune/" rel="alternate" type="text/html" title="MacPPPC: Build and Deploy macOS PPPC Profiles to Intune from Your Browser" />
      <published>2026-06-06T00:00:00+00:00</published>
      <updated>2026-06-06T00:00:00+00:00</updated>
      <id>https://rksolutions.nl/posts/macpppc-deploy-macos-pppc-profiles-to-intune</id>
      <content type="html" xml:base="https://rksolutions.nl/posts/macpppc-deploy-macos-pppc-profiles-to-intune/"><![CDATA[<p>Every Mac admin knows the scene. A user is on a call, tries to share their screen, and hits a permission prompt. They call you to fix it, restart the app, and rejoin late. Or they shrug it off, finish the call without sharing, and ping you afterwards to sort it out. Either way the work didn’t happen on time - and that screen-recording prompt is just one of countless macOS permissions that behave the same way. Intune can’t push most of them directly. Every app that isn’t natively supported has to be configured by hand, either as a <code class="language-plaintext highlighter-rouge">.mobileconfig</code> custom profile or as a Settings Catalog policy, and both mean wrestling with raw XML or JSON that you build yourself.</p>

<p>The fix for all of this is a PPPC profile, but Intune gives you no UI for building one. You either copy XML from a blog post and pray, or you reach for Jamf. Neither is great.</p>

<p>That’s the gap <strong>MacPPPC</strong> closes. It’s a free, fully browser-based tool that builds the <code class="language-plaintext highlighter-rouge">.mobileconfig</code> for you and deploys it directly to Intune over Microsoft Graph - using your own Entra ID sign-in. No backend, no Jamf, no manual XML. It’s live at <a href="https://macpppc.com">macpppc.com</a>.</p>

<p><strong>Table of Contents</strong></p>

<ul>
  <li><a href="#what-is-macpppc">What is MacPPPC?</a></li>
  <li><a href="#a-quick-refresher-on-pppc-and-tcc">A quick refresher on PPPC and TCC</a></li>
  <li><a href="#requirements">Requirements</a></li>
  <li><a href="#how-it-works">How it works</a>
    <ul>
      <li><a href="#step-1-build-the-profile">Step 1: Build the profile</a></li>
      <li><a href="#step-2-deploy-to-intune">Step 2: Deploy to Intune</a></li>
      <li><a href="#or-just-download-it">Or just download it</a></li>
    </ul>
  </li>
  <li><a href="#the-apple-rules-it-enforces-for-you">The Apple rules it enforces for you</a></li>
  <li><a href="#security-and-privacy">Security and privacy</a></li>
  <li><a href="#required-entra-permissions">Required Entra permissions</a></li>
  <li><a href="#conclusion">Conclusion</a></li>
</ul>

<h2 id="what-is-macpppc">What is MacPPPC?</h2>

<p>MacPPPC is a single-page web app that does two things. First, it builds a macOS Privacy Preferences Policy Control profile that pre-approves an app’s access to protected resources so your users never see the prompt. Second, it pushes that profile straight into Intune - either as a classic <code class="language-plaintext highlighter-rouge">macOSCustomConfiguration</code> custom profile or as a modern Settings Catalog policy - complete with scope tags, deployment channel, and assignments.</p>

<p>The whole thing runs client-side in React. Profiles are generated in your browser, and the Intune deployment happens directly between your browser and <code class="language-plaintext highlighter-rouge">graph.microsoft.com</code>. There is no server in the middle, no telemetry, and nothing to install.</p>

<p><strong>What it gives you:</strong></p>

<ul>
  <li>A point-and-click builder for all <strong>24 PPPC services Apple exposes to MDM</strong> - Accessibility, Full Disk Access (and the granular Desktop/Documents/Downloads/Network/Removable/Admin folder variants), Camera, Microphone, Screen Recording, Input Monitoring, Bluetooth, Contacts, Calendars, Reminders, Photos, Media Library, Speech Recognition, Apple Events automation with receiver-app pairing, and more.</li>
  <li><strong>87 prebuilt apps</strong>, each with its designated code requirement already verified - everything from 1Password, Adobe Illustrator, and Zoom to AnyDesk, Arc, and BeyondTrust. The common ones are a single click; for anything else, drop in an <code class="language-plaintext highlighter-rouge">Info.plist</code> or a zipped <code class="language-plaintext highlighter-rouge">.app</code> bundle and it reads the designated requirement for you.</li>
  <li><strong>Direct Intune deployment</strong> over Graph in either format - a classic custom profile or a Settings Catalog policy - with scope tags, device or user channel, and full assignment control (none, all users, all devices, or groups with include/exclude and per-group assignment filters).</li>
  <li>A <strong>live preview</strong> of exactly what’s being generated - XML for a custom profile, or JSON when you target the Settings Catalog - before anything leaves your browser.</li>
  <li><strong>No Client Secret.</strong> It uses the SPA + PKCE OAuth flow, so only your Entra app’s public Client ID is ever involved.</li>
</ul>

<h2 id="a-quick-refresher-on-pppc-and-tcc">A quick refresher on PPPC and TCC</h2>

<p>If you live mostly in the Windows side of M365, this is the bit worth slowing down for. macOS protects sensitive resources - the camera, the microphone, your files, screen contents - behind a subsystem called <strong>TCC</strong> (Transparency, Consent, and Control). When an app first touches one of those resources, macOS asks the user to allow or deny it.</p>

<p>That’s fine for one Mac. Across a fleet it’s a support nightmare: users deny by accident, apps break, and there’s no consistency. <strong>PPPC</strong> is Apple’s MDM answer. A PPPC profile tells the Mac, ahead of time, “this specific app, identified by its code signature, is allowed (or denied) this specific resource.” The prompt never appears, and the decision is yours, not the user’s.</p>

<p>The catch is that the profile has to be exactly right. Each app is pinned to a <strong>designated code requirement</strong> - a signature string that proves the binary is the genuine, untampered app. Each permission maps to an exact Apple TCC service name like <code class="language-plaintext highlighter-rouge">SystemPolicyAllFiles</code> or <code class="language-plaintext highlighter-rouge">ScreenCapture</code>. One typo and the profile silently does nothing. Now multiply that by every app you need to cover and every Mac in your fleet - by hand it’s hours of fiddly, error-prone work, repeated per app. MacPPPC fills in the code requirements and service names for you, and lets you configure every app and permission in one bulk pass instead of one painful profile at a time.</p>

<h2 id="requirements">Requirements</h2>

<p>Here’s the best part: nothing. You just open it in a modern browser and it’s working. To deploy straight to Intune you only need:</p>

<ul>
  <li>An <strong>Entra ID account</strong> with rights to create device configuration profiles in Intune (Intune Administrator, or a custom role with the scopes below)</li>
  <li>Admin consent for the delegated Graph scopes the first time it’s used in your tenant</li>
</ul>

<p>And if you just want the profile to upload by hand, you don’t even need to sign in - build it, download it, done.</p>

<h2 id="how-it-works">How it works</h2>

<p>The app is one page in two steps.</p>

<div class="post-gallery">
  <div class="gallery-track">
    <img src="/assets/images/2026-06/macpppc-build-profiles.png" alt="MacPPPC build screen: Adobe Illustrator selected with its verified code requirement, the Settings Catalog deployment format chosen, the 24 permission toggles, and the live profile preview in the right rail" />
    <img src="/assets/images/2026-06/macpppc-deploy-intune.png" alt="MacPPPC deploy screen connected to an Intune tenant: policy name, scope tags, device-or-user channel, assignment options, and the validated .mobileconfig preview with syntax highlighting" />
    <img src="/assets/images/2026-06/macpppc-group-assignment.png" alt="MacPPPC group assignment panel: include/exclude security groups with live search across the tenant's Entra groups and an optional assignment filter" />
  </div>
  <button class="gallery-nav gallery-prev" aria-label="Previous screenshot">&#8249;</button>
  <button class="gallery-nav gallery-next" aria-label="Next screenshot">&#8250;</button>
  <div class="gallery-dots">
    <button class="gallery-dot active" data-idx="0" aria-label="Build profiles"></button>
    <button class="gallery-dot" data-idx="1" aria-label="Deploy to Intune"></button>
    <button class="gallery-dot" data-idx="2" aria-label="Group assignment"></button>
  </div>
</div>

<h3 id="step-1-build-the-profile">Step 1: Build the profile</h3>

<ol>
  <li><strong>Add your apps.</strong> Drag-and-drop a <code class="language-plaintext highlighter-rouge">.zip</code> of an <code class="language-plaintext highlighter-rouge">.app</code> bundle, drop an <code class="language-plaintext highlighter-rouge">Info.plist</code> directly, or pick from the catalog of 87 prebuilt apps. Uploaded apps are matched against the catalog by their <code class="language-plaintext highlighter-rouge">CFBundleIdentifier</code>, and if there’s a match the canonical code requirement is filled in automatically.</li>
  <li><strong>Toggle the permissions</strong> you want per app. The authorization dropdown (Allow / Allow standard user / Deny) is constrained automatically by Apple’s rules - more on that below.</li>
  <li><strong>Pick the deployment format.</strong> Choose a classic custom profile (<code class="language-plaintext highlighter-rouge">.mobileconfig</code>) or a <strong>Settings Catalog policy (recommended)</strong>. The Settings Catalog is the modern path Microsoft is steering everyone toward - it’s understood natively by Intune, re-importable, and cleaner to manage than a raw custom-config blob.</li>
  <li><strong>Pick an output mode.</strong> <em>Single bundle</em> puts every app into one profile (the default), or <em>Separate profiles</em> generates one per app, packaged as a ZIP and deployed as separate Intune policies.</li>
  <li><strong>Watch the live preview.</strong> The right rail shows the generated payload with syntax highlighting - XML for a custom profile, or JSON if you’re targeting the Settings Catalog - so you can verify it before you deploy.</li>
</ol>

<p>Here’s a trimmed example of a single-app payload as a <strong>classic custom profile</strong> (<code class="language-plaintext highlighter-rouge">.mobileconfig</code>) - one <code class="language-plaintext highlighter-rouge">com.apple.TCC.configuration-profile-policy</code> payload with a <code class="language-plaintext highlighter-rouge">Services</code> dictionary keyed by Apple’s exact TCC service names:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;key&gt;</span>Services<span class="nt">&lt;/key&gt;</span>
<span class="nt">&lt;dict&gt;</span>
  <span class="nt">&lt;key&gt;</span>ScreenCapture<span class="nt">&lt;/key&gt;</span>
  <span class="nt">&lt;array&gt;</span>
    <span class="nt">&lt;dict&gt;</span>
      <span class="nt">&lt;key&gt;</span>Identifier<span class="nt">&lt;/key&gt;</span>
      <span class="nt">&lt;string&gt;</span>us.zoom.xos<span class="nt">&lt;/string&gt;</span>
      <span class="nt">&lt;key&gt;</span>IdentifierType<span class="nt">&lt;/key&gt;</span>
      <span class="nt">&lt;string&gt;</span>bundleID<span class="nt">&lt;/string&gt;</span>
      <span class="nt">&lt;key&gt;</span>CodeRequirement<span class="nt">&lt;/key&gt;</span>
      <span class="nt">&lt;string&gt;</span>identifier "us.zoom.xos" and anchor apple generic ...<span class="nt">&lt;/string&gt;</span>
      <span class="nt">&lt;key&gt;</span>Authorization<span class="nt">&lt;/key&gt;</span>
      <span class="nt">&lt;string&gt;</span>AllowStandardUserToSetSystemService<span class="nt">&lt;/string&gt;</span>
    <span class="nt">&lt;/dict&gt;</span>
  <span class="nt">&lt;/array&gt;</span>
<span class="nt">&lt;/dict&gt;</span>
</code></pre></div></div>

<p>The exact same intent, expressed as a <strong>Settings Catalog</strong> policy, is JSON instead - posted to <code class="language-plaintext highlighter-rouge">/beta/deviceManagement/configurationPolicies</code>. It’s more verbose because every setting is a typed Graph instance, but it’s what Intune understands natively and what you can re-import later (always-null template fields trimmed here for readability):</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PPPC Configuration"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"platforms"</span><span class="p">:</span><span class="w"> </span><span class="s2">"macOS"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"technologies"</span><span class="p">:</span><span class="w"> </span><span class="s2">"mdm,appleRemoteManagement"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"roleScopeTagIds"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"0"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"settingCount"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
  </span><span class="nl">"settings"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"settingInstance"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"@odata.type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"#microsoft.graph.deviceManagementConfigurationGroupSettingCollectionInstance"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"settingDefinitionId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"com.apple.tcc.configuration-profile-policy_services_screencapture"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"groupSettingCollectionValue"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
          </span><span class="p">{</span><span class="w">
            </span><span class="nl">"children"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
              </span><span class="p">{</span><span class="w">
                </span><span class="nl">"@odata.type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"settingDefinitionId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"com.apple.tcc.configuration-profile-policy_services_screencapture_item_authorization"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"choiceSettingValue"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                  </span><span class="nl">"value"</span><span class="p">:</span><span class="w"> </span><span class="s2">"com.apple.tcc.configuration-profile-policy_services_screencapture_item_authorization_2"</span><span class="p">,</span><span class="w">
                  </span><span class="nl">"children"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
                </span><span class="p">}</span><span class="w">
              </span><span class="p">},</span><span class="w">
              </span><span class="p">{</span><span class="w">
                </span><span class="nl">"@odata.type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"settingDefinitionId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"com.apple.tcc.configuration-profile-policy_services_screencapture_item_identifier"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"simpleSettingValue"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                  </span><span class="nl">"@odata.type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"#microsoft.graph.deviceManagementConfigurationStringSettingValue"</span><span class="p">,</span><span class="w">
                  </span><span class="nl">"value"</span><span class="p">:</span><span class="w"> </span><span class="s2">"us.zoom.xos"</span><span class="w">
                </span><span class="p">}</span><span class="w">
              </span><span class="p">}</span><span class="w">
            </span><span class="p">]</span><span class="w">
          </span><span class="p">}</span><span class="w">
        </span><span class="p">]</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>That choice value ending in <code class="language-plaintext highlighter-rouge">_2</code> is the Settings Catalog way of saying <em>Allow standard user to enable</em> - the exact same authorization the XML expressed as <code class="language-plaintext highlighter-rouge">AllowStandardUserToSetSystemService</code>. MacPPPC generates whichever one you pick; you never hand-write either.</p>

<h3 id="step-2-deploy-to-intune">Step 2: Deploy to Intune</h3>

<ol>
  <li><strong>Sign in</strong> with Entra ID using the button in the top-right. MSAL handles the redirect and your tokens stay in your browser.</li>
  <li><strong>Name each policy.</strong> In bundle mode that’s one row. In separate mode it’s one row per app, plus an optional bulk-rename pattern using <code class="language-plaintext highlighter-rouge">{appName}</code> and <code class="language-plaintext highlighter-rouge">{bundleId}</code> placeholders.</li>
  <li><strong>Pick scope tags</strong> (multi-select, defaults to <em>Default</em>) and the <strong>deployment channel</strong> - device or user - per policy.</li>
  <li><strong>Configure the assignment</strong> per policy: none, all users, all devices, or specific groups with include/exclude. Each include group can carry its own assignment filter, and <em>Copy to all</em> mirrors one policy’s assignment onto every other.</li>
  <li><strong>Click Deploy.</strong> Each policy is created via Graph - a <code class="language-plaintext highlighter-rouge">macOSCustomConfiguration</code> under <code class="language-plaintext highlighter-rouge">/beta/deviceManagement/deviceConfigurations</code> for a classic profile, or a Settings Catalog policy under <code class="language-plaintext highlighter-rouge">/beta/deviceManagement/configurationPolicies</code> - then assigned via the matching <code class="language-plaintext highlighter-rouge">.../assign</code> call. Results appear inline with deep links straight to the Intune portal.</li>
</ol>

<h3 id="or-just-download-it">Or just download it</h3>

<p>Not ready to give a browser tool access to your tenant? Skip the sign-in entirely. The Download button hands you the same output (a <code class="language-plaintext highlighter-rouge">.mobileconfig</code> for a custom profile, or a Settings Catalog JSON you can pull in through Intune’s <em>Import policy</em> button), ready to upload by hand. You get the generator without ever connecting Graph.</p>

<h2 id="the-apple-rules-it-enforces-for-you">The Apple rules it enforces for you</h2>

<p>A few macOS permissions can’t be force-granted from MDM, no matter what you put in the profile. This trips people up constantly, so MacPPPC bakes the rules into the UI instead of letting you ship a profile that won’t work:</p>

<ul>
  <li><strong>Camera and Microphone</strong> honour only <em>Deny</em>. Apple ignores everything else for these - you cannot force-allow them, so the builder only offers Deny.</li>
  <li><strong>Screen Recording and Input Monitoring</strong> honour <em>Deny</em> or <em>Allow standard user to enable</em>. A hard force-Allow isn’t permitted via profile, so that option is removed.</li>
  <li><strong>Everything else</strong> gets the full Allow / Allow-standard-user / Deny dropdown.</li>
</ul>

<p>The generated profiles are intentionally <strong>unsigned</strong> - Intune doesn’t require signing for custom configuration profiles, so there’s nothing extra to manage.</p>

<h2 id="security-and-privacy">Security and privacy</h2>

<p>Because this tool touches your tenant, it’s worth being explicit about what it does and doesn’t do:</p>

<ul>
  <li><strong>100% client-side.</strong> The static bundle runs entirely in your browser. No backend, no analytics on your tenant data, no proxy.</li>
  <li><strong>Graph calls go direct</strong> from your browser to <code class="language-plaintext highlighter-rouge">graph.microsoft.com</code>. Your profile data never passes through any server I run.</li>
  <li><strong>MSAL tokens</strong> live in your browser’s <code class="language-plaintext highlighter-rouge">localStorage</code> so they survive the Microsoft redirect and persist across reloads. Signing out clears them, and they’re never sent anywhere except Microsoft.</li>
  <li><strong>No Client Secret.</strong> Sign-in uses the SPA + PKCE flow against a public Client ID - there’s no secret baked into the build to leak. The app acts purely with your own delegated permissions, scoped to exactly what’s needed: read scope tags and groups, and write device configuration profiles. Nothing beyond what your account can already do is ever exposed.</li>
</ul>

<h2 id="required-entra-permissions">Required Entra permissions</h2>

<p>The app requests these <strong>delegated</strong> scopes on sign-in. All four require admin consent the first time it’s used in a tenant:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">DeviceManagementConfiguration.ReadWrite.All</code> - create and update device configuration profiles</li>
  <li><code class="language-plaintext highlighter-rouge">DeviceManagementRBAC.Read.All</code> - read the scope tag list</li>
  <li><code class="language-plaintext highlighter-rouge">Group.Read.All</code> - search security groups for assignment</li>
  <li><code class="language-plaintext highlighter-rouge">User.Read</code> - show your name in the top bar</li>
</ul>

<p>That’s the minimum needed to build and assign profiles, and nothing more.</p>

<h2 id="conclusion">Conclusion</h2>

<p>MacPPPC turns one of the more tedious corners of macOS management into a few clicks: pick your apps, toggle the permissions, and either download a clean <code class="language-plaintext highlighter-rouge">.mobileconfig</code> or push it straight into Intune over Graph - with full control over scope tags, channel, and assignments. It enforces Apple’s MDM quirks so you don’t ship a profile that silently fails, and it does it all in the browser with no backend touching your data.</p>

<p>If you manage Macs in Intune and you’ve been avoiding PPPC because the XML is fiddly and Jamf is overkill, give it a try at <a href="https://macpppc.com">macpppc.com</a>. It’s free, it’s client-side, and as always - test in a pilot group before you roll it out broadly. Big thanks to <strong>Simon Hartmann Eriksen</strong> for building the original PPPC Builder and for teaming up on MacPPPC.</p>

<h2 id="related">Related</h2>

<ul>
  <li><a href="/posts/rksolutions-powershell-module/">RKSolutions PowerShell Module</a> - Microsoft Graph reporting for Intune, Entra, and M365.</li>
</ul>]]></content>

      
      
      <author>
          <name>Roy Klooster</name>
      </author>

      
        <category term="Intune" />
      
        <category term="Graph API" />
      
        <category term="Tools" />
      
        <category term="Security" />
      

      
      
        <summary type="html"><![CDATA[MacPPPC is a free, client-side web app that builds macOS PPPC privacy profiles and deploys them straight to Microsoft Intune via Graph - no manual XML or JSON, nothing to install.]]></summary>
      
    </entry>
  
    <entry>
      

      <title type="html">The MSP License Ladder #6: The Full Picture</title>
      <link href="https://rksolutions.nl/posts/msp-license-ladder-06-the-full-picture/" rel="alternate" type="text/html" title="The MSP License Ladder #6: The Full Picture" />
      <published>2026-05-23T00:00:00+00:00</published>
      <updated>2026-05-23T00:00:00+00:00</updated>
      <id>https://rksolutions.nl/posts/msp-license-ladder-06-the-full-picture</id>
      <content type="html" xml:base="https://rksolutions.nl/posts/msp-license-ladder-06-the-full-picture/"><![CDATA[<p>Most MSP customers think Business Premium or E3 means “fully covered.” In reality, those licenses leave five operational gaps that only become visible after an incident, an audit, or an escalation that nobody has the tooling to investigate properly.</p>

<p>This final post ties them together into a single reference you can use when scoping a customer, building a service tier, or answering the question every MSP eventually gets: “what are we actually missing?”</p>

<h2 id="the-five-gaps">The five gaps</h2>

<p>Each post in this series started with the same premise: Business Premium and E3 are solid baselines. They cover identity, device management, email protection, and basic compliance. But they stop short in five areas that matter the moment a customer moves past “good enough.”</p>

<h3 id="1---the-hunting-gap">#1 - The Hunting Gap</h3>

<p><strong>What’s missing:</strong> Business Premium and E3 alert on known threats but give you no way to hunt for the ones that haven’t triggered an alert yet. No Advanced Hunting, no KQL access to raw telemetry, no Custom Detection Rules.</p>

<p><strong>What closes it:</strong> Defender for Endpoint Plan 2 - 30 days of queryable telemetry, custom detections that run continuously, and the ability to hunt across your entire customer fleet.</p>

<p><strong>Why it matters:</strong> Reactive alerting catches known patterns. Hunting catches the setup step - the lateral movement, the staging, the persistence mechanism that hasn’t fired yet. <a href="/posts/msp-license-ladder-01-hunting-gap/">Read the full post</a>.</p>

<p>Most compromise starts long before the alert.</p>

<h3 id="2---the-data-gap">#2 - The Data Gap</h3>

<p><strong>What’s missing:</strong> Base Purview covers email and files in SharePoint/OneDrive. It does not cover endpoints, Teams chat, or cloud apps. A departing employee copies sensitive data to a USB drive or personal cloud storage, and base Purview never sees it.</p>

<p><strong>What closes it:</strong> Purview Suite ($10/user/month) - endpoint DLP, Teams DLP, Insider Risk Management, eDiscovery Premium, and event-based retention.</p>

<p><strong>Why it matters:</strong> Data protection that stops at email and documents misses the places data actually leaves - endpoints, messaging, and cloud apps. <a href="/posts/msp-license-ladder-02-data-gap/">Read the full post</a>.</p>

<h3 id="3---the-identity-gap">#3 - The Identity Gap</h3>

<p><strong>What’s missing:</strong> Entra ID P1 covers MFA and Conditional Access. It does not cover risk-based decisions, just-in-time privileged access, automated access reviews, or on-prem Active Directory visibility.</p>

<p><strong>What closes it:</strong> Entra ID P2 adds risk-based CA, PIM, and Access Reviews. Defender for Identity adds on-prem AD threat detection. Both are included in the <a href="https://learn.microsoft.com/microsoft-365/admin/security-and-compliance/add-defender-suite-business-premium">Defender Suite for Business Premium</a> ($10/user/month).</p>

<p><strong>Why it matters:</strong> Standing admin privileges are the single most exploited pattern in identity attacks. PIM removes them. Risk-based CA adapts access decisions to real-time threat signals instead of static rules. Defender for Identity (DfI) sits on your on-prem domain controllers and detects identity attacks against AD itself, including Kerberoasting, DCSync, and lateral movement. Without it, you have full visibility into cloud identity and zero visibility into the on-prem AD that still holds most customers’ privileged accounts. <a href="/posts/msp-license-ladder-03-identity-gap/">Read the full post</a>.</p>

<h3 id="4---the-exposure-gap">#4 - The Exposure Gap</h3>

<p><strong>What’s missing:</strong> Base SKUs prevent threats but cannot measure exposure. No post-delivery email investigation, no automated incident response for email, no visibility into shadow IT or shadow AI, no OAuth app governance.</p>

<p><strong>What closes it:</strong> MDO P2 adds Threat Explorer, AIR, Campaign Views, and Attack Simulation Training. Defender for Cloud Apps adds shadow IT discovery, OAuth governance, session controls, and shadow AI detection. Both are in the Defender Suite.</p>

<p><strong>Why it matters:</strong> Prevention without investigation is half the story. When a phishing campaign lands in 30 mailboxes, you need to scope it in minutes, not hours. When a former MSP leaves behind an OAuth app with Mail.ReadWrite, you need to find it before it is used. <a href="/posts/msp-license-ladder-04-exposure-gap/">Read the full post</a>.</p>

<h3 id="5---the-endpoint-gap">#5 - The Endpoint Gap</h3>

<p><strong>What’s missing:</strong> Intune Plan 1 enrolls and configures devices. It does not give you least-privilege elevation, third-party app patching, device health analytics, or zero-trust remote support.</p>

<p><strong>What closes it:</strong> The <a href="https://learn.microsoft.com/intune/fundamentals/add-ons">Intune Suite</a> ($10/user/month) - EPM, EAM, Advanced Analytics, Remote Help, Cloud PKI, and Intune Plan 2 features. After July 2026, E3 gets Remote Help, Advanced Analytics, and Plan 2 for free. E5 gets everything. Business Premium gets nothing.</p>

<p><strong>Why it matters:</strong> Every 15-minute remote session for an elevation request, every hour spent repackaging a Win32 update - that is operational cost the Intune Suite replaces with a predictable line item. <a href="/posts/msp-license-ladder-05-endpoint-gap/">Read the full post</a>.</p>

<h2 id="the-licensing-map">The licensing map</h2>

<p>Three upgrade paths cover all five gaps. The right one depends on the customer’s base SKU.</p>

<h3 id="business-premium-customers">Business Premium customers</h3>

<table>
  <thead>
    <tr>
      <th>Add-on</th>
      <th>List price</th>
      <th>Gaps closed</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Defender Suite for BP</strong></td>
      <td>$10/user/mo</td>
      <td>Hunting, Identity, Exposure</td>
    </tr>
    <tr>
      <td><strong>Purview Suite for BP</strong></td>
      <td>$10/user/mo</td>
      <td>Data</td>
    </tr>
    <tr>
      <td><strong>Defender + Purview bundle</strong></td>
      <td>$15/user/mo</td>
      <td>Hunting, Identity, Exposure, Data</td>
    </tr>
    <tr>
      <td><strong>Intune Suite</strong></td>
      <td>$10/user/mo</td>
      <td>Endpoints</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>$25/user/month closes all five gaps on Business Premium.</strong> That is the ceiling. Not every customer needs all of it - start with the gaps that match their risk profile and compliance requirements.</p>
</blockquote>

<h3 id="e3-customers">E3 customers</h3>

<table>
  <thead>
    <tr>
      <th>Add-on</th>
      <th>List price</th>
      <th>Gaps closed</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>E5 Security</strong></td>
      <td>$12/user/mo</td>
      <td>Hunting, Identity, Exposure</td>
    </tr>
    <tr>
      <td><strong>Purview Suite</strong></td>
      <td>$10/user/mo</td>
      <td>Data</td>
    </tr>
    <tr>
      <td><strong>Intune Suite</strong></td>
      <td>$10/user/mo</td>
      <td>Endpoints (partial - July 2026 folds in Remote Help, Advanced Analytics, Plan 2)</td>
    </tr>
  </tbody>
</table>

<h3 id="e5-customers">E5 customers</h3>

<p>E5 includes E5 Security (Hunting, Identity, Exposure gaps closed). After July 2026, the full Intune Suite folds in automatically.</p>

<table>
  <thead>
    <tr>
      <th>Add-on</th>
      <th>List price</th>
      <th>Gaps closed</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Purview Suite</strong></td>
      <td>$10/user/mo</td>
      <td>Data</td>
    </tr>
    <tr>
      <td><strong>Intune Suite</strong> (until July 2026)</td>
      <td>$10/user/mo</td>
      <td>Endpoints</td>
    </tr>
  </tbody>
</table>

<p>Prices are Microsoft list per user per month, as of May 2026. CSP, NCE, and Enterprise Agreement pricing differs; check with your licensing partner for the real customer number.</p>

<h2 id="the-pattern-across-all-five-posts">The pattern across all five posts</h2>

<p>Every post followed the same structure because the licensing problem is the same every time.</p>

<p><strong>The base SKU covers the obvious use case.</strong> Alerts fire. Policies deploy. Compliance checks run.</p>

<p><strong>The gap is in what happens next.</strong> Hunting after the alert. Investigating after the phish. Reviewing after the access grant. Patching after the enrollment.</p>

<p><strong>The upgrade is not a feature purchase. It is an operational trade.</strong> You are replacing unpredictable manual work with a predictable per-user cost and a repeatable process.</p>

<p>That last point is the one that matters most for MSPs. Every gap in this series is also a service opportunity:</p>

<ul>
  <li><strong>Hunting:</strong> managed detection, KQL library, custom detection rule tuning</li>
  <li><strong>Data:</strong> DLP policy authoring, Insider Risk triage, eDiscovery delivery</li>
  <li><strong>Identity:</strong> PIM onboarding, access review cadence, hybrid identity hardening</li>
  <li><strong>Exposure:</strong> phishing investigation, OAuth app governance, Shadow IT and AI review</li>
  <li><strong>Endpoints:</strong> app lifecycle management, EPM policy tuning, third-party patching</li>
</ul>

<div class="post-note"><strong>The license is the floor. The value is the baseline you deploy, the policies you tune, the KQL library you build, and the triage capacity you provide on top of it.</strong></div>

<h2 id="where-to-start">Where to start</h2>

<p>Do not try to close all five gaps at once. Pick the one that matches the customer’s most urgent risk or compliance requirement:</p>

<ul>
  <li><strong>Post-incident:</strong> start with #1 (Hunting) or #4 (Exposure) - the customer just had an incident and wants investigation capability</li>
  <li><strong>Compliance audit:</strong> start with #2 (Data) - the auditor is asking about DLP, retention, and eDiscovery</li>
  <li><strong>Admin sprawl:</strong> start with #3 (Identity) or #5 (Endpoints) - standing admin privileges are the problem</li>
  <li><strong>Operational overhead:</strong> start with #5 (Endpoints) - too much time on manual packaging and elevation requests</li>
</ul>

<p>Deploy in one tenant first. Validate the baseline. Then scale out.</p>

<p>Business Premium and E3 are not bad licenses. They are starting points. The moment a customer needs investigation, governance, automation, or operational scale, the real licensing conversation begins.</p>

<h2 id="the-series">The series</h2>

<table>
  <thead>
    <tr>
      <th>#</th>
      <th>Gap</th>
      <th>What closes it</th>
      <th>Post</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Hunting</td>
      <td>MDE Plan 2</td>
      <td><a href="/posts/msp-license-ladder-01-hunting-gap/">The Hunting Gap</a></td>
    </tr>
    <tr>
      <td>2</td>
      <td>Data</td>
      <td>Purview Suite</td>
      <td><a href="/posts/msp-license-ladder-02-data-gap/">The Data Gap</a></td>
    </tr>
    <tr>
      <td>3</td>
      <td>Identity</td>
      <td>Entra ID P2 + DfI</td>
      <td><a href="/posts/msp-license-ladder-03-identity-gap/">The Identity Gap</a></td>
    </tr>
    <tr>
      <td>4</td>
      <td>Exposure</td>
      <td>MDO P2 + DfCA</td>
      <td><a href="/posts/msp-license-ladder-04-exposure-gap/">The Exposure Gap</a></td>
    </tr>
    <tr>
      <td>5</td>
      <td>Endpoints</td>
      <td>Intune Suite</td>
      <td><a href="/posts/msp-license-ladder-05-endpoint-gap/">The Endpoint Gap</a></td>
    </tr>
  </tbody>
</table>

<p><em>Last verified: May 2026</em></p>

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "What closes the Hunting Gap in Microsoft 365 Business Premium and E3?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Microsoft Defender for Endpoint Plan 2. It adds 30 days of queryable endpoint telemetry, Advanced Hunting with KQL, and Custom Detection Rules that run continuously across the customer fleet. Business Premium and E3 alert on known threats but do not give you the ability to hunt for the ones that have not triggered yet."
      }
    },
    {
      "@type": "Question",
      "name": "What closes the Data Gap in Microsoft 365 Business Premium and E3?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "The Microsoft Purview Suite at $10 per user per month. It adds endpoint DLP, Teams DLP, Insider Risk Management, eDiscovery Premium, and event-based retention. Base Purview only covers email and files in SharePoint and OneDrive, leaving endpoints, Teams chat, and cloud apps uncovered."
      }
    },
    {
      "@type": "Question",
      "name": "What closes the Identity Gap?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Microsoft Entra ID P2 plus Defender for Identity. Entra ID P2 adds risk-based Conditional Access, Privileged Identity Management, and Access Reviews. Defender for Identity adds on-premises Active Directory threat detection. Both are included in the Defender Suite for Business Premium at $10 per user per month."
      }
    },
    {
      "@type": "Question",
      "name": "What closes the Exposure Gap?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Microsoft Defender for Office 365 Plan 2 plus Defender for Cloud Apps. MDO P2 adds Threat Explorer, Automated Investigation and Response, Campaign Views, and Attack Simulation Training. Defender for Cloud Apps adds shadow IT discovery, OAuth governance, session controls, and shadow AI detection. Both are included in the Defender Suite."
      }
    },
    {
      "@type": "Question",
      "name": "What closes the Endpoint Gap in Microsoft Intune?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "The Microsoft Intune Suite at $10 per user per month. It includes Endpoint Privilege Management, Enterprise App Management, Advanced Analytics, Remote Help, Microsoft Cloud PKI, and Intune Plan 2 features. After July 2026, E3 customers get Remote Help, Advanced Analytics, and Plan 2 for free. E5 gets the full Suite. Business Premium does not."
      }
    },
    {
      "@type": "Question",
      "name": "How much does it cost to close all five gaps on Microsoft 365 Business Premium?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "$25 per user per month at list price. That is the Defender plus Purview bundle ($15/user/month) covering Hunting, Identity, Exposure, and Data, plus the Intune Suite ($10/user/month) covering Endpoints. Not every customer needs all five upgrades; start with the gaps that match their risk profile and compliance requirements."
      }
    }
  ]
}
</script>]]></content>

      
      
      <author>
          <name>Roy Klooster</name>
      </author>

      
        <category term="Security" />
      
        <category term="M365" />
      
        <category term="Intune" />
      
        <category term="Entra ID" />
      
        <category term="MDE" />
      
        <category term="Defender" />
      

      
      
        <summary type="html"><![CDATA[Five gaps. Five upgrades. One licensing map. The MSP License Ladder series capstone with upgrade paths for Business Premium, E3, and E5.]]></summary>
      
    </entry>
  
    <entry>
      

      <title type="html">The MSP License Ladder #5: The Endpoint Gap</title>
      <link href="https://rksolutions.nl/posts/msp-license-ladder-05-endpoint-gap/" rel="alternate" type="text/html" title="The MSP License Ladder #5: The Endpoint Gap" />
      <published>2026-05-16T00:00:00+00:00</published>
      <updated>2026-05-16T00:00:00+00:00</updated>
      <id>https://rksolutions.nl/posts/msp-license-ladder-05-endpoint-gap</id>
      <content type="html" xml:base="https://rksolutions.nl/posts/msp-license-ladder-05-endpoint-gap/"><![CDATA[<p>A customer still relies on a legacy file share, and a user needs to install a custom organization-specific application to access it. On Intune Plan 1, you either give them local admin - permanently, across every app, with no audit trail - or you remote in yourself and burn 15 minutes per request. Scale that across your customer base and those remote sessions add up fast.</p>

<div class="post-note">Installing unmanaged applications is already a problem in its own right - every app outside your deployment pipeline is one you cannot patch, audit, or remove at scale. EPM does not solve the unmanaged app problem, but it does remove the reason users get local admin in the first place.</div>

<p>Meanwhile, GlobalProtect pushes another update. On Intune Plan 1, keeping it current means: download the new installer, repackage it as a Win32 app, configure detection rules, upload to each tenant, deploy, and monitor - or build a custom wrapper around winget and hope it holds. Either way, that is engineering time you are spending on work a catalog should handle for you. And GlobalProtect is just one app out of dozens.</p>

<p>Both are endpoint management problems that Intune Plan 1 does not solve. They are also cost problems. Every 15-minute remote session for an elevation request, every hour spent repackaging a Win32 update, every incident that starts with a user who had local admin they should not have had - that is money. The Intune Suite at $10/user/month is not just a feature upgrade. It is a trade: a predictable line item that replaces unpredictable operational cost.</p>

<p>This post is part of <strong>The MSP License Ladder</strong>, a series on what Business Premium and E3 don’t give you, and how to build on top of them. Every post covers one gap: what’s missing from the base SKU, what it costs to close, and what you can actually build with it. Previous: <a href="/posts/msp-license-ladder-04-exposure-gap/">#4, The Exposure Gap</a>.</p>

<p>A few terms up front:</p>

<ul>
  <li><strong>EPM</strong> (Endpoint Privilege Management): just-in-time admin elevation without standing local admin rights. Users request, you approve (or auto-approve by rule).</li>
  <li><strong>EAM</strong> (Enterprise App Management): curated app catalog with 450+ third-party apps, auto-update, and supersedence handling. Microsoft hosts the binaries.</li>
  <li><strong>Intune Suite</strong>: the bundle that includes EPM, EAM, Advanced Analytics, Remote Help, Cloud PKI, and Intune Plan 2 features. $10/user/month on top of Intune Plan 1.</li>
</ul>

<p><strong>Table of Contents</strong></p>

<ul>
  <li><a href="#what-you-actually-get-today">What you actually get today</a></li>
  <li><a href="#the-july-2026-split">The July 2026 split</a></li>
  <li><a href="#the-concrete-gaps">The concrete gaps</a>
    <ul>
      <li><a href="#endpoint-privilege-management">Endpoint Privilege Management</a></li>
      <li><a href="#enterprise-app-management">Enterprise App Management</a></li>
      <li><a href="#advanced-analytics">Advanced Analytics</a></li>
      <li><a href="#remote-help">Remote Help</a></li>
      <li><a href="#intune-plan-2">Intune Plan 2</a></li>
      <li><a href="#cloud-pki">Cloud PKI</a></li>
    </ul>
  </li>
  <li><a href="#the-upgrade-paths">The upgrade paths</a></li>
  <li><a href="#what-you-unlock">What you unlock</a></li>
  <li><a href="#when-not-to-bother">When not to bother</a></li>
  <li><a href="#security-copilot-in-intune">Security Copilot in Intune</a></li>
  <li><a href="#how-to-pitch-it">How to pitch it</a></li>
  <li><a href="#conclusion">Conclusion</a></li>
</ul>

<h2 id="what-you-actually-get-today">What you actually get today</h2>

<p><strong>Business Premium</strong>, <strong>Microsoft 365 E3</strong>, and <strong>Microsoft 365 E5</strong> all include <strong>Intune Plan 1</strong>:</p>

<p>Enroll devices across Windows, macOS, iOS, and Android. Push configuration profiles, compliance policies, and Windows Update rings. Deploy Win32 apps, LOB apps, and Microsoft Store apps. Use device compliance as a Conditional Access grant control. Provision new machines with Windows Autopilot. Get basic device and app performance data through endpoint analytics.</p>

<p>All three SKUs share the same endpoint management baseline right now. None of them include the Intune Suite or any of its components.</p>

<h2 id="the-july-2026-split">The July 2026 split</h2>

<p>This is what makes this post different from every other post in the series. In July 2026, Microsoft is folding Intune Suite components into E3 and E5 - but not into Business Premium. For the first time in the series, a gap closes automatically for some customers and stays open for others.</p>

<table>
  <thead>
    <tr>
      <th>Component</th>
      <th>BP after July 2026</th>
      <th>E3 after July 2026</th>
      <th>E5 after July 2026</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Remote Help</strong></td>
      <td>Add-on</td>
      <td><strong>Included</strong></td>
      <td><strong>Included</strong></td>
    </tr>
    <tr>
      <td><strong>Advanced Analytics</strong></td>
      <td>Add-on</td>
      <td><strong>Included</strong></td>
      <td><strong>Included</strong></td>
    </tr>
    <tr>
      <td><strong>Intune Plan 2</strong> (Tunnel MAM, specialty devices, firmware OTA)</td>
      <td>Add-on</td>
      <td><strong>Included</strong></td>
      <td><strong>Included</strong></td>
    </tr>
    <tr>
      <td><strong>Endpoint Privilege Management</strong></td>
      <td>Add-on</td>
      <td>Add-on</td>
      <td><strong>Included</strong></td>
    </tr>
    <tr>
      <td><strong>Enterprise App Management</strong></td>
      <td>Add-on</td>
      <td>Add-on</td>
      <td><strong>Included</strong></td>
    </tr>
    <tr>
      <td><strong>Cloud PKI</strong></td>
      <td>Add-on</td>
      <td>Add-on</td>
      <td><strong>Included</strong></td>
    </tr>
  </tbody>
</table>

<p>Source: <a href="https://techcommunity.microsoft.com/blog/microsoftintuneblog/microsoft-365-adds-advanced-microsoft-intune-solutions-at-scale/4474272">Microsoft 365 adds advanced Microsoft Intune solutions at scale</a>. Rollout begins CY26 Q3. Eligible tenants are auto-provisioned with 30-day admin center notice.</p>

<p><strong>The takeaway:</strong> E5 customers get everything. E3 customers get Remote Help, Advanced Analytics, and Plan 2 features - but not EPM, EAM, or Cloud PKI. Business Premium customers get nothing. The Endpoint Gap stays fully open for BP.</p>

<h2 id="the-concrete-gaps">The concrete gaps</h2>

<p>Intune Plan 1 enrolls devices, pushes configuration, and deploys apps. Microsoft Store apps handle their own updates, but every Win32 and LOB app you deploy is yours to keep current - unless the app has its own auto-update mechanism, you are repackaging and redeploying manually. That is only the beginning of where the baseline stops. These are the gaps that show up after enrollment.</p>

<h3 id="endpoint-privilege-management">Endpoint Privilege Management</h3>

<p>A user needs admin rights to install a custom application for a legacy file share. On Intune Plan 1, your options are: give them local admin (permanent, unscoped, no audit trail) or remote in yourself (15 minutes per request, does not scale). And since EPM only allows elevation for local executables, you also need to copy the installer from the share to the local drive first.</p>

<p><strong>EPM</strong> replaces both with just-in-time elevation. You define elevation rules: auto-approve known executables by file hash or certificate, require approval for unknowns, block everything else. The user right-clicks and selects “Run with elevated access.” If the rule matches, it elevates that single process. If not, it routes to you for approval. Every elevation is logged with full metadata - executable name, user, device, rule matched, timestamp.</p>

<div class="post-note">EPM is not strictly an app installation tool, even though that is how many people first encounter it. Any executable that needs elevation can be managed through EPM rules - app updaters, driver installers, configuration tools, scripts that write to protected locations. The value is removing the reason users have standing local admin, regardless of what triggers the elevation request.</div>

<p>This ties directly back to <a href="/posts/msp-license-ladder-01-hunting-gap/">#1, The Hunting Gap</a>. Local admin sprawl is exactly what Advanced Hunting flags in <code class="language-plaintext highlighter-rouge">DeviceLogonEvents</code>. EPM prevents it at the source instead of hunting for it after the fact.</p>

<h3 id="enterprise-app-management">Enterprise App Management</h3>

<p>Take something as common as GlobalProtect. On Intune Plan 1, you have two options: package every update as a new Win32 app (download the installer, wrap it, configure detection rules, upload, deploy, repeat next month), or build a custom Win32 app that shells out to winget and hope the detection logic holds. Both work. Neither scale. And that is just one app - multiply it by every third-party tool your customers rely on that does not auto-update through its own mechanism.</p>

<p><strong>EAM</strong> provides the <strong>Enterprise App Catalog</strong> - a curated collection of 450+ third-party apps hosted by Microsoft. GlobalProtect is in the catalog. So are Zoom, 7-Zip, Notepad++, and hundreds of others. You pick the app, and Intune provides the binary, the detection rules, the install commands, and the supersedence logic. When a new version lands in the catalog (target: 80-90% within 24 hours of vendor release), Intune flags it in the <strong>Enterprise App Catalog apps with updates</strong> view. You review it, create the supersedence relationship, and the update rolls out. It is not fully automatic - you still approve the update - but the heavy lifting (packaging, detection rules, hosting) is done for you. No more repackaging. No more winget workarounds.</p>

<p>The difference: you stop being a software distribution pipeline and start being a policy layer on top of one.</p>

<h3 id="advanced-analytics">Advanced Analytics</h3>

<p>A user reports their laptop is slow. You check compliance - it says “compliant.” That tells you the device meets your minimum bar. It does not tell you why it is slow.</p>

<p><strong>Advanced Analytics</strong> extends base endpoint analytics with reports that answer the question compliance cannot: <strong>resource performance</strong> (CPU and RAM issues by device, model, and manufacturer), <strong>battery health</strong> (cycle count, designed vs actual capacity, degradation trends), <strong>anomalies</strong> (regressions in user experience after configuration changes), and <strong>device timeline</strong> (low-latency event history for troubleshooting). The <strong>properties catalog</strong> collects detailed hardware inventory per device, but spotting a trend across hundreds of devices from individual device views is impractical - that is where fleet-wide device query comes in.</p>

<p>The standout feature is <strong>device query for multiple devices</strong>. Once you deploy a <strong>properties catalog</strong> policy to your Windows devices, Intune collects hardware inventory data that you can query using KQL - across your entire fleet, not just one device at a time. The properties catalog covers CPU, disk, memory, TPM, BIOS, battery, network adapters, encryption status, OS version, and more. You can run queries like “show me all devices with an unencrypted volume,” “list devices with less than 50% battery health,” or “count devices by OS version” - and get results across hundreds of devices in seconds. Device query also works on iOS, Android, and macOS without a properties catalog policy.</p>

<p>Think of it as Advanced Hunting for device inventory instead of security telemetry.</p>

<h3 id="remote-help">Remote Help</h3>

<p>Quick Assist works fine for basic Windows remote support. But it does not tie into your identity stack, it has no role-based access control, no compliance warnings before connecting, and no audit trail in the Intune admin center. And it only works on Windows.</p>

<p><strong>Remote Help</strong> is remote assistance built into Intune. Both helper and sharer authenticate with Entra ID - every session is identity-verified within your tenant. RBAC controls what the helper can do: view-only or full control. On Windows, helpers can also enter UAC credentials on the sharer’s device for elevated access. Sessions are logged in the Intune admin center with full audit history. Before connecting, the helper sees the device’s compliance state.</p>

<p>Where Remote Help pulls ahead of Quick Assist and third-party tools:</p>

<ul>
  <li><strong>Cross-platform</strong>: Windows, macOS (native app or web-based screen sharing), and Android (including unattended access for dedicated devices)</li>
  <li><strong>Unenrolled device support</strong>: optionally allow help on devices not yet enrolled in Intune - useful for onboarding scenarios</li>
  <li><strong>Remote launch</strong> (Windows): start a session from the Intune admin center by sending a notification to the sharer’s device - without the sharer initiating it</li>
  <li><strong>Conditional Access integration</strong>: require MFA, compliant devices, or specific locations before allowing a help session</li>
  <li><strong>Web app fallback</strong>: if the sharer cannot install the native app, they can share their screen via a browser (view-only for the helper)</li>
</ul>

<h3 id="intune-plan-2">Intune Plan 2</h3>

<p>Three capabilities bundled together:</p>

<ul>
  <li><strong>Microsoft Tunnel for MAM</strong>: mobile VPN gateway that works without device enrollment. The standout for BYOD - users connect to corporate resources from personal iOS/Android devices through an app-level tunnel, without enrolling the device in Intune.</li>
  <li><strong>Specialty device management</strong>: enrollment and policy support for AR/VR headsets, large smart-screen devices, and conference room equipment.</li>
  <li><strong>Firmware-over-the-air</strong>: remote firmware updates for supported Zebra devices.</li>
</ul>

<h3 id="cloud-pki">Cloud PKI</h3>

<p>Certificate-based authentication without on-prem ADCS or NDES infrastructure. Matters most for cloud-native customers who need Wi-Fi or VPN certificate profiles but do not have (and do not want) a server room to run a PKI. If the customer has an existing on-prem PKI that works, Cloud PKI solves a problem they do not have.</p>

<h2 id="the-upgrade-paths">The upgrade paths</h2>

<p>Three paths to close the Endpoint Gap.</p>

<table>
  <thead>
    <tr>
      <th>Path</th>
      <th>Best for</th>
      <th>Includes</th>
      <th>List price</th>
      <th>Main constraint</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Intune Suite</strong></td>
      <td>BP customers who want everything</td>
      <td>EPM, EAM, Advanced Analytics, Remote Help, Cloud PKI, Plan 2 features</td>
      <td><strong>$10/user/mo</strong></td>
      <td>Requires Intune Plan 1</td>
    </tr>
    <tr>
      <td><strong>Standalone add-ons</strong></td>
      <td>Targeted purchases</td>
      <td>EPM, EAM, Advanced Analytics, Remote Help, or Cloud PKI individually</td>
      <td><strong>~$3-4 each</strong></td>
      <td>No Plan 2 features unless bought separately</td>
    </tr>
    <tr>
      <td><strong>Wait for July 2026</strong></td>
      <td>E3/E5 customers</td>
      <td>Folded into base SKU automatically</td>
      <td><strong>Free</strong> (via $3 price increase)</td>
      <td>BP gets nothing; E3 misses EPM, EAM, Cloud PKI</td>
    </tr>
  </tbody>
</table>

<p>Prices are Microsoft list per user per month, as of May 2026. Confirm standalone add-on pricing at purchase time.</p>

<p><strong>The bundle math:</strong> EPM + EAM standalone is roughly $6-8/user/month for two components. The Intune Suite at $10/user/month adds Advanced Analytics, Remote Help, Cloud PKI, and Plan 2 features on top. Unless the customer only needs one specific component, the Suite is the better path.</p>

<h2 id="what-you-unlock">What you unlock</h2>

<table>
  <thead>
    <tr>
      <th>Capability</th>
      <th>What changes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>EPM</strong></td>
      <td>Just-in-time elevation with approval workflows and full audit logging. No more standing local admin.</td>
    </tr>
    <tr>
      <td><strong>EAM</strong></td>
      <td>450+ third-party apps from a curated catalog with auto-update and supersedence. No more manual packaging.</td>
    </tr>
    <tr>
      <td><strong>Advanced Analytics</strong></td>
      <td>Device health scoring, anomaly detection, battery health, and fleet-wide KQL device queries.</td>
    </tr>
    <tr>
      <td><strong>Remote Help</strong></td>
      <td>Entra-authenticated remote assistance with RBAC, session recording, and browser-based access for unmanaged devices.</td>
    </tr>
    <tr>
      <td><strong>Tunnel for MAM</strong></td>
      <td>App-level VPN for personal devices without device enrollment.</td>
    </tr>
    <tr>
      <td><strong>Cloud PKI</strong></td>
      <td>Cloud-native certificate authority for Wi-Fi and VPN profiles without on-prem infrastructure.</td>
    </tr>
  </tbody>
</table>

<p><strong>The practical difference:</strong> the custom app install from the opening becomes a 10-second EPM approval instead of a 15-minute remote session. Third-party app updates become catalog-driven rollouts instead of manual packaging sprints across 5 tenants.</p>

<h2 id="when-not-to-bother">When not to bother</h2>

<p>Not every customer needs the Intune Suite. Two scenarios where the upgrade will not earn its keep:</p>

<p><strong>E3/E5 customers approaching July 2026.</strong> If the customer is 2-3 months away from the fold-in and the need is not urgent, waiting is free. E3 gets Remote Help, Advanced Analytics, and Plan 2 features. E5 gets everything. The exception is E3 customers who need EPM or EAM specifically - those do not fold into E3, only into E5.</p>

<p><strong>Very small BP customer with no app sprawl and no elevation patterns.</strong> If the customer has 10 managed devices, all corporate, running a handful of LOB apps, and the owner is already local admin everywhere, the Suite adds capability they will not use enough to justify $10/user/month. EPM matters most for organizations where users regularly need elevation - a 5-person office where everyone has local admin already does not have that pattern.</p>

<h2 id="security-copilot-in-intune">Security Copilot in Intune</h2>

<p>One more thing worth knowing. <strong>Security Copilot</strong> - which powers the Copilot experience inside the Intune admin center - is included with <strong>Microsoft 365 E5</strong> at no additional cost. E5 customers get 400 Security Compute Units (SCUs) per month per 1,000 users, auto-provisioned. Business Premium and E3 customers would need a standalone Security Copilot purchase.</p>

<p>Copilot in Intune lets you run natural language device queries (“show me all devices that failed the latest Windows update”), troubleshoot policy conflicts, and generate KQL from plain English. It is not part of the Intune Suite - it sits on a different licensing model entirely (consumption-based SCUs vs per-user). It deserves its own coverage, but for this post the key point is: E5 gets it, BP and E3 do not.</p>

<h2 id="how-to-pitch-it">How to pitch it</h2>

<p><strong>Post-incident (EPM).</strong> <em>“The compromised device last month started with a user who had standing local admin. EPM removes that and lets you approve specific elevations on demand. Every request is logged.”</em></p>

<p><strong>Patch urgency (EAM).</strong> <em>“You repackage GlobalProtect every month across every tenant. EAM auto-patches 450+ third-party apps from a Microsoft-hosted catalog. You set the policy once.”</em></p>

<p><strong>Insurance and compliance (Advanced Analytics + Remote Help).</strong> <em>“Your cyber insurance questionnaire asks about device health monitoring and secure remote access. Advanced Analytics gives you fleet health scoring. Remote Help gives you auditable, identity-verified support sessions.”</em></p>

<p><strong>July 2026 timing (for BP customers).</strong> <em>“Your E3 and E5 competitors are about to get Remote Help, Advanced Analytics, and Plan 2 features for free. Business Premium does not. The Intune Suite at $10/user/month is how you close that gap.”</em></p>

<p>Price it as a managed endpoint service, not a licence passthrough. The $10/user/month Intune Suite is the floor.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Intune Plan 1 gets devices enrolled and configured. The Intune Suite adds what comes after: EPM removes standing local admin and replaces it with audited, just-in-time elevation. EAM takes third-party app packaging off your plate. Advanced Analytics gives you fleet-wide device health data you can actually query. Remote Help ties remote support to your identity stack across Windows, macOS, and Android.</p>

<p>The $10/user/month cost is real, but so is the time you are currently spending on manual elevation requests, Win32 repackaging, and blind troubleshooting. For many customers, the Suite pays for itself in reduced operational overhead before the security benefits even enter the conversation.</p>

<p>After July 2026, the math changes. E5 customers get the full Suite folded in. E3 customers get Remote Help, Advanced Analytics, and Plan 2 features. Business Premium customers get nothing - the Endpoint Gap stays open, and closing it remains a separate purchase.</p>

<p>Start with one customer tenant. Deploy an EPM elevation settings policy, connect the Enterprise App Catalog, and review the first week of data. Then scale out.</p>

<table>
  <thead>
    <tr>
      <th>Resource</th>
      <th>What it covers</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/intune/fundamentals/add-ons">Intune Suite add-ons</a></td>
      <td>Component matrix, standalone vs Suite vs Plan 2</td>
    </tr>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/intune/epm/overview">EPM overview</a></td>
      <td>Elevation rules, policies, and deployment</td>
    </tr>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/intune/app-management/deployment/enterprise-app-management">Enterprise App Management</a></td>
      <td>App catalog, auto-update, supersedence</td>
    </tr>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/intune/advanced-analytics/">Advanced Analytics</a></td>
      <td>Device health, anomaly detection, device query</td>
    </tr>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/intune/remote-help/">Remote Help</a></td>
      <td>Zero-trust remote assistance setup</td>
    </tr>
    <tr>
      <td><a href="https://www.microsoft.com/en-us/security/business/microsoft-intune-pricing">Intune Plans and Pricing</a></td>
      <td>Suite and standalone pricing</td>
    </tr>
    <tr>
      <td><a href="https://techcommunity.microsoft.com/blog/microsoftintuneblog/microsoft-365-adds-advanced-microsoft-intune-solutions-at-scale/4474272">July 2026 Intune fold-in</a></td>
      <td>E3/E5 component breakdown and rollout timeline</td>
    </tr>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/copilot/security/security-copilot-inclusion">Security Copilot for E5</a></td>
      <td>SCU allocation and auto-provisioning</td>
    </tr>
  </tbody>
</table>

<hr />

<p><em>Part of <a href="/posts/msp-license-ladder-01-hunting-gap/">The MSP License Ladder</a> series. Previous: <a href="/posts/msp-license-ladder-04-exposure-gap/">#4, The Exposure Gap</a>.</em></p>

<p><em>Last verified: May 2026</em></p>

<h2 id="related">Related</h2>

<ul>
  <li><a href="/posts/msp-license-ladder-01-hunting-gap/">The MSP License Ladder #1: The Hunting Gap</a> – what Defender for Endpoint Plan 2 unlocks for proactive threat hunting across your customer fleet.</li>
  <li><a href="/posts/msp-license-ladder-02-data-gap/">The MSP License Ladder #2: The Data Gap</a> – what Purview Suite unlocks over base Business Premium and E3 for DLP, Insider Risk, and eDiscovery.</li>
  <li><a href="/posts/msp-license-ladder-03-identity-gap/">The MSP License Ladder #3: The Identity Gap</a> – what Entra ID P2 and Defender for Identity unlock for risk-based CA, just-in-time privilege, and on-prem AD visibility.</li>
  <li><a href="/posts/msp-license-ladder-04-exposure-gap/">The MSP License Ladder #4: The Exposure Gap</a> – what MDO P2 and Defender for Cloud Apps unlock for post-delivery investigation and Shadow IT/AI visibility.</li>
</ul>]]></content>

      
      
      <author>
          <name>Roy Klooster</name>
      </author>

      
        <category term="Intune" />
      
        <category term="Security" />
      
        <category term="M365" />
      

      
      
        <summary type="html"><![CDATA[Intune Plan 1 enrolls and configures devices. It does not give you least-privilege elevation, third-party app patching, device health analytics, or zero-trust remote support. The Intune Suite closes those gaps - and in July 2026, E3 and E5 get parts of it for free while Business Premium does not.]]></summary>
      
    </entry>
  
    <entry>
      

      <title type="html">The MSP License Ladder #4: The Exposure Gap</title>
      <link href="https://rksolutions.nl/posts/msp-license-ladder-04-exposure-gap/" rel="alternate" type="text/html" title="The MSP License Ladder #4: The Exposure Gap" />
      <published>2026-05-12T00:00:00+00:00</published>
      <updated>2026-05-12T00:00:00+00:00</updated>
      <id>https://rksolutions.nl/posts/msp-license-ladder-04-exposure-gap</id>
      <content type="html" xml:base="https://rksolutions.nl/posts/msp-license-ladder-04-exposure-gap/"><![CDATA[<p>A phishing campaign targets a customer. Safe Links blocks the URL, Safe Attachments catches the payload, and the alerts close. Prevention worked. But the CISO asks: “Was this a targeted campaign or a spray? Did anyone receive a variant that ZAP hasn’t caught yet? Are there related messages we haven’t found?” On MDO Plan 1, you can run a Message Trace, but you cannot correlate messages into a campaign, see click telemetry across the tenant, or purge 30 affected mailboxes in one action.</p>

<p>Meanwhile, a different customer onboards after leaving their previous MSP. The old MSP’s ticketing tool still holds an active OAuth consent with <code class="language-plaintext highlighter-rouge">Mail.ReadWrite</code> and <code class="language-plaintext highlighter-rouge">Directory.Read.All</code> scope. Entra shows the consent exists, but not whether the app is still reading mail, whether the scope was ever appropriate, or whether the former MSP even still controls the app registration. And that is just the apps that went through Entra. The browser-based AI tools, file converters, and screen recording services that never touch SSO are completely invisible.</p>

<p>Both are exposure problems. Prevention works, but you cannot measure the exposure that slips past it. That is the gap this post is about.</p>

<p>This post is part of <strong>The MSP License Ladder</strong>, a series on what Business Premium and E3 don’t give you, and how to build on top of them. Every post covers one gap: what’s missing from the base SKU, what it costs to close, and what you can actually build with it. Previous: <a href="/posts/msp-license-ladder-03-identity-gap/">#3, The Identity Gap</a>.</p>

<p>A few terms up front:</p>

<ul>
  <li><strong>EOP</strong> (Exchange Online Protection): baseline email security in every M365 subscription. Anti-spam, anti-malware, spoofing protection.</li>
  <li><strong>MDO</strong> (Microsoft Defender for Office 365): Plan 1 adds prevention (Safe Links, Safe Attachments). Plan 2 adds investigation and response (Threat Explorer, AIR, Attack Simulation Training).</li>
  <li><strong>DfCA</strong> (Defender for Cloud Apps, previously known as MCAS): Microsoft’s Cloud Access Security Broker. Shadow IT discovery, OAuth app governance, session policies, SaaS DLP.</li>
  <li><strong>AIR</strong> (Automated Investigation and Response): playbook-driven investigation that correlates signals across email, endpoints, and identity.</li>
</ul>

<p><strong>Table of Contents</strong></p>

<ul>
  <li><a href="#what-you-actually-get-today">What you actually get today</a></li>
  <li><a href="#the-concrete-gaps-email-and-collaboration">The concrete gaps: email and collaboration</a></li>
  <li><a href="#the-concrete-gaps-cloud-apps-and-oauth">The concrete gaps: cloud apps and OAuth</a></li>
  <li><a href="#the-upgrade-paths">The upgrade paths</a></li>
  <li><a href="#what-you-unlock">What you unlock</a></li>
  <li><a href="#when-not-to-bother">When not to bother</a></li>
  <li><a href="#how-to-pitch-it">How to pitch it</a></li>
  <li><a href="#conclusion">Conclusion</a></li>
</ul>

<h2 id="what-you-actually-get-today">What you actually get today</h2>

<p><strong>Business Premium</strong> ships with <strong>EOP</strong> plus <strong>MDO Plan 1</strong>:</p>

<ul>
  <li><strong>Safe Links</strong>: URL detonation at click time across email, Office documents, and Teams</li>
  <li><strong>Safe Attachments</strong>: sandbox detonation for email attachments and files in SharePoint, OneDrive, and Teams</li>
  <li><strong>Anti-phishing</strong>: user and domain impersonation protection, mailbox intelligence</li>
  <li><strong>Zero-hour auto purge</strong> (ZAP): retroactive removal of malicious messages already delivered</li>
  <li><strong>Real-time detections</strong>: a lightweight investigation view (not the full Threat Explorer)</li>
</ul>

<p><strong>Microsoft 365 E3 today</strong> ships with <strong>EOP only</strong>. No Safe Links, no Safe Attachments, no anti-phishing impersonation protection.</p>

<p><strong>E3 from July 2026</strong> gains <strong>MDO Plan 1</strong> automatically as part of the <a href="https://www.microsoft.com/en-us/licensing/news/2026-M365-Packaging-Pricing-Updates">Microsoft 365 packaging update</a>. Rollout begins June 2026 and completes by August 1, 2026. After that date, E3 and Business Premium share the same email security baseline.</p>

<p><strong>For SaaS and OAuth on both SKUs:</strong> base Entra ID shows consented apps in the Enterprise Applications blade. No risk scoring, no discovery of apps outside SSO, no session controls, no AI tool detection.</p>

<h2 id="the-concrete-gaps-email-and-collaboration">The concrete gaps: email and collaboration</h2>

<p>MDO Plan 1 prevents threats. It does not help you investigate what got through. These are the scenarios where that gap shows up.</p>

<h3 id="post-delivery-visibility">Post-delivery visibility</h3>

<p>A phishing email was delivered to 30 mailboxes. You need to find everyone who received it, not just those who clicked. <strong>Threat Explorer</strong> gives you 30 days of searchable email data filtered by sender, subject, URL, attachment hash, or delivery action. On Plan 1, <strong>Real-time detections</strong> is a subset that cannot search historical delivered mail at the same depth.</p>

<h3 id="automated-investigation-and-response">Automated Investigation and Response</h3>

<p>A user reports a phish. You need to correlate the sender’s patterns, the URL’s reputation, the attachment’s detonation results, and what the recipient did after clicking. On Plan 1, that is manual. <strong>AIR</strong> automates it: runs a playbook, correlates signals, and produces a remediation recommendation you review and approve.</p>

<h3 id="campaign-views">Campaign Views</h3>

<p>Three employees report suspicious emails. Are they related? <strong>Campaign Views</strong> groups related phishing attempts by sender infrastructure, payload, and social engineering pattern into a single view. On Plan 1, you check each report individually.</p>

<h3 id="attack-simulation-training">Attack Simulation Training</h3>

<p>The customer’s cyber insurance renewal asks about phishing simulations and click rates. <strong>Attack Simulation Training</strong> runs realistic simulations, tracks who clicks, and auto-assigns training. The results are the artefact the insurer wants. Plan 1 has no mechanism for this.</p>

<h2 id="the-concrete-gaps-cloud-apps-and-oauth">The concrete gaps: cloud apps and OAuth</h2>

<p>Base Entra ID shows OAuth consents in the Enterprise Applications blade. That is where cloud app visibility ends.</p>

<h3 id="shadow-it-discovery">Shadow IT discovery</h3>

<p>Users sign up for SaaS tools with their work email. Some go through SSO; many do not. <strong>Defender for Cloud Apps</strong> discovers the rest by analyzing network traffic against a catalog of over 31,000 cloud apps, each scored on 90+ risk factors.</p>

<h3 id="oauth-app-risk-scoring">OAuth app risk scoring</h3>

<p>An employee consented to an app with <code class="language-plaintext highlighter-rouge">Mail.ReadWrite</code> scope. Is the publisher verified? Is the scope excessive? <strong>DfCA’s app governance</strong> adds risk scoring: publisher verification, scope risk classification, credential hygiene, and behavioral anomaly detection, including app-to-app permissions.</p>

<h3 id="session-policies">Session policies</h3>

<p>A contractor accesses Salesforce from a personal device. You want to allow read access but block downloads. Conditional Access can block or allow the sign-in but cannot control the session. <strong>DfCA session policies</strong> add in-session controls: block downloads, watermark files, prevent copy/paste, all proxied through DfCA without requiring device management.</p>

<h3 id="shadow-ai-detection">Shadow AI detection</h3>

<p>Business Premium already includes <strong>cloud app discovery</strong> through Defender for Business. That discovery already covers the <strong>Generative AI</strong> category - you can see which AI tools are being accessed and how much traffic flows to them. What Business Premium does not give you is the ability to act on it. DfCA adds per-user attribution (who is using which AI tool, not just that someone did), data volume breakdowns, and policy controls to sanction, unsanction, or block specific AI apps. That is the difference between a dashboard that says “ChatGPT was accessed” and a policy that says “block uploads to unsanctioned AI tools for everyone except the data science team.”</p>

<h2 id="the-upgrade-paths">The upgrade paths</h2>

<p>Three paths to close the email and SaaS gap.</p>

<table>
  <thead>
    <tr>
      <th>Path</th>
      <th>Best for</th>
      <th>Includes</th>
      <th>List price</th>
      <th>Main constraint</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Defender Suite for Business Premium</strong></td>
      <td>BP customers under 300 users</td>
      <td>MDO P2, DfCA, MDE P2, DfI, Entra ID P2</td>
      <td><strong>$10/user/mo</strong></td>
      <td>300-user cap</td>
    </tr>
    <tr>
      <td><strong>MDO P2 + DfCA standalone</strong></td>
      <td>Component-by-component buyers</td>
      <td>MDO P2 + DfCA only</td>
      <td><strong>~$5 + ~$3.50/user/mo</strong></td>
      <td>No MDE P2, DfI, or Entra P2</td>
    </tr>
    <tr>
      <td><strong>Microsoft Defender Suite (E5 Security)</strong></td>
      <td>M365 E3 customers</td>
      <td>Everything in Defender Suite for BP + Defender for IoT</td>
      <td><strong>$12/user/mo</strong></td>
      <td>Larger upfront spend</td>
    </tr>
  </tbody>
</table>

<p>Prices are Microsoft list per user per month, as of May 2026. CSP, NCE, and Enterprise Agreement pricing differs.</p>

<p><strong>The bundle wins again.</strong> MDO P2 + DfCA standalone costs ~$8.50/user/month for email and SaaS only. For $10/user/month the Defender Suite adds MDE P2, DfI, and Entra ID P2. Unless the customer needs only one component, the bundle is the better path.</p>

<h3 id="july-2026-note-for-e3-customers">July 2026 note for E3 customers</h3>

<p>After July 2026, E3 includes MDO P1. The email gap narrows to the P2 delta (Threat Explorer, AIR, Attack Simulation Training). The MDO P2 upgrade path stays the same.</p>

<h2 id="what-you-unlock">What you unlock</h2>

<table>
  <thead>
    <tr>
      <th>Capability</th>
      <th>What changes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Threat Explorer</strong></td>
      <td>30-day searchable email history by sender, URL, attachment, or delivery action.</td>
    </tr>
    <tr>
      <td><strong>AIR</strong></td>
      <td>Playbook-driven investigation with remediation actions you review and approve.</td>
    </tr>
    <tr>
      <td><strong>Campaign Views</strong></td>
      <td>Related phishing attempts grouped into campaigns across all affected users.</td>
    </tr>
    <tr>
      <td><strong>Attack Simulation Training</strong></td>
      <td>Phishing simulations with click rates and auto-assigned training.</td>
    </tr>
    <tr>
      <td><strong>Shadow IT discovery</strong></td>
      <td>31,000+ cataloged apps scored on 90+ risk factors.</td>
    </tr>
    <tr>
      <td><strong>OAuth app governance</strong></td>
      <td>Publisher verification, scope risk, credential hygiene, behavioral anomalies.</td>
    </tr>
    <tr>
      <td><strong>Session policies</strong></td>
      <td>In-session controls on third-party SaaS without device management.</td>
    </tr>
    <tr>
      <td><strong>Shadow AI detection</strong></td>
      <td>Generative AI category with per-user attribution, data volume, and sanction controls.</td>
    </tr>
  </tbody>
</table>

<p><strong>The practical difference:</strong> the phishing campaign from the opening takes 5 minutes to scope in Threat Explorer instead of hours. AIR recommends soft-deleting messages from all 30 mailboxes. The former MSP’s ticketing tool with <code class="language-plaintext highlighter-rouge">Mail.ReadWrite</code> and <code class="language-plaintext highlighter-rouge">Directory.Read.All</code> gets flagged automatically. Shadow AI tools surface with per-user data volume in the cloud discovery dashboard.</p>

<h2 id="when-not-to-bother">When not to bother</h2>

<p>Not every customer needs MDO P2 and DfCA. Two scenarios where the upgrade will not earn its keep:</p>

<p><strong>Too few mailboxes for Attack Simulation Training (AST).</strong> AST needs enough recipients for a meaningful click rate. A 5-person office does not have the volume for realistic phishing simulations, and the results are not statistically useful. Threat Explorer, AIR, and Campaign Views still add investigation value at any size, but if AST is the main justification for the upgrade, the economics do not work below roughly 25-30 users.</p>

<p><strong>Tight app allow-list, no BYOD, no SaaS sprawl.</strong> If the customer runs a handful of IT-managed apps behind SSO, every device is corporate-managed, and users cannot install their own tools, DfCA’s Shadow IT discovery will mostly confirm what you already know. The Shadow AI angle can still matter if the customer is concerned about browser-based AI tool usage, but the broader OAuth governance and session policy workloads will stay quiet. In that scenario, MDO P2 on its own may be the better scoped purchase.</p>

<h2 id="how-to-pitch-it">How to pitch it</h2>

<p><strong>Post-incident.</strong> <em>“The phishing campaign last month hit 30 mailboxes and took a full day to scope. With Threat Explorer, that is a 5-minute search. AIR automates the investigation and recommends the remediation.”</em></p>

<p><strong>Insurance renewal.</strong> <em>“Your cyber insurance questionnaire asks about phishing simulations. Attack Simulation Training gives you real simulations with click rates and auto-assigned training. That is the artefact the underwriter wants.”</em></p>

<p><strong>Shadow AI.</strong> <em>“Your staff is using AI tools you cannot see from the admin center. DfCA shows which tools, who uses them, and how much data flows. That is the visibility you need before writing an AI usage policy.”</em></p>

<p><strong>OAuth risk.</strong> <em>“You have 40 consented apps in Entra. How many have Mail.ReadWrite from an unverified publisher? DfCA flags the ones you should revoke before they become an incident.”</em></p>

<p>Price it as a managed security service, not a licence passthrough. The $10/user/month Defender Suite for BP is the floor.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Prevention is half the story. MDO P2 adds the investigation layer for email: Threat Explorer, AIR, Campaign Views, and Attack Simulation Training. Defender for Cloud Apps adds the visibility layer for everything outside email: Shadow IT, OAuth governance, session controls, and Shadow AI detection. Both sides close with the same purchase: <strong>Defender Suite for Business Premium</strong> ($10/user/month) or <strong>E5 Security</strong> ($12/user/month).</p>

<p>After July 2026, E3 gains MDO P1 in the base subscription. The email exposure gap narrows to the P2 delta. The cloud app gap remains unchanged.</p>

<p>Start with one customer tenant. Run a Threat Explorer search for a known indicator, connect DfCA’s cloud discovery, and review the findings. Then scale out.</p>

<table>
  <thead>
    <tr>
      <th>Resource</th>
      <th>What it covers</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/defender-office-365/mdo-about">MDO Plan 1 vs Plan 2 cheat sheet</a></td>
      <td>Feature comparison between EOP, P1, and P2</td>
    </tr>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/defender-office-365/threat-explorer-real-time-detections-about">Threat Explorer overview</a></td>
      <td>Post-delivery email search and investigation</td>
    </tr>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/defender-office-365/attack-simulation-training-get-started">Attack Simulation Training</a></td>
      <td>Phishing simulation setup and management</td>
    </tr>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/defender-cloud-apps/what-is-defender-for-cloud-apps">Defender for Cloud Apps overview</a></td>
      <td>Shadow IT, OAuth governance, session policies</td>
    </tr>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/defender-cloud-apps/risk-score">Cloud app catalog and risk scores</a></td>
      <td>31,000+ apps, 90+ risk factors, Generative AI category</td>
    </tr>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/microsoft-365/admin/security-and-compliance/add-defender-suite-business-premium">Defender Suite for Business Premium</a></td>
      <td>Bundle contents and pricing</td>
    </tr>
    <tr>
      <td><a href="https://www.microsoft.com/en-us/licensing/news/2026-M365-Packaging-Pricing-Updates">July 2026 packaging update</a></td>
      <td>MDO P1 in E3, Intune additions, price changes</td>
    </tr>
  </tbody>
</table>

<hr />

<p><em>Part of <a href="/posts/msp-license-ladder-01-hunting-gap/">The MSP License Ladder</a> series. Previous: <a href="/posts/msp-license-ladder-03-identity-gap/">#3, The Identity Gap</a>.</em></p>

<p><em>Last verified: May 2026</em></p>

<h2 id="related">Related</h2>

<ul>
  <li><a href="/posts/msp-license-ladder-01-hunting-gap/">The MSP License Ladder #1: The Hunting Gap</a> – what Defender for Endpoint Plan 2 unlocks for proactive threat hunting across your customer fleet.</li>
  <li><a href="/posts/msp-license-ladder-02-data-gap/">The MSP License Ladder #2: The Data Gap</a> – what Purview Suite unlocks over base Business Premium and E3 for DLP, Insider Risk, and eDiscovery.</li>
  <li><a href="/posts/msp-license-ladder-03-identity-gap/">The MSP License Ladder #3: The Identity Gap</a> – what Entra ID P2 and Defender for Identity unlock for risk-based CA, just-in-time privilege, and on-prem AD visibility.</li>
  <li><a href="/posts/msp-license-ladder-05-endpoint-gap/">The MSP License Ladder #5: The Endpoint Gap</a> – what the Intune Suite unlocks for least-privilege elevation, third-party app patching, and device health analytics.</li>
</ul>]]></content>

      
      
      <author>
          <name>Roy Klooster</name>
      </author>

      
        <category term="Defender" />
      
        <category term="Security" />
      
        <category term="M365" />
      

      
      
        <summary type="html"><![CDATA[Base SKUs prevent threats but cannot measure exposure. MDO P2 adds post-delivery investigation across email and Teams. Defender for Cloud Apps reveals the cloud apps, OAuth consents, and AI tools you cannot see from the admin center.]]></summary>
      
    </entry>
  
    <entry>
      

      <title type="html">The MSP License Ladder #3: The Identity Gap</title>
      <link href="https://rksolutions.nl/posts/msp-license-ladder-03-identity-gap/" rel="alternate" type="text/html" title="The MSP License Ladder #3: The Identity Gap" />
      <published>2026-05-05T00:00:00+00:00</published>
      <updated>2026-05-05T00:00:00+00:00</updated>
      <id>https://rksolutions.nl/posts/msp-license-ladder-03-identity-gap</id>
      <content type="html" xml:base="https://rksolutions.nl/posts/msp-license-ladder-03-identity-gap/"><![CDATA[<p>You are onboarding a new customer and running through their configuration. Fifteen admin accounts, all with permanent Global Admin. No approval workflow, no activation window, no audit trail. The customer’s previous MSP set it up three years ago and nobody has touched it since. You know this is a risk, but on Entra ID P1 there is no mechanism to fix it without removing the role entirely.</p>

<p>The same customer has four Domain Controllers running on-prem. You check the Defender portal and there is nothing. No identity alerts, no lateral movement paths, no credential theft detections. Not because everything is fine, but because nothing is watching.</p>

<p>Two blind spots, one cloud and one on-prem. That is the identity gap, and the licensing path to closing both sides is usually the same bundle.</p>

<p>This post is part of <strong>The MSP License Ladder</strong>, a series on what Business Premium and E3 don’t give you, and how to build on top of them. Every post covers one gap: what’s missing from the base SKU, what it costs to close, and what you can actually build with it. Previous: <a href="/posts/msp-license-ladder-02-data-gap/">#2, The Data Gap</a>.</p>

<p>A few terms up front:</p>

<ul>
  <li><strong>PIM</strong> (Privileged Identity Management): just-in-time role activation with approval workflows and audit trails. Admins request elevation, use it, and it expires automatically.</li>
  <li><strong>Identity Protection</strong>: Microsoft’s risk engine that scores every sign-in and user for anomalies (unfamiliar location, impossible travel, leaked credentials) and feeds those scores into Conditional Access.</li>
  <li><strong>DfI</strong> (Defender for Identity): an identity threat detection solution that installs lightweight sensors on Domain Controllers to detect on-prem attacks like lateral movement, Kerberoasting, and DCSync.</li>
  <li><strong>CA</strong> (Conditional Access): policy engine that controls access based on conditions like location, device compliance, group membership, and (with P2) risk level.</li>
</ul>

<p><strong>Table of Contents</strong></p>

<ul>
  <li><a href="#what-you-actually-get-today">What you actually get today</a></li>
  <li><a href="#the-concrete-gaps-cloud-identity">The concrete gaps: cloud identity</a></li>
  <li><a href="#the-concrete-gaps-on-prem-identity">The concrete gaps: on-prem identity</a></li>
  <li><a href="#the-upgrade-paths">The upgrade paths</a></li>
  <li><a href="#what-you-unlock">What you unlock</a></li>
  <li><a href="#when-not-to-bother">When not to bother</a></li>
  <li><a href="#how-to-pitch-it">How to pitch it</a></li>
  <li><a href="#conclusion">Conclusion</a></li>
</ul>

<h2 id="what-you-actually-get-today">What you actually get today</h2>

<p><strong>Business Premium</strong> and <strong>Microsoft 365 E3</strong> both include <strong>Entra ID P1</strong>. That gives you more than most customers realise:</p>

<ul>
  <li><strong>Conditional Access</strong> with conditions for location, device compliance, client app, and group membership</li>
  <li><strong>MFA</strong> with all verification methods (Authenticator, phone, SMS, FIDO2, certificate-based)</li>
  <li><strong>Self-service password reset</strong> with on-prem writeback</li>
  <li><strong>Application Proxy</strong> for publishing on-prem web apps without a VPN</li>
  <li><strong>Custom roles</strong> and <strong>Administrative Units</strong> for scoped delegation</li>
  <li><strong>Hybrid identity</strong> via Entra Connect or Cloud Sync</li>
</ul>

<p>For most customer conversations, P1 sounds like it covers identity. The gaps only show up when you need identity to <strong>react to risk</strong> (risk-based Conditional Access), <strong>govern privilege</strong> (PIM, Access Reviews), or <strong>see on-prem AD</strong> (Defender for Identity). All three sit behind upgrades.</p>

<p>And that last point is worth repeating: <strong>neither base SKU includes any on-prem identity visibility.</strong> Entra ID sees cloud sign-ins. If the customer has Domain Controllers, the entire on-prem identity layer is invisible to the Defender portal without Defender for Identity. <strong>Defender for Business</strong>, which ships with Business Premium, is an endpoint security solution (NGAV, basic EDR, ASR). It does not cover identity on Domain Controllers. That requires a separate product.</p>

<h2 id="the-concrete-gaps-cloud-identity">The concrete gaps: cloud identity</h2>

<p>Four scenarios that come up on real customer calls and leave you reaching for something you do not have.</p>

<h3 id="risk-based-conditional-access">Risk-based Conditional Access</h3>

<p>A user signs in from their regular laptop at 3am from a country they have never visited. On P1, you can write a CA policy that blocks that country entirely, but you cannot distinguish “same user, anomalous pattern” from “stolen credential on a compliant device.” <strong>Risk-based Conditional Access</strong> uses Identity Protection’s sign-in risk and user risk scores to make that distinction. The flexibility goes beyond just MFA:</p>

<ul>
  <li>A medium-risk sign-in can trigger <strong>step-up MFA</strong></li>
  <li>A high-risk user can be forced to <strong>reset their password</strong></li>
  <li>A high-risk sign-in from an admin account can <strong>block access entirely</strong></li>
  <li>You can combine risk with other conditions: block high-risk sign-ins to sensitive apps, require compliant devices for medium-risk, and let low-risk through with MFA</li>
</ul>

<p>That last point matters. For admin accounts, you can write a CA policy that says “if the sign-in risk is high, block access, no exceptions.” That is a hard stop on compromised admin credentials. On Entra ID P1, those risk conditions do not exist in the CA policy editor.</p>

<h3 id="privileged-identity-management">Privileged Identity Management</h3>

<p>Every admin account with permanent Global Admin, Exchange Admin, or Intune Admin has standing privilege 24/7. If an account is compromised, the attacker gets the role immediately and fully. <strong>PIM</strong> replaces standing privilege with just-in-time activation: an admin requests the role, optionally gets approval, activates it for a bounded window (say 4 hours), and then the role expires. Every activation is logged with justification, approver, and duration. On Entra ID P1, PIM does not exist. Every admin keeps their role permanently.</p>

<p><strong>PIM for Groups</strong> extends the same just-in-time model to security group and Microsoft 365 group membership. An engineer can be eligible for a group that grants access to a sensitive SharePoint site or an Azure Key Vault, and activate membership only when needed.</p>

<h3 id="access-reviews">Access Reviews</h3>

<p>Over time, group memberships drift. The contractor who joined the “Finance - Sensitive Data” group for a three-month project is still a member two years later. <strong>Access Reviews</strong> create periodic review workflows where group owners or managers confirm that each member still needs access. Members who are not confirmed are automatically removed. This works for group memberships, role assignments, app access, and guest accounts. On Entra ID P1, there is no mechanism for this. Membership cleanup is manual and ad-hoc.</p>

<h3 id="entitlement-management">Entitlement Management</h3>

<p>A new team member needs access to four groups, two apps, and a SharePoint site. Today that is four separate requests through different channels. <strong>Entitlement Management</strong> bundles those into an access package: one request, one approval workflow, one expiration policy. Users can request access packages through the <strong>My Access</strong> portal, and the entire lifecycle (request, approval, assignment, expiration) is managed in one place. Basic Entitlement Management (access packages, multi-stage approvals, self-service requests) is included in Entra ID P2. Advanced features like auto-assignment policies, custom extensions via Logic Apps, and Lifecycle Workflows require the separate <strong>Entra ID Governance</strong> add-on. On Entra ID P1, none of this exists.</p>

<h2 id="the-concrete-gaps-on-prem-identity">The concrete gaps: on-prem identity</h2>

<p>If your customer has Domain Controllers, the on-prem identity layer is invisible to the Defender portal without <strong>Defender for Identity</strong>. Entra ID sees cloud sign-ins; it does not ingest DC events. <strong>Defender for Business</strong> (included in Business Premium) does not fill this gap either. Defender for Business is focused on endpoint protection: next-generation antivirus, attack surface reduction, and basic EDR. It does not deploy sensors on Domain Controllers and does not monitor Active Directory traffic. Here is what you are missing without DfI:</p>

<h3 id="reconnaissance-detection">Reconnaissance detection</h3>

<p>Before attackers move laterally, they map the environment. DfI detects <strong>account enumeration</strong> via LDAP, <strong>DNS zone transfer attempts</strong>, <strong>SAMR-based user and group discovery</strong>, and <strong>security principal reconnaissance</strong> that often precedes a Kerberoasting attack. It also triggers on any activity targeting <strong>honeytoken accounts</strong> you have configured as decoys.</p>

<h3 id="credential-theft-attacks">Credential theft attacks</h3>

<p><strong>Kerberoasting</strong> (requesting service tickets for offline cracking), <strong>AS-REP roasting</strong> (targeting accounts without pre-authentication), and <strong>DCSync</strong> (requesting AD replication data to extract password hashes). DfI also detects <strong>brute force attacks</strong> over Kerberos and NTLM, <strong>DPAPI master key requests</strong> (used to decrypt saved credentials), <strong>DFSCoerce</strong> attacks, and <strong>shadow credential</strong> abuse. These are the attacks that turn a foothold into full domain compromise.</p>

<h3 id="lateral-movement-and-domain-dominance">Lateral movement and domain dominance</h3>

<p><strong>Pass-the-hash</strong>, <strong>pass-the-ticket</strong>, and <strong>overpass-the-hash</strong> are the classic lateral movement techniques. DfI detects all three, plus <strong>NTLM relay attacks</strong>, <strong>PrintNightmare exploitation</strong>, <strong>SMB packet manipulation</strong>, and <strong>Exchange Server remote code execution</strong> attempts. For domain dominance, DfI covers <strong>Golden Ticket</strong> usage (five different detection methods, including encryption downgrade, time anomaly, and RBCD-based), <strong>DCShadow</strong> (rogue domain controller promotion and replication), <strong>Skeleton Key</strong> malware, and <strong>SID-History injection</strong>.</p>

<h3 id="active-directory-infrastructure-attacks">Active Directory infrastructure attacks</h3>

<p>DfI monitors beyond user activity. It detects suspicious modifications to <strong>AdminSdHolder</strong> (the security descriptor that protects privileged accounts), <strong>AD Certificate Services</strong> abuse (ESC8 attacks, certificate database deletions, audit filter tampering), <strong>AD FS trust relationship modifications</strong>, and <strong>Resource-Based Constrained Delegation</strong> abuse. It also flags <strong>Group Policy tampering</strong> that disables Windows Defender, a common precursor to ransomware deployment.</p>

<h3 id="legacy-protocol-usage">Legacy protocol usage</h3>

<p>NTLM authentication, LDAP cleartext binds, and SMBv1 are still active in 2026 more often than anyone likes to admit. DfI surfaces this usage through its <strong>identity security posture assessments</strong> in Microsoft Secure Score, so you can plan the remediation instead of discovering it during an incident.</p>

<p>On base Entra ID P1, <strong>none of this is visible</strong>. The Defender portal shows endpoint and email telemetry, but the on-prem side of the identity gap is a complete blind spot.</p>

<h2 id="the-upgrade-paths">The upgrade paths</h2>

<p>Three paths to close both gaps, depending on the customer’s base SKU and size.</p>

<table>
  <thead>
    <tr>
      <th>Path</th>
      <th>Best for</th>
      <th>Includes</th>
      <th>List price</th>
      <th>Main constraint</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Defender Suite for Business Premium</strong></td>
      <td>BP customers under 300 users</td>
      <td>Entra ID P2, DfI, MDE P2, MDO P2, DfCA</td>
      <td><strong>$10/user/mo</strong></td>
      <td>300-user cap</td>
    </tr>
    <tr>
      <td><strong>Entra ID P2 + DfI standalone</strong></td>
      <td>Component-by-component buyers</td>
      <td>Entra ID P2 + DfI only</td>
      <td><strong>$9 + $5.50/user/mo</strong></td>
      <td>No MDE P2, MDO P2, or DfCA</td>
    </tr>
    <tr>
      <td><strong>Microsoft Defender Suite (E5 Security)</strong></td>
      <td>M365 E3 customers</td>
      <td>Everything in Defender Suite for BP + Defender for IoT</td>
      <td><strong>$12/user/mo</strong></td>
      <td>Larger upfront spend</td>
    </tr>
  </tbody>
</table>

<p>Prices are Microsoft list per user per month, as of May 2026. CSP, NCE, and Enterprise Agreement pricing differs; check with your licensing partner for the real customer number. Combined with <strong>Purview Suite for Business Premium</strong> (also $10), the bundle drops to <strong>$15/user/month</strong> for both Defender and Purview.</p>

<p><strong>The bundle story is strong here.</strong> Buying Entra ID P2 ($9) and DfI ($5.50) standalone costs $14.50/user/month and only covers identity. For $10/user/month the Defender Suite for BP gives you both plus MDE P2, MDO P2, and DfCA. Unless the customer specifically cannot take the other Defender components (rare), the bundle is the better path.</p>

<h3 id="the-grey-area-licensing-caveat">The grey-area licensing caveat</h3>

<p>Entra ID P2 features follow a <strong>per-user-covered</strong> licensing model, not per-tenant-enabled. This matters:</p>

<ul>
  <li><strong>Identity Protection</strong> risk engine activates tenant-wide once a single P2 license is present. Risk-based CA policies evaluate all sign-ins, not just licensed users. <strong>Compliance-wise, Microsoft requires a P2 license for every user whose sign-ins are evaluated by risk policies.</strong></li>
  <li><strong>PIM</strong> activates tenant-wide. <strong>Compliance-wise, every user with eligible or time-bound role assignments, every approver, and every reviewer needs P2.</strong> If the license expires, eligible assignments are removed and PIM becomes unavailable.</li>
  <li><strong>Access Reviews</strong> follow the same pattern: every user being reviewed and every reviewer needs the license.</li>
</ul>

<p>This is the same grey area covered in <a href="/posts/msp-license-ladder-02-data-gap/">Post #2</a> for Purview. The features work with fewer licenses than compliance requires. Do not let a customer assume one P2 license turns it on for the tenant.</p>

<h3 id="entra-id-governance-a-separate-sku">Entra ID Governance: a separate SKU</h3>

<p>If the customer’s governance needs go beyond basic PIM and Access Reviews, there is a separate <strong>Entra ID Governance</strong> add-on (~$7/user/month on top of P1 or P2). It adds:</p>

<ul>
  <li><strong>Lifecycle Workflows</strong>: automated onboarding and offboarding (pre-hire provisioning, day-one group assignment, leaver account cleanup). This is the “joiner-mover-leaver” automation that P2 does not cover.</li>
  <li><strong>Advanced Access Reviews</strong>: includes <strong>ML-assisted recommendations</strong> that analyze user-to-group affiliation patterns and flag members who are outliers compared to their peers, so reviewers can focus on the memberships that are most likely stale instead of reviewing every entry manually. Also adds scoping to inactive users and Access Reviews for PIM for Groups.</li>
  <li><strong>Advanced Entitlement Management</strong>: auto-assignment policies (assign access packages based on department or cost center without a request), custom extensions via Logic Apps, Verified ID integration, and separation of duties enforcement.</li>
</ul>

<p>Microsoft has explicitly stated: <strong>no new Identity Governance and Administration features will be added to the P2 SKU.</strong> Current P2 features (PIM, basic Access Reviews, basic Entitlement Management) stay, but everything new goes into the Governance add-on. If a customer is building a full identity governance program, budget for it.</p>

<p>For more detail, see the <a href="https://learn.microsoft.com/en-us/entra/id-governance/licensing-fundamentals">Entra ID Governance licensing fundamentals</a>.</p>

<h2 id="what-you-unlock">What you unlock</h2>

<p>With Entra ID P2 and Defender for Identity in place, outcomes across both domains:</p>

<table>
  <thead>
    <tr>
      <th>Capability</th>
      <th>What changes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Risk-based Conditional Access</strong></td>
      <td>Sign-in risk and user risk as CA conditions. Step-up MFA for medium risk, password reset for high-risk users, full access block for high-risk admin sign-ins.</td>
    </tr>
    <tr>
      <td><strong>PIM for roles and groups</strong></td>
      <td>Just-in-time activation for Entra roles, Azure roles, and group membership. No standing privilege. Full audit trail.</td>
    </tr>
    <tr>
      <td><strong>Access Reviews</strong></td>
      <td>Periodic review workflows for group membership, role assignments, app access, and guest accounts. Automatic removal of unconfirmed members.</td>
    </tr>
    <tr>
      <td><strong>Entitlement Management</strong></td>
      <td>Access packages with request, approval, and expiration workflows. Self-service via My Access portal.</td>
    </tr>
    <tr>
      <td><strong>Identity Protection signals</strong></td>
      <td>Risky users report, risky sign-ins report, risk detections, weekly digest, and Graph API access to all risk data.</td>
    </tr>
    <tr>
      <td><strong>DfI on-prem detections</strong></td>
      <td>40+ detection types across reconnaissance, credential theft, lateral movement, and domain dominance.</td>
    </tr>
    <tr>
      <td><strong>Unified incidents</strong></td>
      <td>On-prem identity alerts from DfI correlate with cloud identity and endpoint signals in Defender XDR. One incident, full story.</td>
    </tr>
    <tr>
      <td><strong>Lateral movement path mapping</strong></td>
      <td>DfI builds a map of how an attacker could traverse the environment from a compromised account to sensitive targets.</td>
    </tr>
    <tr>
      <td><strong>Identity posture assessments</strong></td>
      <td>DfI feeds identity security recommendations into Microsoft Secure Score, surfacing legacy protocols and risky configurations.</td>
    </tr>
  </tbody>
</table>

<p><strong>The practical difference:</strong> the admin accounts from the opening of this post activate Global Admin through PIM for 2 hours with approval and justification, then the role expires. Access Reviews run quarterly to confirm who still needs admin eligibility. The DCs now have DfI sensors, and a pass-the-hash attempt triggers an alert in the Defender portal within minutes, correlated with the endpoint telemetry from MDE. You see both the cloud and on-prem sides of identity in one place.</p>

<h2 id="when-not-to-bother">When not to bother</h2>

<p><strong>No Domain Controllers? Skip Defender for Identity.</strong> If the customer is pure-cloud, Defender for Identity does nothing. No DCs, no sensors, no detections. The identity gap for cloud-only tenants is entirely Entra ID P2. If DfI comes along as part of the Defender Suite bundle, fine, but never sell it standalone to a cloud-only customer.</p>

<p><strong>DfI standalone is almost never the right buy.</strong> At $5.50/user/month it covers one gap. The Defender Suite for BP at $10/user/month covers five. Pitch the bundle.</p>

<h2 id="how-to-pitch-it">How to pitch it</h2>

<p>Focus on addressing the customer’s specific risks rather than listing features.</p>

<p><strong>Post-audit finding.</strong> <em>“Your current setup has fifteen accounts with permanent Global Admin and no review process. PIM replaces standing privilege with just-in-time activation: the admin requests the role, it’s approved, and it expires in four hours. Access Reviews run quarterly to confirm who still needs eligibility. Every activation and review decision is logged. That closes the audit finding and gives you an ongoing governance trail.”</em></p>

<p><strong>Regulatory driver.</strong> <em>“NIS2 Article 21 requires access control measures including privilege management. DORA Article 9 requires identity and access management. ISO 27001 A.9 covers privileged access. Risk-based CA and PIM satisfy all three. The audit artefact is the PIM activation log and the Identity Protection risk report.”</em></p>

<p><strong>On-prem identity.</strong> <em>“You have four DCs. Right now, a Kerberoasting attempt or a DCSync attack generates zero signal in the Defender portal. Defender for Identity puts lightweight sensors on those DCs and feeds alerts into the same incident view as your endpoint and email telemetry. It detects over 40 attack types, from reconnaissance all the way to domain dominance. It’s the cheapest visibility upgrade on the entire ladder.”</em></p>

<p>Price it as a managed identity service, not a licence passthrough. The $10/user/month Defender Suite for BP is the floor. The value is the PIM configuration, the CA policies tuned to risk, the DfI deployment across customer DCs, and the triage capacity behind the alerts.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Entra ID P1 covers the fundamentals: Conditional Access, MFA, hybrid identity. It is a solid baseline. But three gaps remain:</p>

<ul>
  <li><strong>It cannot react to risk.</strong> Sign-in anomalies, leaked credentials, and impossible travel generate no signal in P1. Risk-based Conditional Access in P2 closes this gap, with the flexibility to step up MFA, force password resets, or block access entirely depending on the risk level and the user’s role.</li>
  <li><strong>It cannot govern privilege.</strong> Every admin keeps their role permanently. PIM in P2 replaces standing privilege with just-in-time activation. Access Reviews add periodic confirmation that memberships and role assignments are still justified.</li>
  <li><strong>It cannot see on-prem AD.</strong> Domain Controllers are invisible to the Defender portal without Defender for Identity. DfI adds 40+ detection types covering the full attack chain from reconnaissance through domain dominance.</li>
</ul>

<p>Both the cloud and on-prem identity gaps usually get closed with the same purchase. The <strong>Defender Suite for Business Premium</strong> ($10/user/month) and the <strong>E5 Security add-on</strong> ($12/user/month) both include Entra ID P2 and Defender for Identity alongside the other Defender components.</p>

<p>Start with one customer tenant. Deploy PIM for the admin accounts, enable a risk-based CA policy in report-only mode, install DfI sensors on the DCs, and run an Access Review on the most sensitive groups. Review the output for a week. Then scale out.</p>

<table>
  <thead>
    <tr>
      <th>Resource</th>
      <th>What it covers</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/entra/fundamentals/licensing">Entra ID licensing overview</a></td>
      <td>P1 vs P2 vs Governance feature matrix</td>
    </tr>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/entra/id-protection/overview-identity-protection">Identity Protection overview</a></td>
      <td>Risk detections, risk policies, and remediation</td>
    </tr>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-configure">PIM overview</a></td>
      <td>Just-in-time role activation, approval workflows</td>
    </tr>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/concept-pim-for-groups">PIM for Groups</a></td>
      <td>Just-in-time group membership and ownership</td>
    </tr>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/defender-for-identity/what-is">Defender for Identity overview</a></td>
      <td>On-prem identity detection capabilities</td>
    </tr>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/defender-for-identity/alerts-overview">DfI security alerts reference</a></td>
      <td>Full list of 40+ detection types by attack phase</td>
    </tr>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/microsoft-365/admin/security-and-compliance/add-defender-suite-business-premium">Defender Suite for Business Premium</a></td>
      <td>Bundle contents and pricing</td>
    </tr>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/entra/id-governance/licensing-fundamentals">Entra ID Governance licensing</a></td>
      <td>P2 vs Governance add-on scope</td>
    </tr>
    <tr>
      <td><a href="/posts/how-to-secure-your-enterprise-application-via-pim/">Securing Your Enterprise Application via PIM</a></td>
      <td>Deep dive on PIM setup from this blog</td>
    </tr>
  </tbody>
</table>

<p>Next up: <strong><a href="/posts/msp-license-ladder-04-exposure-gap/">#4, The Exposure Gap</a></strong>, on what MDO Plan 2 and Defender for Cloud Apps unlock for post-delivery email investigation, OAuth governance, and shadow IT discovery.</p>

<hr />

<p><em>Part of <a href="/posts/msp-license-ladder-01-hunting-gap/">The MSP License Ladder</a> series. Previous: <a href="/posts/msp-license-ladder-02-data-gap/">#2, The Data Gap</a>. Next: <a href="/posts/msp-license-ladder-04-exposure-gap/">#4, The Exposure Gap</a>.</em></p>

<p><em>Last verified: May 2026</em></p>

<h2 id="related">Related</h2>

<ul>
  <li><a href="/posts/msp-license-ladder-01-hunting-gap/">The MSP License Ladder #1: The Hunting Gap</a> – what Defender for Endpoint Plan 2 unlocks for proactive threat hunting across your customer fleet.</li>
  <li><a href="/posts/msp-license-ladder-02-data-gap/">The MSP License Ladder #2: The Data Gap</a> – what Purview Suite unlocks over base Business Premium and E3 for DLP, Insider Risk, and eDiscovery.</li>
  <li><a href="/posts/msp-license-ladder-04-exposure-gap/">The MSP License Ladder #4: The Exposure Gap</a> – what MDO P2 and Defender for Cloud Apps unlock for post-delivery email investigation and shadow IT discovery.</li>
  <li><a href="/posts/msp-license-ladder-05-endpoint-gap/">The MSP License Ladder #5: The Endpoint Gap</a> – what the Intune Suite unlocks for least-privilege elevation, third-party app patching, and device health analytics.</li>
  <li><a href="/posts/how-to-secure-your-enterprise-application-via-pim/">How to Secure Your Enterprise Application via PIM</a> – detailed walkthrough of PIM configuration for enterprise apps.</li>
</ul>]]></content>

      
      
      <author>
          <name>Roy Klooster</name>
      </author>

      
        <category term="Entra ID" />
      
        <category term="Graph API" />
      
        <category term="Security" />
      
        <category term="M365" />
      

      
      
        <summary type="html"><![CDATA[Entra ID P1 covers MFA and Conditional Access. P2 adds risk-based decisions, PIM, and Access Reviews. Defender for Identity closes the on-prem AD gap.]]></summary>
      
    </entry>
  
    <entry>
      

      <title type="html">The MSP License Ladder #2: The Data Gap</title>
      <link href="https://rksolutions.nl/posts/msp-license-ladder-02-data-gap/" rel="alternate" type="text/html" title="The MSP License Ladder #2: The Data Gap" />
      <published>2026-04-25T00:00:00+00:00</published>
      <updated>2026-04-25T00:00:00+00:00</updated>
      <id>https://rksolutions.nl/posts/msp-license-ladder-02-data-gap</id>
      <content type="html" xml:base="https://rksolutions.nl/posts/msp-license-ladder-02-data-gap/"><![CDATA[<p>A team lead hands in their notice on Friday. By Monday morning they have downloaded three months of client proposals from SharePoint, forwarded a pricing sheet to a personal Gmail, and copied the customer database to a USB stick. You find out two weeks later when the new employer starts undercutting your customer’s bids.</p>

<p>On <strong>Business Premium</strong> and <strong>Microsoft 365 E3</strong>, you had no signal for any of that. No alert when the download volume spiked. No block when the USB was mounted. No flag when the email left the tenant. That is the gap this post is about: base Purview covers more than people expect, but the moment data leaves email and files, you are blind.</p>

<p>This post is part of <strong>The MSP License Ladder</strong>, a series on what Business Premium and E3 don’t give you, and how to build on top of them. Every post covers one gap: what’s missing from the base SKU, what it costs to close, and what you can actually build with it. Previous: <a href="/posts/msp-license-ladder-01-hunting-gap/">#1, The Hunting Gap</a>.</p>

<p>A few terms up front:</p>

<ul>
  <li><strong>DLP</strong> (Data Loss Prevention): policies that detect and protect sensitive content based on patterns, sensitive information types, or trainable classifiers.</li>
  <li><strong>SIT</strong> (Sensitive Information Type): a pattern definition (regex, keyword list, or function) that Purview uses to identify sensitive data. Examples: credit card numbers, national IDs, IBAN numbers.</li>
  <li><strong>IRM</strong> (Insider Risk Management): behavioural analytics that detect risky user activity patterns like unusual download spikes, data exfiltration sequences, or policy violations.</li>
</ul>

<p><strong>Table of Contents</strong></p>

<ul>
  <li><a href="#what-you-actually-get-today">What you actually get today</a></li>
  <li><a href="#the-concrete-gaps">The concrete gaps</a>
    <ul>
      <li><a href="#dlp-scope">DLP scope</a></li>
      <li><a href="#insider-risk-management">Insider Risk Management</a></li>
      <li><a href="#ediscovery-premium">eDiscovery Premium</a></li>
      <li><a href="#communication-compliance">Communication Compliance</a></li>
      <li><a href="#records-management">Records Management</a></li>
    </ul>
  </li>
  <li><a href="#the-upgrade-paths">The upgrade paths</a></li>
  <li><a href="#what-you-unlock">What you unlock</a></li>
  <li><a href="#how-to-deploy-this-across-your-customer-base">How to deploy this across your customer base</a>
    <ul>
      <li><a href="#step-1-build-the-baseline-in-your-own-tenant">Step 1: Build the baseline in your own tenant</a></li>
      <li><a href="#step-2-deploy-across-tenants">Step 2: Deploy across tenants</a></li>
      <li><a href="#step-3-map-the-output-to-frameworks">Step 3: Map the output to frameworks</a></li>
    </ul>
  </li>
  <li><a href="#when-not-to-bother">When not to bother</a></li>
  <li><a href="#how-to-pitch-it">How to pitch it</a></li>
  <li><a href="#conclusion">Conclusion</a></li>
</ul>

<h2 id="what-you-actually-get-today">What you actually get today</h2>

<p>Before jumping to what is missing, be honest about what the base gives you. <strong>Both SKUs include more Purview than most customers assume.</strong></p>

<p><strong>Business Premium</strong> ships with <strong>Purview Information Protection</strong> and <strong>Purview DLP</strong>. You can create sensitivity labels (manual application), build DLP policies that scan Exchange, SharePoint, and OneDrive, and set basic retention policies. That is real data governance. If a user attaches a spreadsheet with 50 credit card numbers to an email, base DLP can catch it.</p>

<p><strong>Microsoft 365 E3</strong> has a similar shape. Manual sensitivity labels, DLP for email and files, basic retention and data lifecycle management. Slightly broader admin tooling but the same ceiling on scope.</p>

<p>The gap is not “you have nothing.” <strong>The gap is scope, automation, and workflow tooling.</strong> Here is what that looks like side by side:</p>

<table>
  <thead>
    <tr>
      <th>DLP capability</th>
      <th>Business Premium</th>
      <th>E3</th>
      <th>Purview Suite add-on</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Sensitivity labels (manual)</td>
      <td>✅</td>
      <td>✅</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>Sensitivity labels (auto-labelling)</td>
      <td>❌</td>
      <td>❌</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>DLP for Exchange</td>
      <td>✅</td>
      <td>✅</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>DLP for SharePoint and OneDrive</td>
      <td>✅</td>
      <td>✅</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>DLP for Teams chat and channels</td>
      <td>⚠️*</td>
      <td>⚠️*</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>DLP for endpoints (USB, clipboard, print)</td>
      <td>❌</td>
      <td>❌</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>DLP for third-party cloud apps</td>
      <td>❌</td>
      <td>❌</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>DLP for browsers (Edge, Chrome)</td>
      <td>❌</td>
      <td>❌</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>Insider Risk Management</td>
      <td>❌</td>
      <td>❌</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>Adaptive Protection (dynamic DLP)</td>
      <td>❌</td>
      <td>❌</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>Litigation Hold</td>
      <td>✅</td>
      <td>✅</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>eDiscovery Standard</td>
      <td>✅</td>
      <td>✅</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>eDiscovery Premium</td>
      <td>❌</td>
      <td>❌</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>Communication Compliance</td>
      <td>❌</td>
      <td>❌</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>Records Management (file plan, event-based)</td>
      <td>❌</td>
      <td>❌</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>Audit Standard</td>
      <td>✅</td>
      <td>✅</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>Audit Premium</td>
      <td>❌</td>
      <td>❌</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>Compliance Manager</td>
      <td>Limited</td>
      <td>Limited</td>
      <td>Full</td>
    </tr>
    <tr>
      <td>Copilot-aware DLP policies</td>
      <td>⚠️**</td>
      <td>⚠️**</td>
      <td>✅</td>
    </tr>
  </tbody>
</table>

<p>*Teams DLP is a grey area on both Business Premium and E3. The Purview portal lets you enable “Teams chat and channel messages” as a DLP location, and the policy will enforce. But <a href="https://learn.microsoft.com/en-us/purview/dlp-microsoft-teams">Microsoft’s licensing docs</a> state that DLP for Teams chat requires E5 or equivalent. Microsoft’s <a href="https://www.microsoft.com/licensing/guidance/Microsoft-Purview">Purview Licensing Guidance</a> is explicit about why:</p>

<blockquote>
  <p><em>“Some services check for a license before granting access or allow you to restrict access to licensed users via your admin portal. Others may not enforce this yet but could in the future. You must appropriately license the use of a service regardless of technical enforcement.”</em></p>
</blockquote>

<p>This is the same per-user-covered pattern as Entra ID P2: the feature activates tenant-wide, but compliance requires every scoped user to be licensed. If you rely on it without the E5 licence, an audit could flag it. Files shared in Teams are a different story: those live in SharePoint/OneDrive and are covered by base DLP regardless.</p>

<p>**Copilot DLP: if users on Business Premium or E3 have a <strong>Microsoft 365 Copilot</strong> license, they get DLP for the M365 Copilot location (prompt safeguarding). The full Copilot-aware DLP (restricting Copilot from processing sensitive files and emails) requires the Purview Suite add-on.</p>

<p>Base DLP covers email and files. Teams chat DLP works in practice on Business Premium and E3 but is not officially licensed there. Endpoints, third-party cloud apps, and browsers are firmly out of scope. Auto-labelling does not exist. Insider Risk Management, eDiscovery Premium, Communication Compliance, and Records Management are all locked behind the next tier.</p>

<p>For the full breakdown of what each SKU includes, see the <a href="https://learn.microsoft.com/en-us/office365/servicedescriptions/microsoft-365-service-descriptions/microsoft-365-tenantlevel-services-licensing-guidance/microsoft-purview-service-description">Microsoft Purview service description</a>.</p>

<h2 id="the-concrete-gaps">The concrete gaps</h2>

<p>Five scenarios that come up on real customer calls and leave you reaching for something you do not have.</p>

<h3 id="dlp-scope">DLP scope</h3>

<p>Your DLP policies scan email and files in Exchange, SharePoint, and OneDrive. An employee pastes a client’s bank details into a Teams chat. Another copies a PII spreadsheet to a USB stick. A third uploads customer data to a personal Dropbox via the browser. <strong>Base Purview does not see any of it.</strong> Teams chat DLP, endpoint DLP, and cloud app DLP are all out of scope on Business Premium and E3.</p>

<p><strong>Auto-labelling</strong> is the other half of this gap. On base SKUs, sensitivity labels are <strong>manual only</strong>. If users forget to label (and they will), the data sits unclassified and unprotected. Auto-labelling based on content inspection closes that loop.</p>

<p><strong>A note on USB controls via Intune.</strong> You do not need Purview Suite to block USB writes entirely. Intune offers three options on its own:</p>

<table>
  <thead>
    <tr>
      <th>Control</th>
      <th>What it does</th>
      <th>Licensing</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Removable Disk: Deny write access</strong></td>
      <td>Blanket block on all USB/removable storage writes</td>
      <td>Intune Plan 1 (base BP/E3)</td>
    </tr>
    <tr>
      <td><strong>Deny write access to removable drives not protected by BitLocker</strong></td>
      <td>Blocks writes unless the USB is BitLocker-encrypted</td>
      <td>Intune Plan 1 (base BP/E3)</td>
    </tr>
    <tr>
      <td><strong>Defender for Endpoint Device Control</strong></td>
      <td>Granular: allow/block/audit per device vendor, user group, or USB class</td>
      <td>Requires MDE onboarding</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>These are <strong>not DLP features</strong>. They do not inspect content. They are blanket or device-level blocks that only apply to <strong>Intune-managed devices</strong>. Unmanaged devices, BYOD, and anything not enrolled are unaffected. Endpoint DLP in Purview Suite is the content-aware layer: it knows what is in the file and decides based on sensitivity, not just the destination.</p>
</blockquote>

<h3 id="insider-risk-management">Insider Risk Management</h3>

<p>A departing employee downloads 50% more files than usual in their last two weeks. A contractor accesses a SharePoint site they have never touched before and exports its contents. On base Purview, <strong>these patterns generate no signal</strong>. <strong>Insider Risk Management</strong> uses behavioural analytics to detect sequences like download spikes, access anomalies, and data exfiltration patterns. It can feed into <strong>Adaptive Protection</strong>, which automatically tightens DLP policies for risky users.</p>

<p>Worth noting: if a leaver is smart, they steal data before handing in their notice, not after. IRM handles this because you can configure the policy timeframe to look back at <strong>up to 90 days of past behaviour</strong>, surfacing risky activity sequences that happened well before HR was notified. None of this exists below Purview Suite.</p>

<h3 id="ediscovery-premium">eDiscovery Premium</h3>

<p>A legal hold needs to include Teams chat, not just mailboxes and SharePoint. A regulatory investigation requires custodian management, advanced review sets, and conversation threading. On base eDiscovery (Standard), you can place holds on mailboxes and sites, but Teams chat coverage, custodian workflows, review sets with analytics, and predictive coding are all Premium features. <strong>When the legal team calls, you either have them or you don’t.</strong></p>

<h3 id="communication-compliance">Communication Compliance</h3>

<p>Regulated sectors (financial services under DORA, healthcare, legal) have obligations to review communications for insider trading language, conflicts of interest, harassment, or regulatory violations. On base Purview there is no mechanism to scan Teams messages, email, or Viva Engage at scale for these patterns. <strong>Communication Compliance</strong> provides policy-based scanning with built-in classifiers for threat, harassment, discrimination, and regulatory language. Without it, the compliance team is relying on manual spot checks.</p>

<h3 id="records-management">Records Management</h3>

<p>Base retention policies are time-based: keep everything for X years, then delete. Real records management needs a file plan, event-based retention (start the clock when a contract ends, not when the file was created), and disposition review before deletion. On base Purview you get the timer. <strong>You do not get the workflow.</strong></p>

<h2 id="the-upgrade-paths">The upgrade paths</h2>

<p><strong>Two paths</strong> to close the data gap, depending on the customer’s base SKU and budget.</p>

<table>
  <thead>
    <tr>
      <th>Path</th>
      <th>Best for</th>
      <th>List price</th>
      <th>Main constraint</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Purview Suite add-on</strong></td>
      <td>BP or E3 customers who need compliance</td>
      <td><strong>$10/user/mo</strong></td>
      <td>300-user cap on BP</td>
    </tr>
    <tr>
      <td><strong>Microsoft 365 E5</strong></td>
      <td>Customers who want security + compliance together</td>
      <td><strong>Full E5 price</strong></td>
      <td>Largest upfront spend</td>
    </tr>
  </tbody>
</table>

<p>Prices are Microsoft list per user per month, as of April 2026. CSP, NCE, and Enterprise Agreement pricing differs. Check with your licensing partner for the real customer number.</p>

<p><strong>The Purview Suite add-on</strong> is available for both Business Premium and E3 customers. It launched September 2025 and adds the full Purview feature set: DLP extended to endpoints, Teams, apps, and cloud; Insider Risk Management; eDiscovery Premium; Records Management; Communication Compliance; Audit Premium; Compliance Manager; Information Barriers; Privileged Access Management; Customer Lockbox; Customer Key; and policy-based controls for Copilot and AI experiences. For the full contents, see the <a href="https://www.microsoft.com/en-us/security/small-medium-business/microsoft-purview-suite-business-premium">Microsoft Purview Suite for Business Premium</a> page.</p>

<p>Combined with <strong>Defender Suite for Business Premium</strong> (also $10), the bundle drops to <strong>$15/user/month</strong> for both, versus $20 if bought separately. If the customer needs both security and compliance (and most do), this is the value pick.</p>

<p><strong>One caveat.</strong> Purview features are <strong>per-user-covered, not per-tenant-enabled</strong>. The features activate once licensed somewhere in the tenant, but compliance-wise every user whose activity is covered by DLP, IRM, eDiscovery, or Communication Compliance needs the license. An IRM policy scoped to “all users” means all users need the license. <strong>Do not let a customer assume one license turns it on for the tenant.</strong> This is the same grey-area pattern as Entra ID P2 and it comes up in audits.</p>

<h2 id="what-you-unlock">What you unlock</h2>

<p>Outcomes, not features. With the Purview Suite add-on in place:</p>

<table>
  <thead>
    <tr>
      <th>Capability</th>
      <th>What changes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>DLP across the full surface</strong></td>
      <td>Teams chat, endpoint, cloud apps, and browsers added to email and files. One policy covers everywhere.</td>
    </tr>
    <tr>
      <td><strong>Auto-labelling</strong></td>
      <td>Sensitivity labels applied automatically based on content. Users no longer have to remember.</td>
    </tr>
    <tr>
      <td><strong>Insider Risk Management</strong></td>
      <td>Behavioural signals detect download spikes, access anomalies, and exfiltration sequences.</td>
    </tr>
    <tr>
      <td><strong>Adaptive Protection</strong></td>
      <td>High-risk users automatically get stricter DLP policies based on their IRM risk score.</td>
    </tr>
    <tr>
      <td><strong>eDiscovery Premium</strong></td>
      <td>Legal holds across Teams chat. Custodian management, review sets, conversation threading.</td>
    </tr>
    <tr>
      <td><strong>Records Management</strong></td>
      <td>File plans, event-based retention, disposition review before deletion.</td>
    </tr>
    <tr>
      <td><strong>Communication Compliance</strong></td>
      <td>Policy-based scanning of Teams, email, Viva Engage for regulatory language patterns.</td>
    </tr>
    <tr>
      <td><strong>Audit Premium</strong></td>
      <td>One-year retention (up from 180 days) and high-bandwidth events. 10-year retention requires a separate add-on.</td>
    </tr>
    <tr>
      <td><strong>Compliance Manager</strong></td>
      <td>Assessment templates for GDPR, ISO 27001, NIST. Pre-mapped improvement actions.</td>
    </tr>
    <tr>
      <td><strong>Copilot-aware policies</strong></td>
      <td>DLP and sensitivity labels extend into Copilot interactions and grounding.</td>
    </tr>
  </tbody>
</table>

<p><strong>The practical difference:</strong> a single policy that says “block credit card numbers from leaving the tenant” now actually means everywhere. The leaving-with-data scenario from the opening of this post generates an IRM alert before the damage is done. When the legal team calls for a hold that includes Teams chat, you have the workflow. When the auditor asks for disposition proof, you have event-based retention with reviewed disposal.</p>

<h2 id="how-to-deploy-this-across-your-customer-base">How to deploy this across your customer base</h2>

<p>The scenario: you manage 15 customer tenants. You need a consistent DLP baseline across all of them, and you need proof that each tenant satisfies the relevant framework controls. The approach: <strong>build once in your own tenant, then replicate.</strong></p>

<h3 id="step-1-build-the-baseline-in-your-own-tenant">Step 1: Build the baseline in your own tenant</h3>

<p>Start in your MSP tenant or a dedicated baseline tenant. This is your template environment. Configure the DLP policies you want every customer to have:</p>

<table>
  <thead>
    <tr>
      <th>Policy</th>
      <th>Sensitive information types</th>
      <th>Action</th>
      <th>Framework controls</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>EU PII</strong></td>
      <td>EU National ID, EU Passport, EU SSN, IBAN</td>
      <td>Block</td>
      <td>NIS2 Art.21(2)(d), DORA Art.9(4)(c), CIS 3.3, NIST PR.DS-01</td>
    </tr>
    <tr>
      <td><strong>Payment card data</strong></td>
      <td>Credit Card Number, EU Debit Card Number</td>
      <td>Block</td>
      <td>DORA Art.9(4)(c), CIS 3.10, NIST PR.DS-01</td>
    </tr>
    <tr>
      <td><strong>Health data</strong></td>
      <td>ICD-10-CM, ICD-9-CM codes</td>
      <td>Notify</td>
      <td>NIS2 Art.21(2)(d), NIST PR.DS-01</td>
    </tr>
    <tr>
      <td><strong>Financial identifiers</strong></td>
      <td>SWIFT Code, EU Tax ID, ABA Routing Number</td>
      <td>Notify</td>
      <td>NIS2 Art.21(2)(d), DORA Art.11(1), CIS 3.3</td>
    </tr>
  </tbody>
</table>

<p>For each policy, configure the locations (Exchange, SharePoint, OneDrive, and Teams/Endpoints if the customer has Purview Suite), the detection thresholds, and the actions (block or notify). <strong>Test the policies in simulation mode first.</strong> Purview’s simulation mode shows you what would trigger without enforcing, so you can tune the thresholds before going live.</p>

<p>A few design decisions worth making upfront:</p>

<ul>
  <li><strong>PII and payment card data get hard blocks.</strong> These are the highest-risk categories. A false positive is cheaper than a breach.</li>
  <li><strong>Health and financial data start as notify-only.</strong> These have more partial-match noise. Set a <code class="language-plaintext highlighter-rouge">minCount</code> threshold of 2 or higher and tighten after you have real data on false positive rates.</li>
  <li><strong>Tag every policy with the framework controls it satisfies.</strong> Use the policy description field or a separate tracking sheet. When the auditor asks “how do you satisfy NIS2 Article 21(2)(d)?”, the answer should be a row in a table, not a slide deck.</li>
</ul>

<h3 id="step-2-deploy-across-tenants">Step 2: Deploy across tenants</h3>

<p>Once the baseline is working in your template tenant, the next step is rolling it out to every customer. You can do this manually per tenant, script it with PowerShell (<a href="/posts/msp-license-ladder-01-hunting-gap/">Post #1</a> showed the multi-tenant GDAP pattern), or use a multi-tenancy tool.</p>

<p>A tool I use for this is <strong><a href="https://inforcer.com">Inforcer</a></strong>. It is a multi-tenancy platform for MSPs that connects to customer tenants through enterprise app registrations (manual onboarding or GDAP). You deploy your baseline policy set across tenants from one place, and when you later change a policy, Inforcer flags which tenants are out of alignment so you can push the update. It takes the manual per-tenant work out of the equation.</p>

<h3 id="step-3-map-the-output-to-frameworks">Step 3: Map the output to frameworks</h3>

<p>Whether you deploy via Inforcer or PowerShell, the end result should be the same: <strong>a coverage matrix that shows which policies satisfy which controls in which tenants.</strong></p>

<table>
  <thead>
    <tr>
      <th>Framework</th>
      <th>Control</th>
      <th>What satisfies it</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>NIS2</td>
      <td>Art.21(2)(d)</td>
      <td>EU PII, Health, Financial policies</td>
    </tr>
    <tr>
      <td>DORA</td>
      <td>Art.9(4)(c)</td>
      <td>EU PII, PCI, Financial policies</td>
    </tr>
    <tr>
      <td>CIS v8</td>
      <td>3.10</td>
      <td>PCI policy</td>
    </tr>
    <tr>
      <td>NIST CSF 2.0</td>
      <td>PR.DS-01</td>
      <td>All 4 policies</td>
    </tr>
  </tbody>
</table>

<p>That is the audit artefact. Export it as a CSV, attach it to the customer’s compliance record, and move on. When the next audit cycle comes, re-export and diff.</p>

<h2 id="when-not-to-bother">When not to bother</h2>

<p>Purview Suite rewards customers who already care about data governance. Two cases where the upgrade sits idle:</p>

<p><strong>No data classification scheme.</strong> If the customer has no idea what data they have, where it lives, or who should access it, deploying DLP policies generates noise, not protection. Start with a data classification exercise. Map the sensitive data types that actually exist in their tenant, decide what “sensitive” means for their business, and then deploy policies to enforce it. Purview Suite is the automation layer once the program exists, not the starting point.</p>

<p><strong>No one to triage.</strong> IRM signals, Communication Compliance alerts, and eDiscovery workflows all require human review. If the customer has no compliance function, no legal team, and no appetite to build one, you are licensing tooling that will generate alerts nobody reads. That is worse than not having it, because unreviewed alerts create liability. If they are not ready to staff the process, they are not ready for the tooling.</p>

<h2 id="how-to-pitch-it">How to pitch it</h2>

<p>Lead with a regulatory obligation, a post-incident trigger, or an audit finding. Do not lead with “data is important.”</p>

<p>Three openers that work:</p>

<ul>
  <li>
    <p><strong>Regulatory driver.</strong> <em>“NIS2 Article 21 requires measures for data handling and access control. Your current DLP covers email and files. It does not cover endpoints, Teams, or cloud apps. Purview Suite closes that scope gap and gives you a framework-mapped artefact to show the auditor.”</em></p>
  </li>
  <li>
    <p><strong>Post-incident trigger.</strong> <em>“The data leak last quarter was a USB copy that base DLP could not see. Endpoint DLP would have blocked it. IRM would have flagged the download spike before the USB was even mounted.”</em></p>
  </li>
  <li>
    <p><strong>Audit finding.</strong> <em>“Your last audit flagged that retention policies are time-based only, with no disposition review. Records Management in Purview Suite adds file plans, event-based retention, and reviewed disposal. That closes the finding.”</em></p>
  </li>
</ul>

<p>Price it as part of a managed governance service, not a licence passthrough. The $10/user/month is the floor. The value is the baseline you deploy across every customer, the framework-mapped coverage matrix, and the triage capacity behind the alerts.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Business Premium and E3 give you more Purview than most people realise. You can label, you can DLP email and files, you can retain. But the moment data moves to an endpoint, a browser, or a cloud app, base Purview stops watching. The employee from the opening of this post walks out with the data, and you find out from the customer’s competitor.</p>

<p><strong>The Purview Suite add-on</strong> ($10/user/month) closes that gap. Build a baseline policy set in your own tenant, validate it in simulation mode, deploy it across your customers, and keep it aligned. When the auditor asks what controls you satisfy, hand them the framework matrix instead of a conversation.</p>

<p>Start with one customer tenant. Get the SIT detections right for their data. Then scale out.</p>

<table>
  <thead>
    <tr>
      <th>Resource</th>
      <th>What it covers</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/office365/servicedescriptions/microsoft-365-service-descriptions/microsoft-365-tenantlevel-services-licensing-guidance/microsoft-purview-service-description">Purview service description</a></td>
      <td>Authoritative SKU-by-SKU feature matrix (updated April 2026)</td>
    </tr>
    <tr>
      <td><a href="https://www.microsoft.com/licensing/guidance/Microsoft-Purview">Purview Licensing Guidance</a></td>
      <td>Official licensing FAQ, source of the “technical enforcement” caveat</td>
    </tr>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/office365/servicedescriptions/microsoft-365-service-descriptions/microsoft-365-tenantlevel-services-licensing-guidance/microsoft-365-security-compliance-licensing-guidance">M365 compliance licensing guidance</a></td>
      <td>Broader security and compliance licensing overview</td>
    </tr>
    <tr>
      <td><a href="https://www.microsoft.com/en-us/security/small-medium-business/microsoft-purview-suite-business-premium">Purview Suite for Business Premium</a></td>
      <td>Product page with full contents and pricing</td>
    </tr>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/purview/dlp-microsoft-teams">DLP and Microsoft Teams</a></td>
      <td>Teams DLP setup, licensing requirements, and scope</td>
    </tr>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/purview/dlp-learn-about-dlp">Purview DLP documentation</a></td>
      <td>Getting started with DLP policies</td>
    </tr>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/purview/insider-risk-management">Insider Risk Management</a></td>
      <td>IRM overview and configuration</td>
    </tr>
    <tr>
      <td><a href="https://learn.microsoft.com/en-us/purview/ediscovery-premium-overview">eDiscovery Premium overview</a></td>
      <td>Premium eDiscovery capabilities and workflows</td>
    </tr>
  </tbody>
</table>

<p>Next up: <strong><a href="/posts/msp-license-ladder-03-identity-gap/">#3, The Identity Gap</a></strong>, on what Entra ID P2 and Defender for Identity unlock across your cloud and on-prem identity surface, and why they are almost always bought together.</p>

<hr />

<p><em>Part of <a href="/posts/msp-license-ladder-01-hunting-gap/">The MSP License Ladder</a> series. Previous: <a href="/posts/msp-license-ladder-01-hunting-gap/">#1, The Hunting Gap</a>. Next: <a href="/posts/msp-license-ladder-03-identity-gap/">#3, The Identity Gap</a>.</em></p>

<p><em>Last verified: April 2026</em></p>

<h2 id="related">Related</h2>

<ul>
  <li><a href="/posts/msp-license-ladder-01-hunting-gap/">The MSP License Ladder #1: The Hunting Gap</a> – what Defender for Endpoint Plan 2 unlocks for proactive threat hunting across your customer fleet.</li>
  <li><a href="/posts/msp-license-ladder-03-identity-gap/">The MSP License Ladder #3: The Identity Gap</a> – what Entra ID P2 and Defender for Identity unlock for risk-based CA, just-in-time privilege, and on-prem AD visibility.</li>
  <li><a href="/posts/msp-license-ladder-04-exposure-gap/">The MSP License Ladder #4: The Exposure Gap</a> – what MDO P2 and Defender for Cloud Apps unlock for post-delivery email investigation and shadow IT discovery.</li>
  <li><a href="/posts/msp-license-ladder-05-endpoint-gap/">The MSP License Ladder #5: The Endpoint Gap</a> – what the Intune Suite unlocks for least-privilege elevation, third-party app patching, and device health analytics.</li>
</ul>]]></content>

      
      
      <author>
          <name>Roy Klooster</name>
      </author>

      
        <category term="Compliance" />
      
        <category term="Security" />
      
        <category term="PowerShell" />
      
        <category term="Automation" />
      
        <category term="M365" />
      

      
      
        <summary type="html"><![CDATA[Base Purview on Business Premium and E3 covers more than most expect, but endpoints, Teams chat, cloud apps, and browsers are out of scope. Purview Suite closes the gap at $10/user/month.]]></summary>
      
    </entry>
  
    <entry>
      

      <title type="html">The MSP License Ladder #1: The Hunting Gap</title>
      <link href="https://rksolutions.nl/posts/msp-license-ladder-01-hunting-gap/" rel="alternate" type="text/html" title="The MSP License Ladder #1: The Hunting Gap" />
      <published>2026-04-21T00:00:00+00:00</published>
      <updated>2026-04-21T00:00:00+00:00</updated>
      <id>https://rksolutions.nl/posts/msp-license-ladder-01-hunting-gap</id>
      <content type="html" xml:base="https://rksolutions.nl/posts/msp-license-ladder-01-hunting-gap/"><![CDATA[<p>A customer calls you on Monday morning. Defender flagged malware on a laptop over the weekend and cleaned it up. The alert is closed. Except the real questions are just starting. Was a new local admin account added to that device in the last week? Did a process quietly elevate to SYSTEM? Has that account signed in anywhere else in the fleet?</p>

<p>On <strong>Microsoft 365 Business Premium</strong> and <strong>Microsoft 365 E3</strong>, you cannot answer those questions across the fleet. That is the shift this post is about: reactive security (an alert fires, you respond) versus proactive security (you hunt for the setup steps before the detonation). <strong>Microsoft Defender for Endpoint Plan 2</strong> is the rung that flips that switch.</p>

<p>A few terms up front:</p>

<ul>
  <li><strong>IoC</strong> (Indicator of Compromise): a file hash, IP, domain, or process pattern tied to known malicious activity. When a vendor or CERT publishes a breach, they usually publish IoCs so you can sweep your own estate.</li>
  <li><strong>KQL</strong> (Kusto Query Language): the query language the Defender portal and Advanced Hunting use.</li>
  <li><strong>EDR</strong> (Endpoint Detection and Response): tooling that records endpoint activity and lets you query it afterwards, not just block the bad stuff.</li>
  <li><strong>NGAV</strong> (Next-Generation Antivirus): behaviour-based AV. The “block it” layer.</li>
</ul>

<p><strong>Table of Contents</strong></p>

<ul>
  <li><a href="#reactive-by-default">Reactive by default</a></li>
  <li><a href="#the-proactive-gap">The proactive gap</a>
    <ul>
      <li><a href="#post-incident-reconstruction">Post-incident reconstruction</a></li>
      <li><a href="#retrospective-ioc-sweep">Retrospective IoC sweep</a></li>
      <li><a href="#the-custom-detection-you-want-to-keep-running">The custom detection you want to keep running</a></li>
    </ul>
  </li>
  <li><a href="#the-upgrade-paths">The upgrade paths</a></li>
  <li><a href="#what-defender-for-endpoint-plan-2-gives-you">What Defender for Endpoint Plan 2 gives you</a></li>
  <li><a href="#demo-multi-tenant-local-admin-hunt">Demo: multi-tenant local admin hunt</a>
    <ul>
      <li><a href="#the-kql">The KQL</a></li>
      <li><a href="#the-powershell-script">The PowerShell script</a></li>
      <li><a href="#the-azure-devops-pipeline">The Azure DevOps pipeline</a></li>
    </ul>
  </li>
  <li><a href="#when-not-to-bother">When not to bother</a></li>
  <li><a href="#how-to-pitch-it">How to pitch it</a></li>
  <li><a href="#conclusion">Conclusion</a></li>
</ul>

<h2 id="reactive-by-default">Reactive by default</h2>

<p><strong>Business Premium</strong> ships with <strong>Defender for Business</strong>. NGAV, Attack Surface Reduction rules, basic EDR with alerts and automated investigation, device isolation, web content filtering, 30-day retention. Solid at the block-and-alert layer.</p>

<p><strong>Microsoft 365 E3</strong> ships with <strong>Defender for Endpoint Plan 1</strong>. NGAV and Attack Surface Reduction only. No hunting surface at all. Without an add-on, Microsoft 365 E3 is a weaker endpoint posture than Business Premium.</p>

<p>Both are reactive. An alert fires, you respond. Neither lets you ask your fleet a question the product did not think to ask on your behalf.</p>

<h2 id="the-proactive-gap">The proactive gap</h2>

<p>Three scenarios that come up on real customer calls and leave you reaching for something you do not have.</p>

<h3 id="post-incident-reconstruction">Post-incident reconstruction</h3>

<p>Defender caught the malware. Now you want to rebuild the story. Was a local admin added before detonation? Did a process elevate? On Defender for Business you can stare at the per-device timeline, but you cannot run a single KQL query that joins admin additions with logons with process events across every device.</p>

<h3 id="retrospective-ioc-sweep">Retrospective IoC sweep</h3>

<p>A partner drops an IoC list. You want to know if any device in any customer tenant has ever seen those hashes, domains, or IPs. On Defender for Business you can search alerts. You cannot run a cross-fleet KQL query, and that is what an IoC sweep actually is.</p>

<h3 id="the-custom-detection-you-want-to-keep-running">The custom detection you want to keep running</h3>

<p>You notice a subtle pattern unique to one of your customers’ stacks. You want a rule that alerts the moment it reappears, anywhere. Defender for Business has no <strong>Custom Detection Rules</strong>. You are stuck with Microsoft’s built-in detections plus Attack Surface Reduction.</p>

<p>Every one of those is a proactive question. Base SKUs answer the reactive ones and leave the proactive ones on the floor.</p>

<h2 id="the-upgrade-paths">The upgrade paths</h2>

<table>
  <thead>
    <tr>
      <th>Path</th>
      <th>Best for</th>
      <th>Includes</th>
      <th>List price</th>
      <th>Main constraint</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Defender Suite for Business Premium</strong></td>
      <td>Business Premium customers under 300 users</td>
      <td>Defender for Endpoint Plan 2, Defender for Office 365 Plan 2, Defender for Identity, Defender for Cloud Apps, Entra ID P2</td>
      <td><strong>$10</strong></td>
      <td>300-user cap</td>
    </tr>
    <tr>
      <td><strong>Defender for Endpoint Plan 2 standalone</strong></td>
      <td>Customers who only need endpoint</td>
      <td>Defender for Endpoint Plan 2 only</td>
      <td><strong>$5.20</strong></td>
      <td>Leaves identity, email, SaaS gaps open</td>
    </tr>
    <tr>
      <td><strong>Microsoft 365 E5 Security add-on</strong></td>
      <td>Microsoft 365 E3 customers</td>
      <td>Everything in Defender Suite plus Defender for IoT</td>
      <td><strong>$12</strong></td>
      <td>Larger upfront spend</td>
    </tr>
  </tbody>
</table>

<p>Prices are Microsoft list per user per month, as of April 2026. CSP, NCE, and Enterprise Agreement pricing differs; check with your licensing partner for the real customer number. Defender Suite for Business Premium is the value pick at roughly 65% cheaper than buying its components individually. Combined with Purview Suite for Business Premium (also $10), the bundle drops to <strong>$15/user/month</strong> for both.</p>

<p><strong>One caveat.</strong> Defender for Business and Defender for Endpoint Plan 2 cannot coexist in the same tenant. If both licence types are present, the tenant defaults to Defender for Business. To actually get Defender for Endpoint Plan 2, every user needs a Defender for Endpoint Plan 2 licence and you must open a Microsoft Support ticket to switch the experience. Plan this as a tenant-wide flip, not a phased migration. See the <a href="https://learn.microsoft.com/en-us/defender-business/mdb-faq#can-i-have-a-mix-of-microsoft-endpoint-security-subscriptions">Defender for Business FAQ</a> for the official word.</p>

<h2 id="what-defender-for-endpoint-plan-2-gives-you">What Defender for Endpoint Plan 2 gives you</h2>

<p><strong>What it gives you:</strong></p>

<ul>
  <li><strong>Advanced Hunting</strong>: 30 days of raw, KQL-queryable telemetry across every device. Tables you will use most: <code class="language-plaintext highlighter-rouge">DeviceEvents</code>, <code class="language-plaintext highlighter-rouge">DeviceLogonEvents</code>, <code class="language-plaintext highlighter-rouge">DeviceProcessEvents</code>, <code class="language-plaintext highlighter-rouge">DeviceFileEvents</code>, <code class="language-plaintext highlighter-rouge">DeviceNetworkEvents</code>, <code class="language-plaintext highlighter-rouge">DeviceRegistryEvents</code>.</li>
  <li><strong>180-day device timeline</strong> for deep-dive investigations on a specific device.</li>
  <li><strong>Custom Detection Rules</strong>: your own KQL turned into automated detections and response actions. Runs in near real time for supported detections. This is where proactive lives.</li>
  <li><strong>Live Response</strong>: an interactive shell into a device during an investigation. Grab files, run scripts, collect memory samples.</li>
  <li><strong>Graph Threat Hunting API</strong> (<code class="language-plaintext highlighter-rouge">runHuntingQuery</code>) with the <code class="language-plaintext highlighter-rouge">ThreatHunting.Read.All</code> permission. This is what makes multi-tenant automation possible.</li>
  <li><strong>Full Threat and Vulnerability Management</strong> and <strong>Microsoft Threat Experts</strong> (opt-in).</li>
</ul>

<h2 id="demo-multi-tenant-local-admin-hunt">Demo: multi-tenant local admin hunt</h2>

<p>The scenario: after an incident at one customer, run the same hunting query across every customer tenant you manage. Post-incident reconstruction for the affected tenant, proactive sweep for the rest.</p>

<p>Shout-out to <strong>Damien Van Robaeys</strong> here. His post on <a href="https://www.systanddeploy.com/2026/03/hunt-devices-with-local-admin-account.html">hunting devices with local admin accounts via Defender for Endpoint</a> already nailed the KQL side of this, so I am not reinventing the wheel on the query itself. What I am adding is the process around it: how to take a single-tenant, portal-driven hunt and wrap it so it runs across every customer tenant you manage, from a single script. Go read his original for the deep dive on the query logic, then come back here for the multi-tenant plumbing.</p>

<p><strong>Requirements:</strong></p>

<ul>
  <li>A <strong>multi-tenant app registration</strong> in your MSP tenant with application permission <code class="language-plaintext highlighter-rouge">ThreatHunting.Read.All</code>.</li>
  <li>A <strong>certificate</strong> (self-signed or from your PKI) uploaded to the app registration.</li>
  <li><strong>Admin consent</strong> granted in each customer tenant via the admin consent URL:
    <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://login.microsoftonline.com/{customer-tenant-id}/adminconsent?client_id={your-app-id}
</code></pre></div>    </div>
  </li>
  <li>An <strong>Azure DevOps</strong> project with two variable groups:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">defender-hunt</code> - auth only: <code class="language-plaintext highlighter-rouge">CLIENT_ID</code>, <code class="language-plaintext highlighter-rouge">CERT_THUMBPRINT</code>, <code class="language-plaintext highlighter-rouge">CERT_PASSWORD</code> (marked secret). A Secure File holds the <code class="language-plaintext highlighter-rouge">.pfx</code>.</li>
      <li><code class="language-plaintext highlighter-rouge">customer-tenants</code> - the fleet: one variable per tenant, using the convention <code class="language-plaintext highlighter-rouge">TENANT_&lt;ShortName&gt;</code> with the tenant’s directory ID as the value. For example <code class="language-plaintext highlighter-rouge">TENANT_A = &lt;TENANT_ID&gt;</code>, <code class="language-plaintext highlighter-rouge">TENANT_B = &lt;TENANT_ID&gt;</code>. Add a tenant by adding a row; remove one by deleting it. Splitting this group from the auth group means every future workflow - licensing sweeps, policy audits, backup checks - reuses the same tenant list without duplicating it.</li>
    </ul>
  </li>
  <li>PowerShell 7 on the pipeline agent and the <code class="language-plaintext highlighter-rouge">Microsoft.Graph.Authentication</code> module.</li>
</ul>

<p>Keeping the tenant list in a shared variable group means no customer data in the repo, and the script has no file dependencies. Everything it needs comes from <code class="language-plaintext highlighter-rouge">$env</code>.</p>

<h3 id="the-kql">The KQL</h3>

<p>Local admin additions in the last 7 days, joined with interactive logons by the same account on the same device. One pass answers “was a new admin added, and has it been used”:</p>

<pre><code class="language-kql">let timeWindow = 7d;
let adminAdds =
    DeviceEvents
    | where Timestamp &gt; ago(timeWindow)
    | where ActionType == "UserAccountAddedToLocalGroup"
    | where AdditionalFields has "Administrators"
    | extend AddedAccount = tostring(parse_json(AdditionalFields).AccountName)
    | project AddTime = Timestamp, DeviceId, DeviceName, AddedAccount,
              AddedBy = InitiatingProcessAccountName,
              AddedByProcess = InitiatingProcessFileName;
let interactiveLogons =
    DeviceLogonEvents
    | where Timestamp &gt; ago(timeWindow)
    | where LogonType in ("Interactive", "RemoteInteractive")
    | summarize LogonCount = count(), LastLogon = max(Timestamp)
        by DeviceId, AccountName;
adminAdds
| join kind = leftouter interactiveLogons
    on $left.DeviceId == $right.DeviceId
   and $left.AddedAccount == $right.AccountName
| project AddTime, DeviceName, AddedAccount, AddedBy, AddedByProcess, LogonCount, LastLogon
| order by AddTime desc
</code></pre>

<h3 id="the-powershell-script">The PowerShell script</h3>

<p>Commit this as <code class="language-plaintext highlighter-rouge">scripts/Invoke-LocalAdminHunt.ps1</code> in the Azure DevOps repo. It reads everything from <code class="language-plaintext highlighter-rouge">$env</code>, loops the tenants, runs the hunting query, and prints results. No parameters, no file dependencies, no secrets on disk.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># scripts/Invoke-LocalAdminHunt.ps1</span><span class="w">
</span><span class="c"># Inputs (all from pipeline env vars, set via variable groups):</span><span class="w">
</span><span class="c">#   CLIENT_ID       - App registration client ID</span><span class="w">
</span><span class="c">#   CERT_THUMBPRINT - Thumbprint of the cert already imported into CurrentUser\My</span><span class="w">
</span><span class="c">#   TENANT_*        - One env var per customer tenant, from the customer-tenants</span><span class="w">
</span><span class="c">#                     variable group. Key is TENANT_&lt;ShortName&gt;, value is the</span><span class="w">
</span><span class="c">#                     tenant directory ID. Example: TENANT_Contoso = &lt;guid&gt;.</span><span class="w">

</span><span class="n">Install-Module</span><span class="w"> </span><span class="nx">Microsoft.Graph.Authentication</span><span class="w"> </span><span class="nt">-Scope</span><span class="w"> </span><span class="nx">CurrentUser</span><span class="w"> </span><span class="nt">-Force</span><span class="w">
</span><span class="n">Import-Module</span><span class="w"> </span><span class="nx">Microsoft.Graph.Authentication</span><span class="w">

</span><span class="nv">$ClientId</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">CLIENT_ID</span><span class="w">
</span><span class="nv">$Thumbprint</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">CERT_THUMBPRINT</span><span class="w">

</span><span class="nv">$tenants</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ChildItem</span><span class="w"> </span><span class="nx">Env:</span><span class="w"> </span><span class="o">|</span><span class="w">
    </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="w"> </span><span class="o">-like</span><span class="w"> </span><span class="s1">'TENANT_*'</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">|</span><span class="w">
    </span><span class="n">ForEach-Object</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
            </span><span class="nx">Name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="err">.</span><span class="nx">Name</span><span class="w"> </span><span class="err">-</span><span class="nx">replace</span><span class="w"> </span><span class="s1">'^TENANT_'</span><span class="p">,</span><span class="s1">''</span><span class="w">
            </span><span class="nx">Id</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="err">.</span><span class="nx">Value</span><span class="err">.</span><span class="nx">Trim</span><span class="err">()</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">

</span><span class="nv">$query</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sh">@'
let timeWindow = 7d;
let adminAdds =
    DeviceEvents
    | where Timestamp &gt; ago(timeWindow)
    | where ActionType == "UserAccountAddedToLocalGroup"
    | where AdditionalFields has "Administrators"
    | extend AddedAccount = tostring(parse_json(AdditionalFields).AccountName)
    | project AddTime = Timestamp, DeviceId, DeviceName, AddedAccount,
              AddedBy = InitiatingProcessAccountName,
              AddedByProcess = InitiatingProcessFileName;
let interactiveLogons =
    DeviceLogonEvents
    | where Timestamp &gt; ago(timeWindow)
    | where LogonType in ("Interactive", "RemoteInteractive")
    | summarize LogonCount = count(), LastLogon = max(Timestamp)
        by DeviceId, AccountName;
adminAdds
| join kind = leftouter interactiveLogons
    on $left.DeviceId == $right.DeviceId
   and $left.AddedAccount == $right.AccountName
| project AddTime, DeviceName, AddedAccount, AddedBy, AddedByProcess, LogonCount, LastLogon
| order by AddTime desc
'@</span><span class="w">

</span><span class="nv">$results</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-Object</span><span class="w"> </span><span class="nx">System.Collections.Generic.List</span><span class="p">[</span><span class="n">object</span><span class="p">]</span><span class="w">

</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$tenant</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$tenants</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Connect-MgGraph</span><span class="w"> </span><span class="nt">-ClientId</span><span class="w"> </span><span class="nv">$ClientId</span><span class="w"> </span><span class="se">`
</span><span class="w">                        </span><span class="nt">-CertificateThumbprint</span><span class="w"> </span><span class="nv">$Thumbprint</span><span class="w"> </span><span class="se">`
</span><span class="w">                        </span><span class="nt">-TenantId</span><span class="w"> </span><span class="nv">$tenant</span><span class="o">.</span><span class="nf">Id</span><span class="w"> </span><span class="se">`
</span><span class="w">                        </span><span class="nt">-NoWelcome</span><span class="w">

        </span><span class="nv">$body</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w"> </span><span class="nx">Query</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$query</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-Json</span><span class="w"> </span><span class="nt">-Depth</span><span class="w"> </span><span class="nx">3</span><span class="w">

        </span><span class="nv">$response</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-MgGraphRequest</span><span class="w"> </span><span class="nt">-Method</span><span class="w"> </span><span class="nx">POST</span><span class="w"> </span><span class="se">`
</span><span class="w">            </span><span class="nt">-Uri</span><span class="w"> </span><span class="s1">'https://graph.microsoft.com/v1.0/security/runHuntingQuery'</span><span class="w"> </span><span class="se">`
</span><span class="w">            </span><span class="nt">-Body</span><span class="w"> </span><span class="nv">$body</span><span class="w"> </span><span class="nt">-ContentType</span><span class="w"> </span><span class="s1">'application/json'</span><span class="w">

        </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$row</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$response</span><span class="o">.</span><span class="nf">results</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nv">$results</span><span class="o">.</span><span class="nf">Add</span><span class="p">([</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
                </span><span class="nx">Tenant</span><span class="w">       </span><span class="o">=</span><span class="w"> </span><span class="nv">$tenant</span><span class="err">.</span><span class="nx">Name</span><span class="w">
                </span><span class="nx">TenantId</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="nv">$tenant</span><span class="err">.</span><span class="nx">Id</span><span class="w">
                </span><span class="nx">AddTime</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="nv">$row</span><span class="err">.</span><span class="nx">AddTime</span><span class="w">
                </span><span class="nx">DeviceName</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="nv">$row</span><span class="err">.</span><span class="nx">DeviceName</span><span class="w">
                </span><span class="nx">AddedAccount</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$row</span><span class="err">.</span><span class="nx">AddedAccount</span><span class="w">
                </span><span class="nx">AddedBy</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="nv">$row</span><span class="err">.</span><span class="nx">AddedBy</span><span class="w">
                </span><span class="nx">Process</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="nv">$row</span><span class="err">.</span><span class="nx">AddedByProcess</span><span class="w">
                </span><span class="nx">LogonCount</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="nv">$row</span><span class="err">.</span><span class="nx">LogonCount</span><span class="w">
                </span><span class="nx">LastLogon</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="nv">$row</span><span class="err">.</span><span class="nx">LastLogon</span><span class="w">
            </span><span class="p">})</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Write-Warning</span><span class="w"> </span><span class="s2">"Tenant </span><span class="si">$(</span><span class="nv">$tenant</span><span class="o">.</span><span class="nf">Name</span><span class="si">)</span><span class="s2"> [</span><span class="si">$(</span><span class="nv">$tenant</span><span class="o">.</span><span class="nf">Id</span><span class="si">)</span><span class="s2">]: </span><span class="bp">$_</span><span class="s2">"</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">finally</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Disconnect-MgGraph</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Out-Null</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"</span><span class="si">$(</span><span class="nv">$results</span><span class="o">.</span><span class="nf">Count</span><span class="si">)</span><span class="s2"> matches across </span><span class="si">$(</span><span class="nv">$tenants</span><span class="o">.</span><span class="nf">Count</span><span class="si">)</span><span class="s2"> tenants"</span><span class="w">
</span><span class="nv">$results</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Format-Table</span><span class="w"> </span><span class="nt">-AutoSize</span><span class="w">

</span><span class="c"># From here, it is up to the admin how to follow this up. Pick whatever fits</span><span class="w">
</span><span class="c"># your stack: post $results to a Teams or Slack webhook, fire off a</span><span class="w">
</span><span class="c"># Send-MailMessage / Graph sendMail digest, open a ticket in your PSA</span><span class="w">
</span><span class="c"># (HaloPSA, Autotask, ConnectWise) via its REST API, push to Log Analytics,</span><span class="w">
</span><span class="c"># or hand off to a Logic App. The script gets the data out of Defender;</span><span class="w">
</span><span class="c"># how it lands in your triage workflow is your call.</span><span class="w">
</span></code></pre></div></div>

<h3 id="the-azure-devops-pipeline">The Azure DevOps pipeline</h3>

<p>With the script in the repo, the pipeline stays small. It downloads the certificate, imports it into the agent’s store, and calls <code class="language-plaintext highlighter-rouge">Invoke-LocalAdminHunt.ps1</code> with the variable groups attached so the script sees everything it needs on <code class="language-plaintext highlighter-rouge">$env</code>.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># azure-pipelines.yml</span>
<span class="na">trigger</span><span class="pi">:</span> <span class="s">none</span>

<span class="na">schedules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">cron</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0</span><span class="nv"> </span><span class="s">6</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*"</span>
    <span class="na">displayName</span><span class="pi">:</span> <span class="s">Daily multi-tenant local admin hunt</span>
    <span class="na">branches</span><span class="pi">:</span>
      <span class="na">include</span><span class="pi">:</span> <span class="pi">[</span> <span class="nv">main</span> <span class="pi">]</span>
    <span class="na">always</span><span class="pi">:</span> <span class="kc">true</span>

<span class="na">pool</span><span class="pi">:</span>
  <span class="na">vmImage</span><span class="pi">:</span> <span class="s">windows-latest</span>

<span class="na">variables</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">group</span><span class="pi">:</span> <span class="s">defender-hunt</span>
  <span class="pi">-</span> <span class="na">group</span><span class="pi">:</span> <span class="s">customer-tenants</span>

<span class="na">steps</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">DownloadSecureFile@1</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">huntCert</span>
    <span class="na">displayName</span><span class="pi">:</span> <span class="s">Download certificate</span>
    <span class="na">inputs</span><span class="pi">:</span>
      <span class="na">secureFile</span><span class="pi">:</span> <span class="s1">'</span><span class="s">defender-hunt.pfx'</span>

  <span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">PowerShell@2</span>
    <span class="na">displayName</span><span class="pi">:</span> <span class="s">Import certificate into CurrentUser\My</span>
    <span class="na">inputs</span><span class="pi">:</span>
      <span class="na">targetType</span><span class="pi">:</span> <span class="s">inline</span>
      <span class="na">pwsh</span><span class="pi">:</span> <span class="kc">true</span>
      <span class="na">script</span><span class="pi">:</span> <span class="pi">|</span>
        <span class="s">$pwd = ConvertTo-SecureString '$(CERT_PASSWORD)' -AsPlainText -Force</span>
        <span class="s">Import-PfxCertificate -FilePath "$(huntCert.secureFilePath)" `</span>
                              <span class="s">-CertStoreLocation Cert:\CurrentUser\My `</span>
                              <span class="s">-Password $pwd | Out-Null</span>

  <span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">PowerShell@2</span>
    <span class="na">displayName</span><span class="pi">:</span> <span class="s">Run multi-tenant hunt</span>
    <span class="na">inputs</span><span class="pi">:</span>
      <span class="na">filePath</span><span class="pi">:</span> <span class="s">scripts/Invoke-LocalAdminHunt.ps1</span>
      <span class="na">pwsh</span><span class="pi">:</span> <span class="kc">true</span>
</code></pre></div></div>

<p>Run the pipeline ad-hoc after an incident. Let the schedule cover the daily proactive sweep. For near real time against a specific tenant, lift the KQL into a <strong>Custom Detection Rule</strong> in that tenant’s Defender portal. Swap the KQL in the <code class="language-plaintext highlighter-rouge">.ps1</code> for any detection pattern you care about. The pipeline stays the same; the script is the thing you iterate on.</p>

<h2 id="when-not-to-bother">When not to bother</h2>

<p>Defender for Endpoint Plan 2 earns its money when someone actually hunts with it. Skip the upgrade when:</p>

<ul>
  <li><strong>Nobody will triage the output.</strong> A daily report no one reads is a paper trail of signal you ignored. Regulators and insurers ask about that.</li>
  <li><strong>The customer already pays for a third-party EDR or MDR.</strong> Stacking Defender for Endpoint Plan 2 on top duplicates coverage without displacing the other tool. That said, this can also be a selling point to save money: <em>“You can drop your current EDR and save $X by switching to Defender for Endpoint Plan 2, which gives you the same EDR features plus proactive hunting and better integration with the Microsoft stack.”</em></li>
</ul>

<h2 id="how-to-pitch-it">How to pitch it</h2>

<p>Lead with the customer’s specific risks, not the feature list. Two reliable openers, phrased the way you would actually say them:</p>

<ul>
  <li>
    <p><em>Remember the questions we could not answer after the last incident? Was a new admin added beforehand, did anything elevate to SYSTEM, has that account been used anywhere else? Defender for Endpoint Plan 2 is the licence that lets us answer those - across every device, in a single query.</em></p>
  </li>
  <li>
    <p><em>Your cyber insurance renewal will ask about EDR, custom detections, and mean time to triage. With Defender Suite for Business Premium you can tick EDR, Custom Detection Rules, and Live Response on a single line, which is easier than arguing about equivalents with the underwriter.</em></p>
  </li>
</ul>

<p>Price it as a managed hunting service, not a licence passthrough. The $10/user/month is the floor. The value is the KQL library you build over time and the triage capacity behind it.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Defender for Endpoint Plan 2 flips MSPs from reactive to proactive. Defender for Business and Plan 1 handle block, alert, respond. Plan 2 hunts the setup step, codifies patterns as Custom Detection Rules, and catches the next attempt before the Monday morning call. Advanced Hunting gives you 30 days of telemetry - tell your customer that ceiling up front.</p>

<p>Test the multi-tenant script in a lab first. Consent one customer tenant, confirm the query works or tweak a bit to your needs, then scale out.</p>

<p>Resources:</p>

<ul>
  <li><a href="https://learn.microsoft.com/en-us/microsoft-365/admin/security-and-compliance/add-defender-suite-business-premium?view=o365-worldwide">Add Microsoft Defender Suite for Business Premium to your subscription</a></li>
  <li><a href="https://learn.microsoft.com/en-us/defender-business/mdb-faq#can-i-have-a-mix-of-microsoft-endpoint-security-subscriptions">Defender for Business FAQ on mixed subscriptions</a></li>
  <li><a href="https://learn.microsoft.com/en-us/defender-xdr/advanced-hunting-overview">Advanced Hunting overview</a></li>
  <li><a href="https://learn.microsoft.com/en-us/graph/api/security-security-runhuntingquery">runHuntingQuery Graph API reference</a></li>
  <li>Damien Van Robaeys’s <a href="https://www.systanddeploy.com/2026/03/hunt-devices-with-local-admin-account.html">hunting devices with local admin accounts</a></li>
</ul>

<p>Next up: <strong><a href="/posts/msp-license-ladder-02-data-gap/">#2, The Data Gap</a></strong>, on what Purview Suite and E5 Compliance unlock over base Business Premium and Microsoft 365 E3.</p>

<h2 id="related">Related</h2>

<ul>
  <li><a href="/posts/msp-license-ladder-02-data-gap/">The MSP License Ladder #2: The Data Gap</a> - what Purview Suite unlocks over base Business Premium and E3 for DLP, Insider Risk, and eDiscovery.</li>
  <li><a href="/posts/msp-license-ladder-03-identity-gap/">The MSP License Ladder #3: The Identity Gap</a> - what Entra ID P2 and Defender for Identity unlock for risk-based CA, just-in-time privilege, and on-prem AD visibility.</li>
  <li><a href="/posts/msp-license-ladder-04-exposure-gap/">The MSP License Ladder #4: The Exposure Gap</a> - what MDO P2 and Defender for Cloud Apps unlock for post-delivery email investigation and shadow IT discovery.</li>
  <li><a href="/posts/msp-license-ladder-05-endpoint-gap/">The MSP License Ladder #5: The Endpoint Gap</a> – what the Intune Suite unlocks for least-privilege elevation, third-party app patching, and device health analytics.</li>
  <li><a href="/posts/inforcercommunity-a-powershell-module-for-the-inforcer-api/">InforcerCommunity PowerShell Module</a> – multi-tenant M365 automation for MSPs using the Inforcer platform.</li>
</ul>]]></content>

      
      
      <author>
          <name>Roy Klooster</name>
      </author>

      
        <category term="Defender" />
      
        <category term="Graph API" />
      
        <category term="MDE" />
      
        <category term="Security" />
      
        <category term="PowerShell" />
      
        <category term="Automation" />
      
        <category term="M365" />
      

      
      
        <summary type="html"><![CDATA[Business Premium and M365 E3 give MSPs basic Defender alerts but no advanced hunting. Defender for Endpoint Plan 2 unlocks 30 days of KQL-queryable telemetry for proactive threat investigation. Includes a multi-tenant hunting script and a license cost comparison showing when the upgrade pays for itself.]]></summary>
      
    </entry>
  
    <entry>
      

      <title type="html">InforcerCommunity: A PowerShell Module for the Inforcer REST API</title>
      <link href="https://rksolutions.nl/posts/inforcercommunity-a-powershell-module-for-the-inforcer-api/" rel="alternate" type="text/html" title="InforcerCommunity: A PowerShell Module for the Inforcer REST API" />
      <published>2026-03-30T00:00:00+00:00</published>
      <updated>2026-05-15T00:00:00+00:00</updated>
      <id>https://rksolutions.nl/posts/inforcercommunity-a-powershell-module-for-the-inforcer-api</id>
      <content type="html" xml:base="https://rksolutions.nl/posts/inforcercommunity-a-powershell-module-for-the-inforcer-api/"><![CDATA[<p>If you use <strong>Inforcer</strong> to manage Microsoft 365 baselines, alignment scores, and policies across tenants, you already know the value of a single pane of glass for compliance and policy drift. But automation and scripting often mean wrestling with REST APIs, building your own auth and error handling, and maintaining scripts that break when the API changes. <strong>InforcerCommunity</strong> is a community PowerShell module that wraps the Inforcer API so you can connect, query tenants, baselines, policies, alignment details, users, and audit events from the command line or from your own scripts - with consistent parameters, sensible defaults, and help that works. This guide explains what it does, how to use it, and how you can contribute or ask for new features.</p>

<p><strong>Table of Contents</strong></p>

<ul>
  <li><a href="#what-is-inforcercommunity">What is InforcerCommunity?</a></li>
  <li><a href="#whats-new-in-v040">What’s New in v0.4.0</a></li>
  <li><a href="#requirements">Requirements</a></li>
  <li><a href="#installation">Installation</a>
    <ul>
      <li><a href="#option-1-from-powershell-gallery-recommended">Option 1: From PowerShell Gallery (recommended)</a></li>
      <li><a href="#option-2-from-source-github">Option 2: From source (GitHub)</a></li>
    </ul>
  </li>
  <li><a href="#quick-start">Quick Start</a></li>
  <li><a href="#assessments">Assessments</a>
    <ul>
      <li><a href="#single-tenant-assessment">Single-Tenant Assessment</a></li>
      <li><a href="#multi-tenant-assessment-matrix">Multi-Tenant Assessment Matrix</a></li>
      <li><a href="#assessment-export-options">Assessment Export Options</a></li>
    </ul>
  </li>
  <li><a href="#tenant-documentation">Tenant Documentation</a></li>
  <li><a href="#environment-comparison">Environment Comparison</a></li>
  <li><a href="#key-cmdlets-and-use-cases">Key Cmdlets and Use Cases</a></li>
  <li><a href="#output-formats-and-filtering">Output Formats and Filtering</a></li>
  <li><a href="#how-to-contribute">How to Contribute</a></li>
  <li><a href="#how-to-report-bugs">How to Report Bugs</a></li>
  <li><a href="#how-to-request-a-feature">How to Request a Feature</a></li>
  <li><a href="#conclusion">Conclusion</a></li>
</ul>

<h2 id="what-is-inforcercommunity">What is InforcerCommunity?</h2>

<p><strong>InforcerCommunity</strong> is a PowerShell script module that talks to the Inforcer REST API.</p>

<p><strong>What it gives you:</strong></p>

<ul>
  <li><strong>Connect once, query everything:</strong> Authenticate with your Inforcer API key and region, then run cmdlets to list tenants, baselines, policies, alignment details, users, and audit events.</li>
  <li><strong>Consistent behavior:</strong> All Get-* cmdlets support <code class="language-plaintext highlighter-rouge">-Format</code>, <code class="language-plaintext highlighter-rouge">-OutputType</code> (PowerShellObject or JsonObject), and <code class="language-plaintext highlighter-rouge">-TenantId</code> for filtering. TenantId accepts a numeric ID, Microsoft Tenant ID (GUID), or tenant name.</li>
  <li><strong>No secrets in scripts:</strong> The API key is stored as a SecureString in the session; you can pass it once via <code class="language-plaintext highlighter-rouge">Connect-Inforcer</code> and then run as many commands as you need.</li>
  <li><strong>Tab completion and help:</strong> Every cmdlet has comment-based help; <code class="language-plaintext highlighter-rouge">Get-Help Connect-Inforcer -Full</code> and tab completion on parameters (e.g. <code class="language-plaintext highlighter-rouge">-EventType</code> on <code class="language-plaintext highlighter-rouge">Get-InforcerAuditEvent</code>) work out of the box.</li>
  <li><strong>Pipeline support:</strong> Pipe tenants into other cmdlets - e.g. <code class="language-plaintext highlighter-rouge">Get-InforcerTenant | Get-InforcerUser</code> or <code class="language-plaintext highlighter-rouge">Get-InforcerTenant | Get-InforcerTenantPolicies</code> works out of the box.</li>
</ul>

<p><strong>Where to find it:</strong></p>

<ul>
  <li><strong>Source code and issues:</strong> <a href="https://github.com/royklo/InforcerCommunity">https://github.com/royklo/InforcerCommunity</a></li>
  <li><strong>PowerShell Gallery:</strong> <a href="https://www.powershellgallery.com/packages/InforcerCommunity">https://www.powershellgallery.com/packages/InforcerCommunity</a></li>
</ul>

<blockquote>
  <p><strong>Community project notice:</strong> InforcerCommunity was created by me for the community. It is <strong>not owned, endorsed, or maintained by Inforcer</strong>. It is an independent, community-driven project to make the Inforcer API easier to use from PowerShell. You use it at your own responsibility.</p>
</blockquote>

<h2 id="whats-new-in-v040">What’s New in v0.4.0</h2>

<p>The headline feature in v0.4.0 is <strong>compliance assessments</strong> - run assessments like Copilot Readiness, CIS Benchmarks, or Essential Eight against one tenant or all of them at once, and get interactive HTML reports or structured data for automation.</p>

<p><strong>v0.4.0 highlights:</strong></p>

<ul>
  <li><strong>New cmdlet: <code class="language-plaintext highlighter-rouge">Get-InforcerAssessment</code></strong> - lists all available assessments (Copilot Readiness, CIS Microsoft 365 Foundations Benchmark, CIS Microsoft Intune for Windows 11 Benchmark, Essential Eight Maturity Level 1, and custom assessments).</li>
  <li><strong>New cmdlet: <code class="language-plaintext highlighter-rouge">Invoke-InforcerAssessment</code></strong> - runs an assessment against a tenant and returns detailed per-check results with pass/fail status, violations, warnings, and per-object scores. Accepts assessment names (<code class="language-plaintext highlighter-rouge">"Copilot Readiness"</code>) and tenant names (<code class="language-plaintext highlighter-rouge">"Contoso"</code>) directly - no need to look up IDs.</li>
  <li><strong>Multi-tenant mode:</strong> Add <code class="language-plaintext highlighter-rouge">-MultiTenant</code> to run the assessment against all your tenants in one command, or pass multiple tenant names (e.g. <code class="language-plaintext highlighter-rouge">-TenantId "Contoso","Fabrikam","Woodgrove"</code>). See a compliance summary for every tenant and generate a matrix comparison report.</li>
  <li><strong>Interactive HTML reports:</strong> <code class="language-plaintext highlighter-rouge">-OutputPath report.html</code> generates a self-contained HTML report with collapsible checks, per-object expandable cards showing violations and passes, markdown-rendered remediation steps, search, and filters. Multi-tenant mode generates a full-viewport matrix with sticky columns, horizontal scroll for 100+ tenants, a tenant show/hide dropdown, and a slide-out detail panel.</li>
  <li><strong>CSV and JSON export:</strong> <code class="language-plaintext highlighter-rouge">-OutputPath report.csv</code> for flat data (UTF-8 no-BOM, ready for Excel), <code class="language-plaintext highlighter-rouge">-OutputType JsonObject</code> for structured JSON - both single-tenant and multi-tenant.</li>
  <li><strong>Async with progress:</strong> Long-running assessments show progress updates every 10 seconds with human-readable elapsed time (e.g. “3m 16s”). Multi-tenant runs show per-tenant progress and a total elapsed time at the end.</li>
</ul>

<p><strong>v0.3.x highlights (included):</strong></p>

<ul>
  <li><strong>Cross-tenant comparison</strong> with <code class="language-plaintext highlighter-rouge">Compare-InforcerEnvironments</code> - four-tab interactive HTML report with comparison, manual review, duplicates, and deprecated settings.</li>
  <li><strong>Cross-category reconciliation</strong> - settings delivered via different Intune template types (Endpoint Security vs Settings Catalog) are now correctly matched by DefinitionId.</li>
  <li><strong>Baseline-scoped comparison</strong> - compare only policies in a specific baseline instead of the entire tenant.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">Export-InforcerTenantDocumentation</code></strong> - HTML, Markdown, and Excel output formats with Settings Catalog resolution, Graph integration, baseline and tag filtering.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">Get-InforcerGroup</code> and <code class="language-plaintext highlighter-rouge">Get-InforcerRole</code></strong> - Entra ID groups (with search, filter, pagination, and member detail) and directory role definitions.</li>
  <li><strong>Settings Catalog resolution</strong> - automatically downloaded and cached from <a href="https://github.com/royklo/IntuneSettingsCatalogData">IntuneSettingsCatalogData</a>.</li>
  <li><strong>Connection cmdlets</strong> - <code class="language-plaintext highlighter-rouge">Connect-Inforcer</code> supports <code class="language-plaintext highlighter-rouge">-FetchGraphData</code> for Microsoft Graph and <code class="language-plaintext highlighter-rouge">-PassThru</code> for cross-account workflows.</li>
</ul>

<h2 id="requirements">Requirements</h2>

<ul>
  <li><strong>PowerShell 7.0 or later</strong> (Windows, macOS, or Linux).</li>
  <li>An <strong>Inforcer API key</strong> from your Inforcer tenant (<strong>Configure &gt; REST API &gt; New API Key</strong>).</li>
</ul>

<h2 id="installation">Installation</h2>

<h3 id="option-1-from-powershell-gallery-recommended">Option 1: From PowerShell Gallery (recommended)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Install-Module -Name InforcerCommunity -Scope CurrentUser
</code></pre></div></div>

<h3 id="option-2-from-source-github">Option 2: From source (GitHub)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/royklo/InforcerCommunity.git
cd InforcerCommunity
Import-Module ./module/InforcerCommunity.psd1 -Force
</code></pre></div></div>

<p><strong>Important:</strong> When loading from source, always run <code class="language-plaintext highlighter-rouge">Import-Module</code> from the <strong>repository root</strong> and use the path <code class="language-plaintext highlighter-rouge">./module/InforcerCommunity.psd1</code>. If you see errors about a missing file or wrong path, make sure you are in the InforcerCommunity repo root.</p>

<h2 id="quick-start">Quick Start</h2>

<p>After installing the module:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Connect with your API key (region: uk, eu, us, or anz)
Connect-Inforcer -ApiKey "your-api-key" -Region uk

# List all tenants you have access to
Get-InforcerTenant

# Get alignment details in table format
Get-InforcerAlignmentDetails

# Get users for a tenant (by name, numeric ID, or GUID)
Get-InforcerUser -TenantId "Contoso"

# Get policies for a specific tenant
Get-InforcerTenantPolicies -TenantId 482

# Disconnect when done
Disconnect-Inforcer
</code></pre></div></div>

<p>You can use <code class="language-plaintext highlighter-rouge">Get-Help &lt;CmdletName&gt; -Full</code> for parameters and examples (e.g. <code class="language-plaintext highlighter-rouge">Get-Help Get-InforcerUser -Full</code>).</p>

<h2 id="assessments">Assessments</h2>

<p>Inforcer provides compliance assessments that evaluate your tenant against industry frameworks and readiness checks - things like Copilot Readiness, CIS Microsoft 365 Foundations Benchmark, CIS Microsoft Intune for Windows 11 Benchmark, and Essential Eight Maturity Level 1. With InforcerCommunity, you can run these assessments from the command line and get structured results you can automate with.</p>

<h3 id="single-tenant-assessment">Single-Tenant Assessment</h3>

<p>Run an assessment against one tenant and see every check with its pass/fail status:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># List available assessments</span><span class="w">
</span><span class="n">Get-InforcerAssessment</span><span class="w">

</span><span class="c"># Run Copilot Readiness against a tenant (by name)</span><span class="w">
</span><span class="n">Invoke-InforcerAssessment</span><span class="w"> </span><span class="nt">-TenantId</span><span class="w"> </span><span class="s2">"Contoso"</span><span class="w"> </span><span class="nt">-AssessmentId</span><span class="w"> </span><span class="s2">"Copilot Readiness"</span><span class="w">
</span></code></pre></div></div>

<p>The output shows a compliance summary followed by each check as a pipeline object:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  Copilot Readiness - 76.2% compliant (16/21 checks passed)

Status           : Pass
name             : Enable Conditional Access policies to block legacy authentication
category         : Entra
subCategory      : Conditional Access
importance       : High
ObjectsEvaluated : 13
FindingsMessage  : 1 out of 13 object(s) are fully-compliant with this check
Scores           : {@{objectId=de66f385...; score=100; objectName=Core - Block - Legacy Authentication; ...}, ...}
</code></pre></div></div>

<p>Each check includes a <code class="language-plaintext highlighter-rouge">Scores</code> property with per-object detail - which Conditional Access policy passed, which failed, and why. You can drill into this:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$results</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-InforcerAssessment</span><span class="w"> </span><span class="nt">-TenantId</span><span class="w"> </span><span class="s2">"Contoso"</span><span class="w"> </span><span class="nt">-AssessmentId</span><span class="w"> </span><span class="s2">"Copilot Readiness"</span><span class="w">

</span><span class="c"># See only failed checks</span><span class="w">
</span><span class="nv">$results</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="nx">Status</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'Fail'</span><span class="w">

</span><span class="c"># Table view</span><span class="w">
</span><span class="nv">$results</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Format-Table</span><span class="w"> </span><span class="nx">Status</span><span class="p">,</span><span class="w"> </span><span class="nx">name</span><span class="p">,</span><span class="w"> </span><span class="nx">category</span><span class="p">,</span><span class="w"> </span><span class="nx">importance</span><span class="w">

</span><span class="c"># Drill into violations for a specific check</span><span class="w">
</span><span class="nv">$results</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">.</span><span class="nf">Scores</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">violations</span><span class="o">.</span><span class="nf">Count</span><span class="w"> </span><span class="o">-gt</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nx">objectName</span><span class="p">,</span><span class="w"> </span><span class="nx">score</span><span class="p">,</span><span class="w"> </span><span class="nx">violations</span><span class="w">
</span></code></pre></div></div>

<p>Generate an interactive HTML report with one parameter:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Invoke-InforcerAssessment</span><span class="w"> </span><span class="nt">-TenantId</span><span class="w"> </span><span class="s2">"Contoso"</span><span class="w"> </span><span class="nt">-AssessmentId</span><span class="w"> </span><span class="s2">"Copilot Readiness"</span><span class="w"> </span><span class="nt">-OutputPath</span><span class="w"> </span><span class="o">.</span><span class="nx">/copilot-report.html</span><span class="w">
</span></code></pre></div></div>

<p>The HTML report includes a navy cover banner, compliance score ring, collapsible check cards grouped by category (Entra, Exchange, M365, Purview, SharePoint), per-object expandable cards showing violations and passes, and markdown-rendered description and remediation steps. Everything is self-contained - no external dependencies, works offline.</p>

<h3 id="multi-tenant-assessment-matrix">Multi-Tenant Assessment Matrix</h3>

<p>The real power comes when you run an assessment across all your tenants at once:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Run against all tenants</span><span class="w">
</span><span class="n">Invoke-InforcerAssessment</span><span class="w"> </span><span class="nt">-AssessmentId</span><span class="w"> </span><span class="s2">"Copilot Readiness"</span><span class="w"> </span><span class="nt">-MultiTenant</span><span class="w"> </span><span class="nt">-OutputPath</span><span class="w"> </span><span class="o">.</span><span class="nx">/matrix.html</span><span class="w">

</span><span class="c"># Or pick specific tenants by name</span><span class="w">
</span><span class="n">Invoke-InforcerAssessment</span><span class="w"> </span><span class="nt">-TenantId</span><span class="w"> </span><span class="s2">"Contoso"</span><span class="p">,</span><span class="s2">"Fabrikam"</span><span class="p">,</span><span class="s2">"Woodgrove"</span><span class="w"> </span><span class="nt">-AssessmentId</span><span class="w"> </span><span class="s2">"Copilot Readiness"</span><span class="w"> </span><span class="nt">-OutputPath</span><span class="w"> </span><span class="o">.</span><span class="nx">/matrix.html</span><span class="w">
</span></code></pre></div></div>

<p>The cmdlet runs each tenant sequentially with progress updates:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Multi-tenant assessment: 'Copilot Readiness' across 10 tenant(s)

[1/10] Running 'Copilot Readiness' against Contoso...
  Still running... 10s elapsed
  Completed in 14s.
[2/10] Running 'Copilot Readiness' against Fabrikam...
  Still running... 3m 10s elapsed
  Completed in 3m 16s.
...

All assessments complete. 10 tenant(s) processed in 16m 18s.
  Contoso - 76.2% (16/21)
  Fabrikam - 42.9% (9/21)
  Woodgrove - 38.1% (8/21)
  ...
</code></pre></div></div>

<p>The matrix HTML report is a full-viewport interactive dashboard: a sticky left column with check names that stays visible while you scroll horizontally across tenant columns, each showing a pass/fail indicator. A tenant filter dropdown lets you show or hide specific tenants (useful when you have 100+ tenants and want to focus on a subset). Click “Details” on any check to open a slide-out panel with the description, impact, and rationale. Category rows group checks by Entra, Exchange, M365, Purview, and SharePoint. Search and status filters (All, Has Failures, All Passed) work across the entire matrix.</p>

<h3 id="assessment-export-options">Assessment Export Options</h3>

<p>All export formats work for both single-tenant and multi-tenant:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># HTML report (single or matrix)</span><span class="w">
</span><span class="n">Invoke-InforcerAssessment</span><span class="w"> </span><span class="nt">-TenantId</span><span class="w"> </span><span class="s2">"Contoso"</span><span class="w"> </span><span class="nt">-AssessmentId</span><span class="w"> </span><span class="s2">"Copilot Readiness"</span><span class="w"> </span><span class="nt">-OutputPath</span><span class="w"> </span><span class="o">.</span><span class="nx">/report.html</span><span class="w">

</span><span class="c"># CSV for Excel or automation (multi-tenant includes Tenant column)</span><span class="w">
</span><span class="n">Invoke-InforcerAssessment</span><span class="w"> </span><span class="nt">-AssessmentId</span><span class="w"> </span><span class="s2">"Copilot Readiness"</span><span class="w"> </span><span class="nt">-MultiTenant</span><span class="w"> </span><span class="nt">-OutputPath</span><span class="w"> </span><span class="o">.</span><span class="nx">/matrix.csv</span><span class="w">

</span><span class="c"># JSON for webhooks, APIs, or further processing</span><span class="w">
</span><span class="n">Invoke-InforcerAssessment</span><span class="w"> </span><span class="nt">-AssessmentId</span><span class="w"> </span><span class="s2">"Copilot Readiness"</span><span class="w"> </span><span class="nt">-MultiTenant</span><span class="w"> </span><span class="nt">-OutputType</span><span class="w"> </span><span class="nx">JsonObject</span><span class="w">

</span><span class="c"># Pipeline for PowerShell automation (each check has TenantName in multi-tenant mode)</span><span class="w">
</span><span class="n">Invoke-InforcerAssessment</span><span class="w"> </span><span class="nt">-AssessmentId</span><span class="w"> </span><span class="s2">"Copilot Readiness"</span><span class="w"> </span><span class="nt">-MultiTenant</span><span class="w"> </span><span class="o">|</span><span class="w">
    </span><span class="n">Where-Object</span><span class="w"> </span><span class="nx">Status</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'Fail'</span><span class="w"> </span><span class="o">|</span><span class="w">
    </span><span class="n">Group-Object</span><span class="w"> </span><span class="nx">TenantName</span><span class="w"> </span><span class="o">|</span><span class="w">
    </span><span class="n">Select-Object</span><span class="w"> </span><span class="nx">Name</span><span class="p">,</span><span class="w"> </span><span class="nx">Count</span><span class="w">
</span></code></pre></div></div>

<h2 id="tenant-documentation">Tenant Documentation</h2>

<p>The new <code class="language-plaintext highlighter-rouge">Export-InforcerTenantDocumentation</code> cmdlet generates comprehensive documentation for an entire tenant in one command:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Generate HTML documentation (opens in browser automatically)</span><span class="w">
</span><span class="n">Export-InforcerTenantDocumentation</span><span class="w"> </span><span class="nt">-TenantId</span><span class="w"> </span><span class="s2">"Contoso"</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="nx">Html</span><span class="w">

</span><span class="c"># Generate HTML and Excel, with Graph enrichment for group/filter names</span><span class="w">
</span><span class="n">Connect-Inforcer</span><span class="w"> </span><span class="nt">-ApiKey</span><span class="w"> </span><span class="s2">"your-api-key"</span><span class="w"> </span><span class="nt">-Region</span><span class="w"> </span><span class="nx">uk</span><span class="w"> </span><span class="nt">-FetchGraphData</span><span class="w">
</span><span class="n">Export-InforcerTenantDocumentation</span><span class="w"> </span><span class="nt">-TenantId</span><span class="w"> </span><span class="s2">"Contoso"</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="nx">Html</span><span class="p">,</span><span class="nx">Excel</span><span class="w"> </span><span class="nt">-OutputPath</span><span class="w"> </span><span class="nx">C:\Reports</span><span class="w">

</span><span class="c"># Export only policies from a specific baseline</span><span class="w">
</span><span class="n">Export-InforcerTenantDocumentation</span><span class="w"> </span><span class="nt">-TenantId</span><span class="w"> </span><span class="nx">139</span><span class="w"> </span><span class="nt">-Baseline</span><span class="w"> </span><span class="s2">"Inforcer Blueprint Baseline - Tier 1"</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="nx">Html</span><span class="w">

</span><span class="c"># Filter by tag</span><span class="w">
</span><span class="n">Export-InforcerTenantDocumentation</span><span class="w"> </span><span class="nt">-TenantId</span><span class="w"> </span><span class="s2">"Contoso"</span><span class="w"> </span><span class="nt">-Tag</span><span class="w"> </span><span class="s2">"Production"</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="nx">Markdown</span><span class="w">
</span></code></pre></div></div>

<p><strong>HTML output</strong> is a self-contained file with no external dependencies - you can email it, archive it, or open it offline. It includes:</p>

<ul>
  <li>Collapsible Product &gt; Category &gt; Policy navigation in a sidebar</li>
  <li>Real-time search with text highlighting</li>
  <li>Dark/light mode toggle (persisted in localStorage)</li>
  <li>Tag filter pills with AND/OR logic</li>
  <li>Hide empty fields and show metadata toggles</li>
  <li>Collapsible long values and a back-to-top button</li>
</ul>

<p><strong>Excel output</strong> creates a workbook with one sheet per product area. Each row is a policy with columns for category, name, description, platform, settings, and assignments - ready for filtering and analysis.</p>

<p><strong>Markdown output</strong> generates a GFM-compatible document with a table of contents and per-policy tables - useful for including in wikis or version-controlled documentation.</p>

<h2 id="environment-comparison">Environment Comparison</h2>

<p>The new <code class="language-plaintext highlighter-rouge">Compare-InforcerEnvironments</code> cmdlet compares two tenants’ Intune configurations and generates an interactive HTML report:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Compare two tenants in the same Inforcer account</span><span class="w">
</span><span class="n">Compare-InforcerEnvironments</span><span class="w"> </span><span class="nt">-SourceTenantId</span><span class="w"> </span><span class="s2">"Contoso"</span><span class="w"> </span><span class="nt">-DestinationTenantId</span><span class="w"> </span><span class="s2">"Fabrikam"</span><span class="w">

</span><span class="c"># Compare across different Inforcer accounts with Graph enrichment</span><span class="w">
</span><span class="nv">$src</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Connect-Inforcer</span><span class="w"> </span><span class="nt">-ApiKey</span><span class="w"> </span><span class="nv">$key1</span><span class="w"> </span><span class="nt">-Region</span><span class="w"> </span><span class="nx">uk</span><span class="w"> </span><span class="nt">-PassThru</span><span class="w">
</span><span class="nv">$dst</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Connect-Inforcer</span><span class="w"> </span><span class="nt">-ApiKey</span><span class="w"> </span><span class="nv">$key2</span><span class="w"> </span><span class="nt">-Region</span><span class="w"> </span><span class="nx">eu</span><span class="w"> </span><span class="nt">-PassThru</span><span class="w">
</span><span class="n">Compare-InforcerEnvironments</span><span class="w"> </span><span class="nt">-SourceTenantId</span><span class="w"> </span><span class="nx">482</span><span class="w"> </span><span class="nt">-DestinationTenantId</span><span class="w"> </span><span class="nx">139</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-SourceSession</span><span class="w"> </span><span class="nv">$src</span><span class="w"> </span><span class="nt">-DestinationSession</span><span class="w"> </span><span class="nv">$dst</span><span class="w"> </span><span class="nt">-FetchGraphData</span><span class="w">
</span></code></pre></div></div>

<p><strong>The HTML report includes four tabs:</strong></p>

<ul>
  <li><strong>Comparison</strong> - flat table of all Settings Catalog settings with sortable columns, status filter pills (Matched/Conflicting/Source Only/Dest Only), category dropdown, and advanced column filters with AND/OR logic</li>
  <li><strong>Manual Review</strong> - non-Settings-Catalog policies (compliance, enrollment, scripts) in a 50/50 source/destination layout grouped by platform. Matching policy names are aligned side-by-side. Scripts and compliance rules are shown as collapsible code blocks with syntax highlighting</li>
  <li><strong>Duplicates</strong> - settings configured in two or more policies with different values, with automated conflict analysis</li>
  <li><strong>Deprecated</strong> - settings flagged as deprecated by Microsoft, grouped by source and destination</li>
</ul>

<p>The report also features an animated configuration match score (with confetti at 100%), dark/light mode toggle, column resize handles, and a responsive layout. Like the Export report, the HTML is fully self-contained with no external dependencies.</p>

<h2 id="key-cmdlets-and-use-cases">Key Cmdlets and Use Cases</h2>

<table>
  <thead>
    <tr>
      <th>Cmdlet</th>
      <th>What it does</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Connect-Inforcer</code></td>
      <td>Establishes a secure connection to the Inforcer API (ApiKey, Region or BaseUrl). Supports <code class="language-plaintext highlighter-rouge">-PassThru</code> for cross-account workflows.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Disconnect-Inforcer</code></td>
      <td>Clears the session and disconnects.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Test-InforcerConnection</code></td>
      <td>Verifies the current API connection.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Get-InforcerTenant</code></td>
      <td>Lists tenants; optional <code class="language-plaintext highlighter-rouge">-TenantId</code> to return a single tenant.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Get-InforcerBaseline</code></td>
      <td>Retrieves baseline groups and members.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Get-InforcerTenantPolicies</code></td>
      <td>Retrieves policies for a given tenant.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Get-InforcerAlignmentDetails</code></td>
      <td>Retrieves alignment scores or per-policy alignment details (optional <code class="language-plaintext highlighter-rouge">-TenantId</code>, <code class="language-plaintext highlighter-rouge">-BaselineId</code>, <code class="language-plaintext highlighter-rouge">-Tag</code>).</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Get-InforcerAuditEvent</code></td>
      <td>Retrieves audit events (optional <code class="language-plaintext highlighter-rouge">-EventType</code>, date range, paging).</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Get-InforcerSupportedEventType</code></td>
      <td>Lists supported audit event types (used for tab completion).</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Get-InforcerUser</code></td>
      <td>Lists/searches users or gets full user detail by ID (optional <code class="language-plaintext highlighter-rouge">-Search</code>, <code class="language-plaintext highlighter-rouge">-MaxResults</code>, <code class="language-plaintext highlighter-rouge">-UserId</code>).</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Get-InforcerGroup</code></td>
      <td>Retrieves Entra ID groups (list with search/filter/pagination, or detail by name/GUID with members).</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Get-InforcerRole</code></td>
      <td>Retrieves Entra ID directory role definitions (built-in, enabled, privileged).</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Export-InforcerTenantDocumentation</code></td>
      <td>Generates tenant documentation in HTML, Markdown, or Excel (optional <code class="language-plaintext highlighter-rouge">-Baseline</code>, <code class="language-plaintext highlighter-rouge">-Tag</code>, <code class="language-plaintext highlighter-rouge">-FetchGraphData</code>).</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Compare-InforcerEnvironments</code></td>
      <td>Compares two tenants’ Intune configuration and generates an interactive HTML comparison report.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Get-InforcerAssessment</code></td>
      <td>Lists available assessments (Copilot Readiness, CIS Benchmarks, Essential Eight, etc.).</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Invoke-InforcerAssessment</code></td>
      <td>Runs an assessment against one or more tenants. Supports <code class="language-plaintext highlighter-rouge">-MultiTenant</code>, <code class="language-plaintext highlighter-rouge">-OutputPath</code> (HTML/CSV), <code class="language-plaintext highlighter-rouge">-OutputType JsonObject</code>.</td>
    </tr>
  </tbody>
</table>

<p><strong>Typical workflows:</strong></p>

<ul>
  <li><strong>Tenant and policy overview:</strong> <code class="language-plaintext highlighter-rouge">Connect-Inforcer</code> -&gt; <code class="language-plaintext highlighter-rouge">Get-InforcerTenant</code> -&gt; <code class="language-plaintext highlighter-rouge">Get-InforcerTenantPolicies -TenantId "Contoso"</code> to inspect a specific tenant’s policies.</li>
  <li><strong>Alignment and drift:</strong> <code class="language-plaintext highlighter-rouge">Get-InforcerAlignmentDetails</code> for score summaries; <code class="language-plaintext highlighter-rouge">Get-InforcerAlignmentDetails -BaselineId "Tier 0"</code> for per-policy detail.</li>
  <li><strong>User overview:</strong> <code class="language-plaintext highlighter-rouge">Get-InforcerUser -TenantId "Contoso"</code> for a user list; <code class="language-plaintext highlighter-rouge">Get-InforcerUser -TenantId 139 -UserId "8e61ce11-..."</code> for full detail including groups, roles, devices, and risk.</li>
  <li><strong>Audit and compliance:</strong> <code class="language-plaintext highlighter-rouge">Get-InforcerAuditEvent</code> with optional <code class="language-plaintext highlighter-rouge">-EventType</code> (tab completion for event types), <code class="language-plaintext highlighter-rouge">-DateFrom</code>, <code class="language-plaintext highlighter-rouge">-DateTo</code>, and paging parameters.</li>
  <li><strong>Tenant documentation:</strong> <code class="language-plaintext highlighter-rouge">Export-InforcerTenantDocumentation -TenantId "Contoso" -Format Html,Excel</code> to generate a complete configuration snapshot.</li>
  <li><strong>Environment comparison:</strong> <code class="language-plaintext highlighter-rouge">Compare-InforcerEnvironments -SourceTenantId "Contoso" -DestinationTenantId "Fabrikam"</code> to see every difference between two tenants.</li>
  <li><strong>Group and role lookup:</strong> <code class="language-plaintext highlighter-rouge">Get-InforcerGroup -TenantId 139 -Search "Finance"</code> for groups, <code class="language-plaintext highlighter-rouge">Get-InforcerRole -TenantId 139 | Where-Object IsPrivileged -eq $true</code> for privileged roles.</li>
  <li><strong>Compliance assessment:</strong> <code class="language-plaintext highlighter-rouge">Invoke-InforcerAssessment -TenantId "Contoso" -AssessmentId "Copilot Readiness"</code> to check a single tenant, or add <code class="language-plaintext highlighter-rouge">-MultiTenant -OutputPath matrix.html</code> for a cross-tenant matrix report.</li>
  <li><strong>Pipeline:</strong> <code class="language-plaintext highlighter-rouge">Get-InforcerTenant -TenantId 139 | Get-InforcerUser</code> to list users for a piped tenant.</li>
</ul>

<p>For full parameter details and example output, see the <a href="https://github.com/royklo/InforcerCommunity/blob/main/docs/CMDLET-REFERENCE.md">Cmdlet Reference</a> in the repository.</p>

<h2 id="output-formats-and-filtering">Output Formats and Filtering</h2>

<ul>
  <li><strong>-Format:</strong> Most Get-* cmdlets support <code class="language-plaintext highlighter-rouge">Table</code> or <code class="language-plaintext highlighter-rouge">Raw</code> (e.g. for alignment details).</li>
  <li><strong>-OutputType:</strong> <code class="language-plaintext highlighter-rouge">PowerShellObject</code> (default) or <code class="language-plaintext highlighter-rouge">JsonObject</code> (JSON with depth 100) for piping into other tools or export.</li>
  <li><strong>-TenantId:</strong> Accepts a numeric Client Tenant ID, a Microsoft Tenant ID (GUID), or a tenant name (case-insensitive match). Use it on <code class="language-plaintext highlighter-rouge">Get-InforcerTenant</code>, <code class="language-plaintext highlighter-rouge">Get-InforcerTenantPolicies</code>, <code class="language-plaintext highlighter-rouge">Get-InforcerAlignmentDetails</code>, <code class="language-plaintext highlighter-rouge">Get-InforcerUser</code>, and others.</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Example: export all tenants as JSON for use elsewhere
Get-InforcerTenant -OutputType JsonObject | Out-File tenants.json -Encoding utf8

# Example: search users by name
Get-InforcerUser -TenantId "Contoso" -Search "Adele"

# Example: get full user detail as JSON
Get-InforcerUser -TenantId 139 -UserId "8e61ce11-a45b-42a6-8ca4-1d881781566d" -OutputType JsonObject
</code></pre></div></div>

<h2 id="how-to-contribute">How to Contribute</h2>

<p>Contributions are welcome. The project uses a standard fork-and-pull-request workflow:</p>

<ul>
  <li><strong>Fork</strong> the repository on GitHub: <a href="https://github.com/royklo/InforcerCommunity">https://github.com/royklo/InforcerCommunity</a>.</li>
  <li><strong>Clone your fork</strong> and create a branch (e.g. <code class="language-plaintext highlighter-rouge">feature/your-feature-name</code> or <code class="language-plaintext highlighter-rouge">fix/bug-description</code>).</li>
  <li><strong>Make your changes</strong> under <code class="language-plaintext highlighter-rouge">module/</code> (see <a href="https://github.com/royklo/InforcerCommunity/blob/main/CONTRIBUTING.md">CONTRIBUTING.md</a> for code style and the consistency contract - parameter order, <code class="language-plaintext highlighter-rouge">-Format</code>/<code class="language-plaintext highlighter-rouge">-OutputType</code>, property names, etc.).</li>
  <li><strong>Run tests</strong> from the repo root: <code class="language-plaintext highlighter-rouge">Invoke-Pester ./Tests/Consistency.Tests.ps1</code>.</li>
  <li><strong>Commit and push</strong> to your fork, then open a <strong>pull request</strong> against the main repository. Fill in the PR template (summary, how to test, related issue if any).</li>
</ul>

<p>New cmdlets must be added to <code class="language-plaintext highlighter-rouge">module/Public/</code>, registered in <code class="language-plaintext highlighter-rouge">FunctionsToExport</code> in the manifest, and documented in <code class="language-plaintext highlighter-rouge">docs/CMDLET-REFERENCE.md</code>. The consistency tests must be updated if you add or change exported cmdlets or key parameters.</p>

<h2 id="how-to-report-bugs">How to Report Bugs</h2>

<p>If something doesn’t work as expected:</p>

<ul>
  <li>Go to <a href="https://github.com/royklo/InforcerCommunity/issues/new">New issue</a>.</li>
  <li>Choose <strong>Bug report</strong>.</li>
  <li>Fill in:</li>
</ul>

<p><strong>Description</strong>: What went wrong?</p>

<ul>
  <li><strong>Steps to reproduce</strong>: Exact commands or steps.</li>
  <li><strong>Expected behavior</strong>: What you expected.</li>
  <li><strong>Actual behavior</strong>: What happened instead (including any error messages).</li>
  <li><strong>Environment</strong>: PowerShell version, OS, and module version (e.g. <code class="language-plaintext highlighter-rouge">Get-Module InforcerCommunity | Select-Object Version</code>).</li>
  <li><strong>Additional context</strong>: Logs, screenshots, or other details.</li>
</ul>

<p>This helps maintainers and the community reproduce and fix issues quickly.</p>

<h2 id="how-to-request-a-feature">How to Request a Feature</h2>

<p>Have an idea for a new cmdlet, parameter, or behaviour?</p>

<ul>
  <li>Go to <a href="https://github.com/royklo/InforcerCommunity/issues/new">New issue</a>.</li>
  <li>Choose <strong>Feature request</strong>.</li>
  <li>Describe:</li>
</ul>

<p>The <strong>feature</strong> you’d like (e.g. a new endpoint, a new parameter, or a different output shape).</p>

<ul>
  <li>The <strong>use case</strong> (why it would help you or others).</li>
  <li>If you have one, a <strong>proposed solution</strong> (e.g. cmdlet name, parameters, example usage).</li>
</ul>

<p>Not every request can be implemented immediately, but all are read and considered; they also help others discover and discuss ideas.</p>

<h2 id="conclusion">Conclusion</h2>

<p>InforcerCommunity turns the Inforcer API into a set of PowerShell cmdlets you can use interactively or in scripts: connect once, then list tenants, baselines, policies, alignment details, users, groups, roles, and audit events with consistent parameters and output. Run compliance assessments like Copilot Readiness and CIS Benchmarks against one tenant or all of them at once, with interactive HTML matrix reports that let you compare compliance across your entire estate. Generate complete tenant documentation in HTML, Markdown, or Excel. Compare two tenants’ Intune configurations side-by-side with an interactive report that shows every difference, duplicate, and deprecated setting. Use tenant names instead of IDs, pipe results between cmdlets, and export to CSV or JSON for integration with other tools and automation pipelines. It’s a community project, not owned or maintained by Inforcer; feedback, bug reports, and feature requests from users like you shape what comes next. Install it from the <a href="https://www.powershellgallery.com/packages/InforcerCommunity">PowerShell Gallery</a>, try the <a href="#quick-start">quick start</a>, and if you hit a bug or have an idea, open an <a href="https://github.com/royklo/InforcerCommunity/issues">issue</a> or send a <a href="https://github.com/royklo/InforcerCommunity/pulls">pull request</a>.</p>

<h2 id="related">Related</h2>

<ul>
  <li><a href="/posts/msp-license-ladder-01-hunting-gap/">The MSP License Ladder #1: The Hunting Gap</a> - why MSPs need Defender for Endpoint Plan 2, with a multi-tenant hunting script.</li>
  <li><a href="/posts/rksolutions-powershell-module/">RKSolutions PowerShell Module</a> - another PowerShell module for M365 reporting across Intune, Entra ID, and license management.</li>
</ul>]]></content>

      
      
      <author>
          <name>Roy Klooster</name>
      </author>

      
        <category term="Conditional Access" />
      
        <category term="Exchange Online" />
      
        <category term="Entra ID" />
      
        <category term="Graph API" />
      
        <category term="Intune" />
      
        <category term="Reports" />
      
        <category term="Tools" />
      

      
      
        <summary type="html"><![CDATA[InforcerCommunity is a community-built PowerShell module for the Inforcer REST API. Version 0.4.0 adds assessment cmdlets with multi-tenant matrix reports, cross-tenant environment comparison, friendly setting names, and cmdlets for Entra ID groups and roles. Designed for MSPs managing multiple M365 tenants through Inforcer.]]></summary>
      
    </entry>
  
    <entry>
      

      <title type="html">RKSolutions PowerShell Module</title>
      <link href="https://rksolutions.nl/posts/rksolutions-powershell-module/" rel="alternate" type="text/html" title="RKSolutions PowerShell Module" />
      <published>2026-02-23T00:00:00+00:00</published>
      <updated>2026-02-23T00:00:00+00:00</updated>
      <id>https://rksolutions.nl/posts/rksolutions-powershell-module</id>
      <content type="html" xml:base="https://rksolutions.nl/posts/rksolutions-powershell-module/"><![CDATA[<p>If you manage Microsoft 365 tenants, you already know the value of a single view for enrollment flows, anomalies, admin roles, license assignment, and custom security attributes. But automation and scripting often mean wrestling with Microsoft Graph APIs, building your own auth and error handling, and maintaining scripts that break when the API changes. <strong>RKSolutions</strong> is a PowerShell module that connects once to Microsoft Graph and lets you run Intune enrollment flows, Intune anomalies, Entra admin roles, M365 license assignment, and Custom Security Attributes reports from the command line or your own scripts, with consistent connection handling, multiple auth methods, and HTML (and optionally CSV/Excel) output. This guide explains what it does, how to use it, and how you can contribute or ask for new features.</p>

<p><strong>Table of Contents</strong></p>

<ul>
  <li><a href="#what-is-the-rksolutions-powershell-module">What is the RKSolutions PowerShell module?</a></li>
  <li><a href="#whats-new-in-v110">What’s new in v1.1.0</a></li>
  <li><a href="#requirements">Requirements</a></li>
  <li><a href="#installation">Installation</a>
    <ul>
      <li><a href="#option-1-from-powershell-gallery-recommended">Option 1: From PowerShell Gallery (recommended)</a></li>
      <li><a href="#option-2-from-source-github">Option 2: From source (GitHub)</a></li>
    </ul>
  </li>
  <li><a href="#key-cmdlets-and-use-cases">Key Cmdlets and Use Cases</a></li>
  <li><a href="#quick-start">Quick Start</a></li>
  <li><a href="#output-formats-and-filtering">Output Formats and Filtering</a></li>
  <li><a href="#how-to-contribute">How to Contribute</a></li>
  <li><a href="#how-to-request-a-feature">How to Request a Feature</a></li>
  <li><a href="#conclusion">Conclusion</a></li>
</ul>

<h2 id="what-is-the-rksolutions-powershell-module">What is the RKSolutions PowerShell module?</h2>

<p><strong>RKSolutions</strong> is a PowerShell script module that talks to the <strong>Microsoft Graph API</strong> to produce report-style output (HTML by default, with optional CSV or Excel). The reports were first released as separate scripts on the <a href="https://www.powershellgallery.com/profiles/Royklo">PowerShell Gallery</a>; the module brings them together in one install with shared auth and consistent parameters.</p>

<p><strong>What it gives you:</strong></p>

<ul>
  <li><strong>Connect once, run every report:</strong> Authenticate with Microsoft Graph (interactive, Client Secret, Certificate, Managed Identity, or Access Token).</li>
  <li><strong>Six report cmdlets, one module:</strong> Intune enrollment flows, Intune anomalies, Entra admin roles, M365 license assignment, and Custom Security Attributes - all from a single <code class="language-plaintext highlighter-rouge">Install-Module</code>.</li>
  <li><strong>Consistent behavior:</strong> All report cmdlets use the same connection from <code class="language-plaintext highlighter-rouge">Connect-RKGraph</code>; they support parameters such as <code class="language-plaintext highlighter-rouge">-ExportPath</code>, <code class="language-plaintext highlighter-rouge">-SendEmail</code>, <code class="language-plaintext highlighter-rouge">-Recipient</code>, and <code class="language-plaintext highlighter-rouge">-From</code> where applicable, and share the same permission model so one connection covers all reports.</li>
  <li><strong>Unified report design:</strong> Every report uses a shared HTML template with consistent branding, Geist/Geist Mono typography, light/dark theme toggle, and interactive DataTables with search, export (Excel/CSV/PDF), and column visibility controls.</li>
  <li><strong>**PowerShell 7.0+:</strong> Runs on Windows, macOS, and Linux.</li>
  <li><strong>No secrets in scripts:</strong> Credentials are handled by the Microsoft Graph session; you connect once with <code class="language-plaintext highlighter-rouge">Connect-RKGraph</code> and then run as many report commands as you need.</li>
  <li><strong>Tab completion and help:</strong> Every cmdlet has comment-based help; <code class="language-plaintext highlighter-rouge">Get-Help Connect-RKGraph -Full</code> and tab completion on parameters work out of the box.</li>
</ul>

<p><strong>Where to find it:</strong></p>

<ul>
  <li><strong>Source code and issues:</strong> <a href="https://github.com/royklo/RKSolutions-Module">GitHub repository</a> · <a href="https://github.com/royklo/RKSolutions-Module/issues">Open new issue</a> · <a href="https://github.com/royklo/RKSolutions-Module/pulls">Pull requests</a></li>
  <li><strong>PowerShell Gallery:</strong> <a href="https://www.powershellgallery.com/packages/RKSolutions">RKSolutions</a></li>
</ul>

<h2 id="whats-new-in-v110">What’s new in v1.1.0</h2>

<p><strong>New report: Custom Security Attributes.</strong> <code class="language-plaintext highlighter-rouge">Get-CustomSecurityAttributesReport</code> auto-discovers all attribute sets in your tenant and generates an interactive report showing which users, devices, and enterprise applications have which attributes assigned. The report includes an overview tab with a coverage matrix, plus a dedicated tab per attribute set with searchable tables. If you’ve set up Custom Security Attributes following our <a href="/posts/forgotten-features-series-part-3-the-metadata-revolution-youre-missing-custom-security-attributes/">Forgotten Features Part 3</a> guide, this cmdlet gives you instant visibility into how they’re being used.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Discover and report all attribute sets
Get-CustomSecurityAttributesReport

# Report on a specific attribute set
Get-CustomSecurityAttributesReport -AttributeSet "ComplianceData"
</code></pre></div></div>

<p><strong>Unified report design.</strong> All reports now share the same HTML template with consistent branding, Geist/Geist Mono typography, pill-style tab navigation, light/dark theme toggle, and interactive DataTables with search, export, and column visibility. The standalone scripts each had their own HTML - the module unifies them into one polished design.</p>

<p><strong>Security hardening.</strong> All report generators now HTML-encode Graph API data before rendering, preventing stored XSS through display names or attribute values. Filter dropdowns use safe DOM API methods, and the email sender parameter is validated before use.</p>

<p><strong>Performance.</strong> Hot loops that previously used PowerShell’s quadratic <code class="language-plaintext highlighter-rouge">$array += </code> pattern now use <code class="language-plaintext highlighter-rouge">List[PSObject].Add()</code> for linear performance in large tenants.</p>

<h2 id="requirements">Requirements</h2>

<ul>
  <li><strong>Powershell 7.0 or higher</strong></li>
  <li><strong>Module:</strong> <code class="language-plaintext highlighter-rouge">Microsoft.Graph.Authentication</code> (required by RKSolutions; install via <code class="language-plaintext highlighter-rouge">Install-Module Microsoft.Graph.Authentication</code>).</li>
  <li><strong>Permissions:</strong><code class="language-plaintext highlighter-rouge">Connect-RKGraph</code> uses a default set of Microsoft Graph scopes so one connection works for all report cmdlets. Grant these to your app or consent interactively as needed. Full list and per-cmdlet breakdown: <a href="https://github.com/royklo/RKSolutions-Module/blob/main/docs/PERMISSIONS.md">docs/PERMISSIONS.md</a>.
    <ul>
      <li><code class="language-plaintext highlighter-rouge">User.Read</code></li>
      <li><code class="language-plaintext highlighter-rouge">User.Read.All</code></li>
      <li><code class="language-plaintext highlighter-rouge">Group.Read.All</code></li>
      <li><code class="language-plaintext highlighter-rouge">GroupMember.Read.All</code></li>
      <li><code class="language-plaintext highlighter-rouge">Device.Read.All</code></li>
      <li><code class="language-plaintext highlighter-rouge">DeviceManagementConfiguration.Read.All</code></li>
      <li><code class="language-plaintext highlighter-rouge">DeviceManagementApps.Read.All</code></li>
      <li><code class="language-plaintext highlighter-rouge">DeviceManagementManagedDevices.Read.All</code></li>
      <li><code class="language-plaintext highlighter-rouge">DeviceManagementServiceConfig.Read.All</code></li>
      <li><code class="language-plaintext highlighter-rouge">Directory.Read.All</code></li>
      <li><code class="language-plaintext highlighter-rouge">Organization.Read.All</code></li>
      <li><code class="language-plaintext highlighter-rouge">AuditLog.Read.All</code></li>
      <li><code class="language-plaintext highlighter-rouge">RoleManagement.Read.Directory</code></li>
      <li><code class="language-plaintext highlighter-rouge">RoleAssignmentSchedule.Read.Directory</code></li>
      <li><code class="language-plaintext highlighter-rouge">PrivilegedEligibilitySchedule.Read.AzureADGroup</code></li>
      <li><code class="language-plaintext highlighter-rouge">Mail.Send</code></li>
      <li><code class="language-plaintext highlighter-rouge">CloudLicensing.Read</code></li>
      <li><code class="language-plaintext highlighter-rouge">CloudPC.Read.All</code></li>
      <li><code class="language-plaintext highlighter-rouge">CustomSecAttributeAssignment.Read.All</code></li>
      <li><code class="language-plaintext highlighter-rouge">CustomSecAttributeDefinition.Read.All</code></li>
    </ul>
  </li>
</ul>

<h2 id="installation">Installation</h2>

<h3 id="option-1-from-powershell-gallery-recommended">Option 1: From PowerShell Gallery (recommended)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Install-Module -Name RKSolutions -Scope CurrentUser
</code></pre></div></div>

<h3 id="option-2-from-source-github">Option 2: From source (GitHub)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/royklo/RKSolutions-Module.git
cd RKSolutions-Module
Import-Module ./module/RKSolutions.psd1 -Force
</code></pre></div></div>

<p><strong>Important:</strong> When loading from source, always run <code class="language-plaintext highlighter-rouge">Import-Module</code> from the <strong>repository root</strong> and use the path <code class="language-plaintext highlighter-rouge">./module/RKSolutions.psd1</code>.</p>

<h2 id="key-cmdlets-and-use-cases">Key Cmdlets and Use Cases</h2>

<table>
  <thead>
    <tr>
      <th>Cmdlet</th>
      <th>What it does</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Connect-RKGraph</code></td>
      <td>Establishes a Microsoft Graph session (Interactive, ClientSecret, Certificate, Identity, or AccessToken).</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Disconnect-RKGraph</code></td>
      <td>Clears the session and disconnects.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Get-IntuneEnrollmentFlowsReport</code></td>
      <td>Assignment overview only (<code class="language-plaintext highlighter-rouge">-AssignmentOverviewOnly</code>) or device-level flow for a given device (<code class="language-plaintext highlighter-rouge">-Device</code>). Optional <code class="language-plaintext highlighter-rouge">-MermaidOverview</code> to export a .mmd diagram. Output: HTML and optionally CSV.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Get-IntuneAnomaliesReport</code></td>
      <td>Intune anomalies HTML report. Optional email: <code class="language-plaintext highlighter-rouge">-SendEmail</code>, <code class="language-plaintext highlighter-rouge">-Recipient</code>, <code class="language-plaintext highlighter-rouge">-From</code>. Optional <code class="language-plaintext highlighter-rouge">-ExportPath</code>.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Get-EntraAdminRolesReport</code></td>
      <td>Entra ID admin roles HTML report. Optional <code class="language-plaintext highlighter-rouge">-SendEmail</code>, <code class="language-plaintext highlighter-rouge">-Recipient</code>, <code class="language-plaintext highlighter-rouge">-From</code>, <code class="language-plaintext highlighter-rouge">-ExportPath</code>, <code class="language-plaintext highlighter-rouge">-DebugMode</code>.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Get-M365LicenseAssignmentReport</code></td>
      <td>M365 license assignment HTML report with filtering and export to Excel, CSV, or PDF. Optional <code class="language-plaintext highlighter-rouge">-SendEmail</code>, <code class="language-plaintext highlighter-rouge">-Recipient</code>, <code class="language-plaintext highlighter-rouge">-From</code>, <code class="language-plaintext highlighter-rouge">-ExportPath</code>.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Get-CustomSecurityAttributesReport</code></td>
      <td>Custom Security Attributes report across users, devices, and enterprise apps. Auto-discovers all attribute sets or targets a specific one with <code class="language-plaintext highlighter-rouge">-AttributeSet</code>. Optional <code class="language-plaintext highlighter-rouge">-SendEmail</code>, <code class="language-plaintext highlighter-rouge">-Recipient</code>, <code class="language-plaintext highlighter-rouge">-From</code>, <code class="language-plaintext highlighter-rouge">-ExportPath</code>.</td>
    </tr>
  </tbody>
</table>

<p>Typical workflows:</p>

<ul>
  <li><strong>Tenant and assignment overview:</strong> <code class="language-plaintext highlighter-rouge">Connect-RKGraph</code> → <code class="language-plaintext highlighter-rouge">Get-IntuneEnrollmentFlowsReport -AssignmentOverviewOnly</code> to see all Intune assignments in one HTML report.</li>
  <li><strong>Device troubleshooting:</strong> <code class="language-plaintext highlighter-rouge">Get-IntuneEnrollmentFlowsReport -Device 'DeviceName'</code> (or Intune device ID / Entra device object ID) for a step-by-step flow</li>
  <li><strong>Compliance and auditing:</strong> <code class="language-plaintext highlighter-rouge">Get-EntraAdminRolesReport</code> and <code class="language-plaintext highlighter-rouge">Get-M365LicenseAssignmentReport</code> for admin roles and license assignment; use <code class="language-plaintext highlighter-rouge">-ExportPath</code> to save to a specific location, or <code class="language-plaintext highlighter-rouge">-SendEmail</code> to mail the report.</li>
  <li><strong>Custom Security Attributes:</strong> <code class="language-plaintext highlighter-rouge">Get-CustomSecurityAttributesReport</code> to report on attribute assignments across users, devices, and enterprise apps with coverage metrics.</li>
</ul>

<p>For full parameter details and example output, see the <strong>Cmdlet Reference</strong> in the repository: <a href="https://github.com/royklo/RKSolutions-Module/blob/main/docs/CMDLET-REFERENCE.md">docs/CMDLET-REFERENCE.md</a>. For which Graph permissions each cmdlet uses: <a href="https://github.com/royklo/RKSolutions-Module/blob/main/docs/PERMISSIONS.md">docs/PERMISSIONS.md</a>.</p>

<h2 id="quick-start">Quick Start</h2>

<p>After installing the module, connect once with <code class="language-plaintext highlighter-rouge">Connect-RKGraph</code>, then run any report cmdlet. When you’re done, run <code class="language-plaintext highlighter-rouge">Disconnect-RKGraph</code>.</p>

<p><strong>1. Connect (interactive)</strong></p>

<p>A browser opens for sign-in. After consent, the session is stored and all report cmdlets use it.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Connect-RKGraph
</code></pre></div></div>

<p><strong>2. Run a report</strong></p>

<p>Pick any of the report cmdlets. Each produces an HTML file in the current folder by default.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Intune assignment overview (all profiles, compliance policies, app assignments; no device needed)
Get-IntuneEnrollmentFlowsReport -AssignmentOverviewOnly

# M365 license assignment
Get-M365LicenseAssignmentReport

# Entra admin roles
Get-EntraAdminRolesReport

# Intune anomalies
Get-IntuneAnomaliesReport

# Custom Security Attributes (all sets auto-discovered)
Get-CustomSecurityAttributesReport
</code></pre></div></div>

<p><strong>3. Intune flow for a specific device</strong></p>

<p>Use <code class="language-plaintext highlighter-rouge">-Device</code> with the device name, Intune device ID, or Entra device object ID. Optionally export CSV or a Mermaid diagram.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Get-IntuneEnrollmentFlowsReport -Device 'DESKTOP-ABC123'

# With CSV export to a folder
Get-IntuneEnrollmentFlowsReport -Device 'DESKTOP-ABC123' -ExportToCsv -ExportFolder 'C:\Reports'

# Export the same flow as a Mermaid .mmd file
Get-IntuneEnrollmentFlowsReport -Device 'DESKTOP-ABC123' -MermaidOverview
</code></pre></div></div>

<p><strong>4. Save report to a specific path</strong></p>

<p>Use <code class="language-plaintext highlighter-rouge">-ExportPath</code> to control where the HTML (or other output) is written.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Get-M365LicenseAssignmentReport -ExportPath 'C:\Reports\M365-Licenses.html'
Get-EntraAdminRolesReport -ExportPath 'D:\Output\EntraRoles.html'
</code></pre></div></div>

<p><strong>5. Email a report</strong></p>

<p>Several report cmdlets support <code class="language-plaintext highlighter-rouge">-SendEmail</code>, <code class="language-plaintext highlighter-rouge">-Recipient</code>, and <code class="language-plaintext highlighter-rouge">-From</code> (requires <code class="language-plaintext highlighter-rouge">Mail.Send</code> and a valid From address).</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Get-EntraAdminRolesReport -SendEmail -Recipient 'admin@contoso.com' -From 'reports@contoso.com'
Get-M365LicenseAssignmentReport -SendEmail -Recipient 'admin@contoso.com' -From 'reports@contoso.com'
</code></pre></div></div>

<p><strong>6. Disconnect</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Disconnect-RKGraph
</code></pre></div></div>

<p>For automation (scheduled tasks, CI), use <code class="language-plaintext highlighter-rouge">Connect-RKGraph</code> with <code class="language-plaintext highlighter-rouge">-TenantId</code>, <code class="language-plaintext highlighter-rouge">-ClientId</code>, and <code class="language-plaintext highlighter-rouge">-ClientSecret</code>, or with <code class="language-plaintext highlighter-rouge">-CertificateThumbprint</code> or <code class="language-plaintext highlighter-rouge">-Identity</code>. See the <a href="https://github.com/royklo/RKSolutions-Module/blob/main/docs/CMDLET-REFERENCE.md">cmdlet reference</a> for full parameter details.</p>

<p>You can use <code class="language-plaintext highlighter-rouge">Get-Help &lt;CmdletName&gt; -Full</code> for parameters and examples (e.g. <code class="language-plaintext highlighter-rouge">Get-Help Get-IntuneEnrollmentFlowsReport -Full</code>).</p>

<h2 id="output-formats-and-filtering">Output Formats and Filtering</h2>

<ul>
  <li><strong>Default output:</strong> All report cmdlets produce <strong>HTML</strong> by default, written to the current folder (or to the path you set with <code class="language-plaintext highlighter-rouge">-ExportPath</code> or report-specific path parameters).</li>
  <li><strong>CSV/Excel:</strong> <code class="language-plaintext highlighter-rouge">Get-IntuneEnrollmentFlowsReport</code> supports <code class="language-plaintext highlighter-rouge">-ExportToCsv</code> and <code class="language-plaintext highlighter-rouge">-ExportFolder</code>. <code class="language-plaintext highlighter-rouge">Get-M365LicenseAssignmentReport</code>supports export to Excel, CSV, or PDF.</li>
  <li><strong>Export path:</strong> Use <code class="language-plaintext highlighter-rouge">-ExportPath</code> (or the cmdlet’s path parameter) to save the report to a specific file or folder.</li>
  <li><strong>Email:</strong> Several report cmdlets support <code class="language-plaintext highlighter-rouge">-SendEmail</code>, <code class="language-plaintext highlighter-rouge">-Recipient</code>, and <code class="language-plaintext highlighter-rouge">-From</code> (requires <code class="language-plaintext highlighter-rouge">Mail.Send</code> and a valid From address).</li>
</ul>

<p>Example: run the license report and save to a custom path:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Get-M365LicenseAssignmentReport -ExportPath 'C:\Reports\M365-Licenses-2025.html'
</code></pre></div></div>

<p>Example: run the Entra admin roles report and email it:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Get-EntraAdminRolesReport -SendEmail -Recipient 'admin@contoso.com' -From 'reports@contoso.com'
</code></pre></div></div>

<h2 id="how-to-contribute">How to Contribute</h2>

<p>Contributions are welcome. The project uses a standard fork-and-pull-request workflow:</p>

<ul>
  <li><strong>Fork</strong> the repository on GitHub: <a href="https://github.com/royklo/RKSolutions-Module">github.com/royklo/RKSolutions-Module</a>.</li>
  <li><strong>Clone your fork</strong> and create a branch (e.g. <code class="language-plaintext highlighter-rouge">feature/your-feature-name</code> or <code class="language-plaintext highlighter-rouge">fix/bug-description</code>).</li>
  <li><strong>Make your changes</strong> under <a href="https://github.com/royklo/RKSolutions-Module/tree/main/module">module/</a> (see <a href="https://github.com/royklo/RKSolutions-Module/blob/main/CONTRIBUTING.md">CONTRIBUTING.md</a> for development setup, code style, and the consistency contract).</li>
  <li><strong>Run tests</strong> from the repo root: <code class="language-plaintext highlighter-rouge">Invoke-Pester ./Tests/Consistency.Tests.ps1</code> (see <a href="https://github.com/royklo/RKSolutions-Module/tree/main/Tests">Tests/</a>).</li>
  <li><strong>Commit and push</strong> to your fork, then open a <a href="https://github.com/royklo/RKSolutions-Module/pulls">pull request</a> against the main repository. Fill in the PR template (summary, how to test, related issue if any).</li>
</ul>

<p>New cmdlets must be added to <a href="https://github.com/royklo/RKSolutions-Module/tree/main/module/Public">module/Public/</a>, registered in <code class="language-plaintext highlighter-rouge">FunctionsToExport</code> in the <a href="https://github.com/royklo/RKSolutions-Module/blob/main/module/RKSolutions.psd1">manifest</a>, and documented in <a href="https://github.com/royklo/RKSolutions-Module/blob/main/docs/CMDLET-REFERENCE.md">docs/CMDLET-REFERENCE.md</a>. If a cmdlet calls Microsoft Graph, update its required scopes, <code class="language-plaintext highlighter-rouge">Connect-RKGraph</code> default scopes if needed, and <a href="https://github.com/royklo/RKSolutions-Module/blob/main/docs/PERMISSIONS.md">docs/PERMISSIONS.md</a>. The <a href="https://github.com/royklo/RKSolutions-Module/blob/main/Tests/Consistency.Tests.ps1">consistency tests</a> must be updated if you add or change exported cmdlets or key parameters.</p>

<h2 id="how-to-request-a-feature">How to Request a Feature</h2>

<p>Have an idea for a new cmdlet, parameter, or behaviour?</p>

<ul>
  <li>Go to <strong>New issue:</strong> <a href="https://github.com/royklo/RKSolutions-Module/issues/new">Open new issue</a> or use the <a href="https://github.com/royklo/RKSolutions-Module/issues/new?template=feature_request.md">Feature request template</a>.</li>
  <li>Choose <strong>Feature request</strong> (if not already selected by the template).</li>
  <li>Describe:</li>
</ul>

<p>The <strong>feature</strong> you’d like (e.g. a new report, a new parameter, or a different output format).</p>

<ul>
  <li>The <strong>use case</strong> (why it would help you or others).</li>
  <li>If you have one, a <strong>proposed solution</strong> (e.g. cmdlet name, parameters, example usage).</li>
</ul>

<p>Not every request can be implemented immediately, but all are read and considered; they also help others discover and discuss ideas.</p>

<h2 id="conclusion">Conclusion</h2>

<p>RKSolutions turns Microsoft Graph into a set of PowerShell report cmdlets you can use interactively or in scripts: connect once with <code class="language-plaintext highlighter-rouge">Connect-RKGraph</code>, then run Intune enrollment flows, Intune anomalies, Entra admin roles, M365 license assignment, and Custom Security Attributes reports with consistent connection handling and output. It’s a community project, not owned or maintained by Microsoft; feedback, bug reports, and feature requests from users like you shape what comes next. Install it from the <a href="https://www.powershellgallery.com/packages/RKSolutions">PowerShell Gallery</a>, try the <a href="#quick-start">quick start</a> above, and if you hit a bug or have an idea, open an <a href="https://github.com/royklo/RKSolutions-Module/issues/new">issue</a> or send a <a href="https://github.com/royklo/RKSolutions-Module/pulls">pull request</a>.</p>

<h2 id="related">Related</h2>

<ul>
  <li><a href="/posts/entra-id-admin-roles-report/">Entra ID Admin Roles Report</a> - standalone script for auditing all Entra ID admin role assignments.</li>
  <li><a href="/posts/m365-license-assignment-report/">M365 License Assignment Report</a> - standalone script for reporting on license assignments and usage.</li>
  <li><a href="/posts/intune-anomalies-report-script/">Intune Anomalies Report</a> - standalone script for detecting compliance, deployment, and device anomalies.</li>
  <li><a href="/posts/forgotten-features-series-part-3-the-metadata-revolution-youre-missing-custom-security-attributes/">Custom Security Attributes (Forgotten Features Part 3)</a> - guide to setting up and using Custom Security Attributes.</li>
  <li><a href="/posts/inforcercommunity-a-powershell-module-for-the-inforcer-api/">InforcerCommunity PowerShell Module</a> - a complementary module for MSPs using the Inforcer platform.</li>
</ul>]]></content>

      
      
      <author>
          <name>Roy Klooster</name>
      </author>

      
        <category term="Entra ID" />
      
        <category term="Graph API" />
      
        <category term="Intune" />
      
        <category term="Reports" />
      
        <category term="Tools" />
      

      
      
        <summary type="html"><![CDATA[RKSolutions is a PowerShell module that connects to Microsoft Graph and generates Intune enrollment, anomaly, Entra admin role, M365 license, and Custom Security Attributes reports from a single command. Supports interactive, certificate, and client secret auth with HTML and CSV/Excel output.]]></summary>
      
    </entry>
  
    <entry>
      

      <title type="html">Forgotten Features Series, Part 5: The Defender Tag Bridge You’re Not Using – Custom MDE Tags via Intune</title>
      <link href="https://rksolutions.nl/posts/forgotten-features-series-part-5-the-defender-tag-bridge-youre-not-using-custom-mde-tags-via-intune/" rel="alternate" type="text/html" title="Forgotten Features Series, Part 5: The Defender Tag Bridge You’re Not Using – Custom MDE Tags via Intune" />
      <published>2026-02-09T00:00:00+00:00</published>
      <updated>2026-02-09T00:00:00+00:00</updated>
      <id>https://rksolutions.nl/posts/forgotten-features-series-part-5-the-defender-tag-bridge-youre-not-using-custom-mde-tags-via-intune</id>
      <content type="html" xml:base="https://rksolutions.nl/posts/forgotten-features-series-part-5-the-defender-tag-bridge-youre-not-using-custom-mde-tags-via-intune/"><![CDATA[<p>You can push custom Microsoft Defender for Endpoint (MDE) device tags straight from Intune with a single OMA-URI configuration profile - no scripts, no manual tagging in the Defender portal. The tag flows from Intune into Defender, where it drives device groups, RBAC, and automated remediation. Here’s how to set it up.</p>

<p>It’s the fifth post in this series on forgotten Microsoft 365 features - capabilities that already ship in the products but rarely get switched on. This one turns the device types you already manage in Intune (shared, kiosk, BYOD, or by department) into Defender for Endpoint tags, and it’s well worth a second look.</p>

<p><strong>Table of Contents</strong></p>

<ul>
  <li><a href="#what-are-microsoft-defender-for-endpoint-device-tags">What are Microsoft Defender for Endpoint Device Tags?</a></li>
  <li><a href="#what-do-you-need-to-get-started">What do you need to get started?</a></li>
  <li><a href="#how-do-you-set-it-up">How do you set it up?</a>
    <ul>
      <li><a href="#step-1-plan-your-tags">Step 1: Plan your tags</a></li>
      <li><a href="#step-2-create-a-custom-oma-uri-profile-in-intune">Step 2: Create a custom OMA-URI profile in Intune</a></li>
      <li><a href="#step-3-optional-apply-via-powershell-instead-of-oma-uri">Step 3 (optional): Apply via Powershell instead of OMA-URI</a></li>
      <li><a href="#step-4-use-tags-in-defender-for-endpoint">Step 4: Use tags in Defender for Endpoint</a></li>
    </ul>
  </li>
  <li><a href="#examples">Examples</a>
    <ul>
      <li><a href="#scenario-1-device-type---shared-kiosk-and-byod">Scenario 1: Device type - shared, kiosk, and BYOD</a></li>
      <li><a href="#scenario-2-department-based-tagging-engineering-hr-finance">Scenario 2: Department-based tagging (Engineering, HR, Finance)</a></li>
    </ul>
  </li>
  <li><a href="#what-do-the-results-look-like">What do the results look like?</a></li>
  <li><a href="#conclusion">Conclusion</a></li>
</ul>

<h2 id="what-are-microsoft-defender-for-endpoint-device-tags">What are Microsoft Defender for Endpoint Device Tags?</h2>

<p>Device tags in Defender for Endpoint are labels you assign to machines, making it easy to group, filter, and remediate them - segmenting your environment by something like device type or department.</p>

<p>In Defender, device groups define which rules apply and who has administrative control. Each device belongs to one group, which controls auto-remediation levels and RBAC permissions. Tags act as the attribute that places devices into the right group.</p>

<p>Because the tag is defined in Intune and reported by the device, Intune stays the single source of truth - which also means a few rules are worth knowing before you deploy:</p>

<blockquote>
  <ul>
    <li>Each Intune policy applies one tag (the “Group” value).</li>
  </ul>
</blockquote>

<ul>
  <li>Tags have a 200-character limit; avoid parentheses and commas if you filter by tag in Defender.</li>
  <li>Tags deployed from Intune are reported by the device, so changes must happen in Intune or the script - editing tags directly in Defender will be overwritten on the next device report.</li>
  <li>There may be some delay before tags appear in the Defender portal.</li>
</ul>

<p>More details: <a href="https://learn.microsoft.com/defender-endpoint/machine-tags">Create and manage device tags - Microsoft Defender for Endpoint</a>.</p>

<h2 id="what-do-you-need-to-get-started">What do you need to get started?</h2>

<ul>
  <li><strong>Licensing:</strong> Microsoft Defender for Endpoint Plan 1 or Plan 2 or Defender for Business</li>
  <li><strong>Management:</strong> Devices must be managed by Microsoft Intune</li>
</ul>

<h2 id="how-do-you-set-it-up">How do you set it up?</h2>

<p>We’re going to use a OMA-URI solution for this. But it’s also possible via Powershell script (see step 3). This profile tells the Windows Defender Advanced Threat Protection client which tag to report to Defender. Setting this up is straightforward once you know where to look.</p>

<h3 id="step-1-plan-your-tags">Step 1: Plan your tags</h3>

<p>Decide on tag values that match how you already segment devices in Intune, like device type (Shared, Kiosk, BYOD, Corporate) or department (Engineering, HR, Finance, IT). These tags will drive device group membership in Defender, influencing RBAC and auto-remediation. Keeping names aligned ensures consistent reporting and policy application.</p>

<h3 id="step-2-create-a-custom-oma-uri-profile-in-intune">Step 2: Create a custom OMA-URI profile in Intune</h3>

<ul>
  <li>Sign in to the <a href="https://intune.microsoft.com/">Microsoft Intune</a>.</li>
  <li>Navigate to <strong>Devices &gt; Windows &gt; Configuration profiles</strong>.</li>
  <li>Click <strong>+ Create &gt; New policy</strong>.</li>
  <li>Select <strong>Platform: Windows 10 and later</strong>. For Profile type, choose <strong>Templates &gt; Custom</strong> (or Custom under Settings catalog if applicable).</li>
  <li>Enter a name like <em>Win - Defender - MDE Tags for Kiosk Devices</em>.</li>
  <li>Under Configuration settings, add an OMA-URI row with:</li>
</ul>

<p><strong>OMA-URI:</strong> <code class="language-plaintext highlighter-rouge">./Device/Vendor/MSFT/WindowsAdvancedThreatProtection/DeviceTagging/Group</code></p>

<ul>
  <li><strong>Data type:</strong> String</li>
  <li><strong>Value:</strong> Your tag name (e.g.* KioskDevice*)</li>
  <li>Save, then assign the profile to the relevant assignment (one profile per tag).</li>
</ul>

<p>The <code class="language-plaintext highlighter-rouge">./Device/Vendor/MSFT/WindowsAdvancedThreatProtection/DeviceTagging/Group</code> node is documented in the <a href="https://learn.microsoft.com/windows/client-management/mdm/windowsadvancedthreatprotection-csp">WindowsAdvancedThreatProtection CSP reference</a>.</p>

<h3 id="step-3-optional-apply-via-powershell-instead-of-oma-uri">Step 3 (optional): Apply via Powershell instead of OMA-URI</h3>

<p><strong>Example PowerShell script (run as system, e.g. via Intune PowerShell scripts):</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$registryPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows Advanced Threat Protection\DeviceTagging"
$name = "Group"
$tag = "KioskDevice" # Change per deployment: Shared, Kiosk, BYOD, Engineering, etc.

if (!(Test-Path $registryPath)) {
 New-Item -Path $registryPath -Force | Out-Null
}
Set-ItemProperty -Path $registryPath -Name $name -Value $tag -PropertyType String -Force

</code></pre></div></div>

<p>Deploy via <strong>Devices</strong> &gt; <strong>Script and Remediations</strong> &gt; <strong>Remediations or Platform Scripts</strong> in Intune, one script per tag value, assigned to the same device groups you would use for the OMA-URI profile. To remove the tag, set the <code class="language-plaintext highlighter-rouge">Group</code> value to an empty string; do not delete the key. The device tag is included in the daily device information report; a restart can trigger a new report sooner.</p>

<h3 id="step-4-use-tags-in-defender-for-endpoint">Step 4: Use tags in Defender for Endpoint</h3>

<p>In the Defender portal, use <strong>Assets &gt; Devices Device inventory</strong> and the <strong>Tags</strong> filter to see only devices with a given tag (e.g. all <code class="language-plaintext highlighter-rouge">Shared</code> or all <code class="language-plaintext highlighter-rouge">Engineering</code>). In <strong>Settings</strong> &gt; <strong>Endpoints</strong> &gt; <strong>Device groups</strong>, create or edit a device group and set membership to include devices where <strong>Tag</strong> equals your Intune-deployed tag; that group then drives RBAC and auto-remediation level. You can also filter and group by tag when triaging alerts or building dynamic lists for response actions.</p>

<h2 id="examples">Examples</h2>

<p>The following scenarios show how device-type and department tags from Intune translate into clearer segmentation and safer automation in Defender for Endpoint.</p>

<h3 id="scenario-1-device-type---shared-kiosk-and-byod">Scenario 1: Device type - shared, kiosk, and BYOD</h3>

<p><strong>Challenge:</strong> You have shared workstations in common areas, kiosks in production and reception, and a growing number of BYOD Windows devices. You want shared and kiosk devices in a device group with limited or no auto-remediation so automated actions don’t disrupt multi-user or locked-down sessions, while corporate and BYOD devices get full remediation. SOC needs to filter by type when investigating incidents.</p>

<p><strong>Solution:</strong> Create one Intune custom profile per device type-tag values <code class="language-plaintext highlighter-rouge">Shared</code>, <code class="language-plaintext highlighter-rouge">Kiosk</code>, and BYOD. Assign each profile to the device groups you already use for those segments (e.g. dynamic or static groups by naming, OU, or enrollment type). In MDE, create device groups whose membership rule is <strong>Tag</strong> equals <code class="language-plaintext highlighter-rouge">Shared</code>, <code class="language-plaintext highlighter-rouge">Kiosk</code> and <code class="language-plaintext highlighter-rouge">BYOD</code>, and set auto-remediation to None or Limited for Shared and Kiosk, and Full for optionally BYOD. Use the Tags filter in Device inventory for quick views by type.</p>

<h3 id="scenario-2-department-based-tagging-engineering-hr-finance">Scenario 2: Department-based tagging (Engineering, HR, Finance)</h3>

<p><strong>Challenge:</strong> You need to report and scope security by department: how many Engineering vs HR vs Finance devices, which alerts affect which business unit, and optionally different RBAC so department-specific analysts see only their department’s devices.</p>

<p><strong>Solution:</strong> Tag devices by department using Intune profiles with values such as <code class="language-plaintext highlighter-rouge">Engineering</code>, <code class="language-plaintext highlighter-rouge">HR</code>, <code class="language-plaintext highlighter-rouge">Finance</code>, <code class="language-plaintext highlighter-rouge">IT</code>. Assign each profile to the corresponding device group (e.g. “Engineering - Windows devices” populated by dynamic rule). In MDE, create device groups by tag (e.g. “Engineering devices,” “Finance devices”) and use them for filtering, reporting, and RBAC. Department leads or tier-1 SOC can filter Device inventory by tag and see only their scope.</p>

<h2 id="what-do-the-results-look-like">What do the results look like?</h2>

<p>After the device syncs and devices report to Defender for Endpoint, the tag appears in the Defender portal. In <strong>Device inventory</strong> you can filter by the tag to see only the devices that received it (in this case it was “<strong><em>BlogPostDemo</em></strong>” as tag). Device groups that use the tag in their membership rule will show the correct devices and apply the chosen RBAC and auto-remediation settings. Below is an example of the tag visible on a device in MDE after deployment from Intune.</p>

<p><img src="/assets/images/2026-02/image-2.png" alt="Microsoft Defender Device Inventory showing devices with BlogPostDemo tag applied via Intune, with Tags column highlighted" /></p>

<h2 id="conclusion">Conclusion</h2>

<p>Forgotten Features Series is about capabilities that are already there - and pushing MDE device tags from Intune is a prime example. One custom OMA-URI profile per tag, assigned to the device groups you already use for shared, kiosk, BYOD, or department, and sync does the rest. Device type and department defined in Intune flow into Defender for Endpoint for reporting, device groups, RBAC, and auto-remediation - without manual tagging in the Security Center or one-off scripts. The value is that you don’t have to bolt on automation or accept that “we can’t tag at scale”; you define the tag in Intune and it flows into MDE.</p>

<p>Because the tag is device-driven when deployed from Intune, changes belong in Intune; editing the tag only in the Defender portal won’t persist. For automatic tagging based on device attributes (name, domain, OS), you can complement this with <a href="https://learn.microsoft.com/en-us/defender-xdr/configure-asset-rules">dynamic rules for device tagging</a> in Microsoft Defender XDR asset rule management.</p>

<h2 id="related">Related</h2>

<ul>
  <li><a href="/posts/asr-rules-the-importance-of-configuration-and-verifying-with-the-asr-rule-inspector/">ASR Rules: Verifying with the ASR Rule Inspector</a> - after tagging devices for Defender, verify that Attack Surface Reduction rules are actually enforced on those endpoints.</li>
</ul>]]></content>

      
      
      <author>
          <name>Roy Klooster</name>
      </author>

      
        <category term="Intune" />
      

      
      
        <summary type="html"><![CDATA[Custom MDE device tags can be pushed directly from Intune via a single OMA-URI configuration profile, with no scripts or manual tagging in the Security Center. This lets you automatically map Intune device categories like shared, kiosk, or BYOD into Defender for Endpoint tags for scoped hunting and alerting.]]></summary>
      
    </entry>
  
</feed>
