<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://kostaskoutrou.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://kostaskoutrou.github.io/" rel="alternate" type="text/html" /><updated>2026-07-18T11:22:26+00:00</updated><id>https://kostaskoutrou.github.io/feed.xml</id><title type="html">Kostas SecBlog</title><subtitle>Simple(r) Security</subtitle><author><name>Kostas Koutroumpouchos</name></author><entry><title type="html">Using Windows Events to identify detections from MDE - Getting to know MDE Part 4</title><link href="https://kostaskoutrou.github.io/2026/07/18/using-windows-events-for-mde.html" rel="alternate" type="text/html" title="Using Windows Events to identify detections from MDE - Getting to know MDE Part 4" /><published>2026-07-18T00:00:00+00:00</published><updated>2026-07-18T00:00:00+00:00</updated><id>https://kostaskoutrou.github.io/2026/07/18/using-windows-events-for-mde</id><content type="html" xml:base="https://kostaskoutrou.github.io/2026/07/18/using-windows-events-for-mde.html"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>Welcome to the 4th part of the blog post series “Getting to know MDE”!</p>

<p>In this series, each post explores a different component, feature, or methodology for understanding and managing Microsoft Defender for Endpoint (MDE).</p>

<blockquote>
  <p>In this post, the focus is on <strong>Windows Events: What Event IDs to look for when investigating for blocks or detections generated by MDE, and using XPath queries to quickly filter for the needed Events.</strong></p>
</blockquote>

<p>Each section briefly describes a different MDE capability, including what Event IDs to look for, and an XPath query to use when searching for events of that capability.</p>

<p>If you want to get a tl;dr XPath query from this blog post which includes all the detections, check the last section.</p>

<p>This post was inspired by the previous post regarding <a href="https://kostaskoutrou.github.io/2026/01/06/using-kql-for-mde.html#device-control">Using KQL to identify detections from MDE</a>, as well as the <a href="https://learn.microsoft.com/en-us/defender-endpoint/attack-surface-reduction-windows-events#custom-xml-templates-for-attack-surface-reduction-events">XPath queries provided by Microsoft</a>, which were expanded upon to search for all Defender features.</p>

<p>So let’s get started.</p>

<hr />

<h2 id="contents">Contents</h2>

<ul id="markdown-toc">
  <li><a href="#introduction" id="markdown-toc-introduction">Introduction</a></li>
  <li><a href="#contents" id="markdown-toc-contents">Contents</a></li>
  <li><a href="#event-viewer-general-info" id="markdown-toc-event-viewer-general-info">Event Viewer general info</a>    <ul>
      <li><a href="#xpath-queries" id="markdown-toc-xpath-queries">XPath queries</a></li>
    </ul>
  </li>
  <li><a href="#demo-files" id="markdown-toc-demo-files">Demo Files</a></li>
  <li><a href="#xpath-queries-for-defender-components" id="markdown-toc-xpath-queries-for-defender-components">XPath Queries for Defender components</a>    <ul>
      <li><a href="#mdav-changes-logging" id="markdown-toc-mdav-changes-logging">MDAV Changes logging</a></li>
      <li><a href="#attack-surface-reduction-asr-rules" id="markdown-toc-attack-surface-reduction-asr-rules">Attack Surface Reduction (ASR) rules</a></li>
      <li><a href="#controlled-folder-access-cfa" id="markdown-toc-controlled-folder-access-cfa">Controlled Folder Access (CFA)</a></li>
      <li><a href="#device-control" id="markdown-toc-device-control">Device Control</a></li>
      <li><a href="#exploit-protection" id="markdown-toc-exploit-protection">Exploit Protection</a></li>
      <li><a href="#network-protection-web-protection-and-smartscreen" id="markdown-toc-network-protection-web-protection-and-smartscreen">Network Protection, Web Protection, and SmartScreen</a></li>
      <li><a href="#tamper-protection" id="markdown-toc-tamper-protection">Tamper Protection</a></li>
      <li><a href="#microsoft-defender-antivirus-mdav-detections" id="markdown-toc-microsoft-defender-antivirus-mdav-detections">Microsoft Defender Antivirus (MDAV) Detections</a></li>
      <li><a href="#potentially-unwanted-apps-pua" id="markdown-toc-potentially-unwanted-apps-pua">Potentially Unwanted Apps (PUA)</a></li>
      <li><a href="#app-control-for-business-and-applocker" id="markdown-toc-app-control-for-business-and-applocker">App Control for Business and AppLocker</a></li>
      <li><a href="#mde" id="markdown-toc-mde">MDE</a></li>
      <li><a href="#general-xpath-query-for-all-detections" id="markdown-toc-general-xpath-query-for-all-detections">General XPath query for all detections</a></li>
    </ul>
  </li>
  <li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
</ul>

<hr />

<h2 id="event-viewer-general-info">Event Viewer general info</h2>

<p>Before starting with the Windows Events of each MDE capability, to get an idea of how a Windows Event is structured, an example of an Event reporting the detection and quarantine of a malicious file is provided below:</p>

<pre><code class="language-xml">&lt;Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event"&gt;
  &lt;System&gt;
    &lt;Provider Name="Microsoft-Windows-Windows Defender" Guid="{11cd958a-c507-4ef3-b3f2-5fd9dfbd2c78}" /&gt;
    &lt;EventID&gt;1117&lt;/EventID&gt;
    &lt;Version&gt;0&lt;/Version&gt;
    &lt;Level&gt;4&lt;/Level&gt;
    &lt;Task&gt;0&lt;/Task&gt;
    &lt;Opcode&gt;0&lt;/Opcode&gt;
    &lt;Keywords&gt;0x8000000000000000&lt;/Keywords&gt;
    &lt;TimeCreated SystemTime="2026-01-22T11:02:01.1593627Z" /&gt;
    &lt;EventRecordID&gt;37116&lt;/EventRecordID&gt;
    &lt;Correlation ActivityID="{5768510e-0445-477f-bf75-2a37805d0ef0}" /&gt;
    &lt;Execution ProcessID="5224" ThreadID="24524" /&gt;
    &lt;Channel&gt;Microsoft-Windows-Windows Defender/Operational&lt;/Channel&gt;
    &lt;Computer&gt;DESKTOP-ABC&lt;/Computer&gt;
    &lt;Security UserID="S-1-5-18" /&gt;
  &lt;/System&gt;
  &lt;EventData&gt;
    &lt;Data Name="Product Name"&gt;Microsoft Defender Antivirus&lt;/Data&gt;
    &lt;Data Name="Product Version"&gt;4.18.25110.6&lt;/Data&gt;
    &lt;Data Name="Detection ID"&gt;{CE415EB5-1414-4FDE-B466-541D57C3035A}&lt;/Data&gt;
    &lt;Data Name="Detection Time"&gt;2026-01-22T10:59:42.891Z&lt;/Data&gt;
    &lt;Data Name="Threat ID"&gt;2147536539&lt;/Data&gt;
    &lt;Data Name="Threat Name"&gt;Exploit:HTML/IframeRef.gen&lt;/Data&gt;
    &lt;Data Name="Severity ID"&gt;5&lt;/Data&gt;
    &lt;Data Name="Severity Name"&gt;Severe&lt;/Data&gt;
    &lt;Data Name="Category ID"&gt;30&lt;/Data&gt;
    &lt;Data Name="Category Name"&gt;Exploit&lt;/Data&gt;
    &lt;Data Name="FWLink"&gt;https://go.microsoft.com/fwlink/?linkid=37020&amp;amp;name=Exploit:HTML/IframeRef.gen&amp;amp;threatid=2147536539&amp;amp;enterprise=0&lt;/Data&gt;
    &lt;Data Name="Status Code"&gt;3&lt;/Data&gt;
    &lt;Data Name="Status Description"&gt;&lt;/Data&gt;
    &lt;Data Name="State"&gt;2&lt;/Data&gt;
    &lt;Data Name="Source ID"&gt;2&lt;/Data&gt;
    &lt;Data Name="Source Name"&gt;System&lt;/Data&gt;
    &lt;Data Name="Process Name"&gt;Unknown&lt;/Data&gt;
    &lt;Data Name="Detection User"&gt;NT AUTHORITY\SYSTEM&lt;/Data&gt;
    &lt;Data Name="Path"&gt;containerfile:_E:\phishing_datasets.zip; file:_E:\phishing_datasets.zip-&amp;gt;phishing_datasets/private-phishing4.mbox-&amp;gt;(IframeRefI)&lt;/Data&gt;
    &lt;Data Name="Origin ID"&gt;1&lt;/Data&gt;
    &lt;Data Name="Origin Name"&gt;Local machine&lt;/Data&gt;
    &lt;Data Name="Execution ID"&gt;0&lt;/Data&gt;
    &lt;Data Name="Execution Name"&gt;Unknown&lt;/Data&gt;
    &lt;Data Name="Type ID"&gt;2&lt;/Data&gt;
    &lt;Data Name="Type Name"&gt;Generic&lt;/Data&gt;
    &lt;Data Name="Pre Execution Status"&gt;0&lt;/Data&gt;
    &lt;Data Name="Action ID"&gt;2&lt;/Data&gt;
    &lt;Data Name="Action Name"&gt;Quarantine&lt;/Data&gt;
    &lt;Data Name="Error Code"&gt;0x00000000&lt;/Data&gt;
    &lt;Data Name="Error Description"&gt;The operation completed successfully. &lt;/Data&gt;
    &lt;Data Name="Post Clean Status"&gt;0&lt;/Data&gt;
    &lt;Data Name="Additional Actions ID"&gt;0&lt;/Data&gt;
    &lt;Data Name="Additional Actions String"&gt;No additional actions required&lt;/Data&gt;
    &lt;Data Name="Remediation User"&gt;NT AUTHORITY\SYSTEM&lt;/Data&gt;
    &lt;Data Name="Security intelligence Version"&gt;AV: 1.443.793.0, AS: 1.443.793.0, NIS: 1.443.793.0&lt;/Data&gt;
    &lt;Data Name="Engine Version"&gt;AM: 1.1.25110.1, NIS: 1.1.25110.1&lt;/Data&gt;
  &lt;/EventData&gt;
&lt;/Event&gt;
</code></pre>

<p>It may look like the event includes a lot of information initially, but in reality most of it is not really useful for typical troubleshooting. Also, Windows Event Viewer parses these logs and presents them in a user-friendly format:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/2546a011-ddba-4d45-8ca0-13ddfa722073" /></p>

<p>Every Windows Event log is divided into two main sections:</p>

<ul>
  <li><strong>The <code>&lt;System&gt;</code> Block</strong>: This contains the <strong>standard metadata</strong> generated by Windows. It tells when it happened, what computer it happened on (DESKTOP-ABC), and where the log is saved (Windows Defender/Operational).</li>
  <li><strong>The <code>&lt;EventData&gt;</code> Block</strong>: This contains the <strong>custom payload</strong> for the specific event. For Windows Defender, this is where to find the exact details about the malware, its location, and what the antivirus decided to do about it.</li>
</ul>

<h3 id="xpath-queries">XPath queries</h3>

<p>A great thing about the Windows Event Viewer is that <strong>all of its logs are stored as XML data with the strictly enforced XML schema</strong> depicted previously. This allows to create custom views using XPath queries, where only the needed events are filtered. XPath (XML Path Language) is a query language used to find and extract specific information from an XML document.</p>

<p>Using XPath allows for creating shareable filtered <strong>views where only the detections and blocks done by Defender are shown</strong>, which results in similar reports to the ones created using the KQL queries shown in the previous post <a href="https://kostaskoutrou.github.io/2026/01/06/using-kql-for-mde.html">Using KQL to identify detections from MDE - Getting to know MDE Part 2</a>.</p>

<p>All the XPath queries used here have the following format:</p>

<pre><code class="language-xml">&lt;QueryList&gt;
  &lt;Query Id="0" Path="Microsoft-Windows-Windows Defender/Operational"&gt;
   &lt;Select Path="Microsoft-Windows-Windows Defender/Operational"&gt;*[System[(EventID=... or EventID=... or ...)]]&lt;/Select&gt;
   &lt;Select&gt; more Paths filters... &lt;/Select&gt;
  &lt;/Query&gt;
&lt;/QueryList&gt;
</code></pre>

<p>The only difference between the queries are <strong>Windows Event channel paths</strong> and the <strong>Event IDs</strong> searched for. To not repeat the explanation of the query under each section, a general explanation is provided here:</p>

<ol>
  <li><code>Query Id="0" Path="..."</code>: Default path to look for is “Microsoft-Windows-Windows Defender/Operational”. This is needed to be stated even though other paths are specified below because the schema rules dictate that this container must declare a starting point. Even if you try to skip it, when you save the Custom View in Windows Event Viewer, a path will be added automatically.</li>
  <li><code>Select Path="Microsoft-Windows-Windows Defender/Operational"&gt;</code>: Search in the path “Microsoft-Windows-Windows Defender/Operational”</li>
  <li><code>*[System[(EventID=... or EventID=... or EventID=...)]]</code>:</li>
  <li><code>*</code>: Search at any root event, i.e., grab all events in the log as a starting point.</li>
  <li><code>[...]</code>: The contents within the brackets in XPath are used to apply a filter to the item just before it, i.e., the asterisk, i.e., all the events.</li>
  <li><code>System</code>: As mentioned before, every Windows Event is split into two main XML sections: <code>&lt;System&gt;</code> (metadata like time, provider and Event ID) and <code>&lt;EventData&gt;</code> (the details of what happened). Here we are filtering the <code>&lt;System&gt;</code> section.</li>
  <li>The brackets again filter to the item just before, i.e., the <code>&lt;System&gt;</code> section.</li>
  <li><code>EventID=...</code>: With this condition the specified Event IDs are filtered.</li>
</ol>

<p>There are 2 ways to import an XPath query in Windows Event Viewer:</p>

<p><strong>1) Create a Custom View</strong></p>

<p>Open Event Viewer and click on <code>Create Custom View...</code>.</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/7820ffb5-0d1e-4da3-b4b8-b1c3039f54c6" /></p>

<p>Click on the <code>XML</code> tab and check the <code>Edit query manually</code> checkbox.</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/d041f4e2-770c-4f39-a261-f0333497e8f8" /></p>

<p>Click Yes on the warning pop-up:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/e1d23a8b-a1a9-44b4-add2-b6a9a3cdf4af" /></p>

<p>You can now paste the XPath queries provided on this post and save them accordingly.</p>

<p><strong>2) Import a Custom View</strong></p>

<p>If you have saved any of the XPath queries as XML files, you can directly import them by clicking on the <code>Import Custom View...</code> in Windows Event Viewer:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/a01dc539-1f02-4046-ace8-56a72dd01539" /></p>

<p><strong>Exporting Results</strong></p>

<p>After importing the custom view, you can export the results to a CSV, txt, XML, or EVTX (Event Files) format by clicking on <code>Save All Events in Custom View As...</code>:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/4cc67355-9e5c-45b9-9572-52888c83e356" /></p>

<p>In the next sections, XPath queries are provided for each Defender feature. These can be used to facilitate troubleshooting towards identifying false positive detections and blocks. More details on the specific XPath queries are found in the sections below.</p>

<h2 id="demo-files">Demo Files</h2>

<p>If there are no Events for a specific feature and there is a need to produce some for confirmation of the query working as expected, there are <a href="https://learn.microsoft.com/en-us/defender-endpoint/defender-endpoint-demonstrations">demo files provided by Microsoft</a>, which can be used to force Defender detections and produce Windows Events.</p>

<h2 id="xpath-queries-for-defender-components">XPath Queries for Defender components</h2>

<h3 id="mdav-changes-logging">MDAV Changes logging</h3>

<p>Starting with the <strong>logging of changes to configurations</strong>, an interesting and useful event related to all Microsoft Defender Antivirus (MDAV) features is Event ID <strong>5007</strong> with description <strong>“Event when settings are changed”</strong>. A lot of events with Event ID 5007 are logged because it includes any change happening to the MDAV configuration, such as:</p>

<ul>
  <li>Signature/Engine updates</li>
  <li>Policy syncs from Group Policy, Intune, SCCM</li>
  <li>Cloud-Delivered Protection Adjustments</li>
  <li>Any MDAV feature changing states (e.g., ASR rule switching from Audit mode to Block mode)</li>
</ul>

<p>Investigating these logs is useful to identify suspicious <strong>tampering</strong> of the MDAV configuration, like disabling ASR rules or Real-Time protection, as well as to <strong>troubleshoot MDAV behavior that started unexpectedly</strong>, such as to identify when an ASR rule switched from Audit mode to Block mode.</p>

<p>In addition to the Event ID above, there are the following Event IDs related to changing settings in Defender:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center">Event ID</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><strong>5000</strong></td>
      <td><strong>Real-time protection enabled</strong>: Microsoft Defender Antivirus real-time protection scanning for malware and other potentially unwanted software was enabled.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>5001</strong></td>
      <td><strong>Real-time protection disabled</strong>: Microsoft Defender Antivirus real-time protection scanning for malware and other potentially unwanted software was disabled.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>5004</strong></td>
      <td>Microsoft Defender Antivirus real-time protection feature <strong>configuration changed</strong>.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>5007</strong></td>
      <td>The antimalware platform configuration changed.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>5009</strong></td>
      <td><strong>Antispyware enabled</strong>: Microsoft Defender Antivirus enabled scanning for malware and other potentially unwanted software.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>5010</strong></td>
      <td><strong>Antispyware disabled</strong>: Microsoft Defender Antivirus scanning for malware and other potentially unwanted software is disabled.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>5011</strong></td>
      <td><strong>Malware protection enabled</strong>: Microsoft Defender Antivirus scanning for viruses is enabled.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>5012</strong></td>
      <td><strong>Malware protection disabled</strong>: Microsoft Defender Antivirus scanning for viruses is disabled.</td>
    </tr>
  </tbody>
</table>

<p>To search for these Event IDs, which can be found under <strong>Applications and Services Logs &gt; Microsoft &gt; Windows &gt; Windows Defender &gt; Operational</strong>, the following XPath query can be used:</p>

<pre><code class="language-xml">&lt;QueryList&gt;
  &lt;Query Id="0" Path="Microsoft-Windows-Windows Defender/Operational"&gt;
   &lt;Select Path="Microsoft-Windows-Windows Defender/Operational"&gt;
    *[System[EventID=5000 or EventID=5001 or EventID=5004 or 
    EventID=5007 or (EventID &amp;gt;= 5009 and EventID &amp;lt;= 5012)]]
  &lt;/Select&gt;
  &lt;/Query&gt;
&lt;/QueryList&gt;
</code></pre>

<h3 id="attack-surface-reduction-asr-rules">Attack Surface Reduction (ASR) rules</h3>

<p>Attack Surface Reduction (ASR) rules in MDAV protect against <strong>risky software behavior</strong> commonly exploited, such as:</p>

<ul>
  <li>Running unsigned drivers or signed by an untrusted CA.</li>
  <li>Injecting code into other processes.</li>
  <li>Running Office Macros.</li>
  <li>Running software that has low prevalence across organizations.</li>
</ul>

<p>For more info about ASR rules, visit <a href="https://learn.microsoft.com/en-us/defender-endpoint/attack-surface-reduction-rules-overview">Microsoft’s relevant documentation</a>.</p>

<p>When it comes to Windows Event logs, the ASR rule events are located in the Windows Event log under <strong>Applications and Services Logs &gt; Microsoft &gt; Windows &gt; Windows Defender &gt; Operational</strong>. The following event IDs are related to ASR rules:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center">Event ID</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><strong>1121</strong></td>
      <td>Event when an ASR rule fires in <strong>block mode</strong></td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>1122</strong></td>
      <td>Event when an ASR rule fires in <strong>audit mode</strong></td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>1129</strong></td>
      <td>Event when the <strong>user overrides</strong> an ASR rule block in <strong>warn mode</strong></td>
    </tr>
  </tbody>
</table>

<p>The following XPath query will search for ASR rule detections, blocks, and user overrides:</p>

<pre><code class="language-xml">&lt;QueryList&gt;
  &lt;Query Id="0" Path="Microsoft-Windows-Windows Defender/Operational"&gt;
   &lt;Select Path="Microsoft-Windows-Windows Defender/Operational"&gt;
    *[System[(EventID=1121 or EventID=1122 or EventID=1129)]]
   &lt;/Select&gt;
  &lt;/Query&gt;
&lt;/QueryList&gt;
</code></pre>

<p>If <strong>blocks only</strong> are needed, just keep Event ID 1121:</p>

<pre><code class="language-xml">&lt;QueryList&gt;
  &lt;Query Id="0" Path="Microsoft-Windows-Windows Defender/Operational"&gt;
   &lt;Select Path="Microsoft-Windows-Windows Defender/Operational"&gt;
    *[System[(EventID=1121)]]
   &lt;/Select&gt;
  &lt;/Query&gt;
&lt;/QueryList&gt;
</code></pre>

<h3 id="controlled-folder-access-cfa">Controlled Folder Access (CFA)</h3>

<p>The basic idea with CFA is that a set of directories (folders) and a set of applications are defined. Only that set of apps is allowed to process in any way that defined set of directories. For more info, click <a href="https://learn.microsoft.com/en-us/defender-endpoint/controlled-folder-access-overview">here</a></p>

<p>When it comes to Windows Event logs, the CFA events are located under <strong>Applications and Services Logs &gt; Microsoft &gt; Windows &gt; Windows Defender &gt; Operational</strong>. The following event IDs are related to CFA:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center">Event ID</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><strong>1123</strong></td>
      <td><strong>Blocked</strong> controlled folder access event</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>1124</strong></td>
      <td><strong>Audited</strong> controlled folder access event</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>1127</strong></td>
      <td>Controlled Folder Access <strong>blocked</strong> an untrusted process from potentially modifying disk sectors. In general, these events are more severe, because they indicate that an attempt was made to write directly to the hard drive, bypassing the file system.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>1128</strong></td>
      <td><strong>Audited</strong> controlled folder access sector write block event. In general, these events are more severe, because they indicate that an attempt was made to write directly to the hard drive, bypassing the file system.</td>
    </tr>
  </tbody>
</table>

<p>The following XPath query will filter for all detection and block events:</p>

<pre><code class="language-xml">&lt;QueryList&gt;
  &lt;Query Id="0" Path="Microsoft-Windows-Windows Defender/Operational"&gt;
   &lt;Select Path="Microsoft-Windows-Windows Defender/Operational"&gt;
    *[System[(EventID=1123 or EventID=1124 or EventID=1127 or EventID=1128)]]
   &lt;/Select&gt;
  &lt;/Query&gt;
&lt;/QueryList&gt;
</code></pre>

<p>For blocks only, use the following:</p>

<pre><code class="language-xml">&lt;QueryList&gt;
  &lt;Query Id="0" Path="Microsoft-Windows-Windows Defender/Operational"&gt;
   &lt;Select Path="Microsoft-Windows-Windows Defender/Operational"&gt;
    *[System[(EventID=1123 or EventID=1127)]]
   &lt;/Select&gt;
  &lt;/Query&gt;
&lt;/QueryList&gt;
</code></pre>

<h3 id="device-control">Device Control</h3>

<p><a href="https://learn.microsoft.com/en-us/defender-endpoint/device-control-overview">Device Control</a> enables controls related to usage and installation of peripheral (USB/Bluetooth) or other devices on endpoints.</p>

<p>When it comes to Windows Event logs, for Device Control there is no specific set of events or a specific path where all the events reside. It is more complicated to identify block events. I could not find an exhaustive set of events to look for to ensure all Device Control events are monitored. The most relevant events are logged under</p>

<ul>
  <li><strong>Security</strong></li>
  <li><strong>Microsoft &gt; Windows &gt; Kernel &gt; PnP &gt; Configuration</strong></li>
  <li><strong>Microsoft &gt; Windows &gt; DeviceSetupManager &gt; Admin</strong></li>
  <li><strong>Microsoft &gt; Windows &gt; PrintService &gt; Admin</strong></li>
</ul>

<table>
  <thead>
    <tr>
      <th style="text-align: center">Event ID</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><strong>6423</strong></td>
      <td>The system blocked the installation of a hardware device (USB, Bluetooth radio, etc.) due to device installation restrictions.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>871</strong></td>
      <td>A print job was explicitly blocked by Defender Device Control or Removable Storage Access Control.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>808</strong></td>
      <td>The print spooler blocked a plug-in module from loading, often due to untrusted driver execution policies.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>215</strong></td>
      <td>A printer driver failed to install due to a security policy or restriction.</td>
    </tr>
  </tbody>
</table>

<p>The XPath query for the Event IDs above is this following:</p>

<pre><code class="language-xml">&lt;QueryList&gt;
  &lt;Query Id="0" Path="Security"&gt;
    &lt;Select Path="Security"&gt;
      *[System[EventID=6423]]
    &lt;/Select&gt;
    &lt;Select Path="Microsoft-Windows-PrintService/Admin"&gt;
      *[System[(EventID=871 or EventID=808 or EventID=215)]]
    &lt;/Select&gt;
  &lt;/Query&gt;
&lt;/QueryList&gt;
</code></pre>

<h3 id="exploit-protection">Exploit Protection</h3>

<p>Exploit Protection helps protect against malware that uses exploits to infect devices and spread. It consists of many mitigations that can be applied to either the whole OS or individual apps. For more info, click <a href="https://learn.microsoft.com/en-us/defender-endpoint/exploit-protection">here</a>.</p>

<p>When it comes to Windows Event logs, most Exploit Protection events are located in the Windows Event log under <strong>Security-Mitigations &gt; Kernel Mode and Security-Mitigations &gt; User Mode</strong>, while some are located in <strong>WER-Diagnostics &gt; Operational</strong> and <strong>Win32k &gt; Operational</strong>. The following event IDs are related to CFA:</p>

<p><strong>Security-Mitigations &gt; Kernel Mode</strong> and <strong>Security-Mitigations &gt; User Mode</strong></p>

<table>
  <thead>
    <tr>
      <th style="text-align: center">Event ID</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><strong>1</strong></td>
      <td>Arbitrary Code Guard (ACG) audit</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>2</strong></td>
      <td>Arbitrary Code Guard (ACG) enforce</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>3</strong></td>
      <td>Don’t allow child processes audit</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>4</strong></td>
      <td>Don’t allow child processes block</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>5</strong></td>
      <td>Block low integrity images audit</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>6</strong></td>
      <td>Block low integrity images block</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>7</strong></td>
      <td>Block remote images audit</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8</strong></td>
      <td>Block remote images block</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>9</strong></td>
      <td>Disable win32k system calls audit</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>10</strong></td>
      <td>Disable win32k system calls block</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>11</strong></td>
      <td>Code integrity guard audit</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>12</strong></td>
      <td>Code integrity guard block</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>13</strong></td>
      <td>Export Address Filtering (EAF) audit</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>14</strong></td>
      <td>Export Address Filtering (EAF) enforce</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>15</strong></td>
      <td>Export Address Filtering+ (EAF+) audit</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>16</strong></td>
      <td>Export Address Filtering+ (EAF+) enforce</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>17</strong></td>
      <td>Import Address Filtering (IAF) audit</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>18</strong></td>
      <td>Import Address Filtering (IAF) enforce</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>19</strong></td>
      <td>Return-Oriented Programming (ROP) StackPivot audit</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>20</strong></td>
      <td>Return-Oriented Programming (ROP) StackPivot enforce</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>21</strong></td>
      <td>Return-Oriented Programming (ROP) CallerCheck audit</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>22</strong></td>
      <td>Return-Oriented Programming (ROP) CallerCheck enforce</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>23</strong></td>
      <td>Return-Oriented Programming (ROP) SimExec audit</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>24</strong></td>
      <td>Return-Oriented Programming (ROP) SimExec enforce</td>
    </tr>
  </tbody>
</table>

<p><strong>WER-Diagnostics &gt; Operational</strong></p>

<table>
  <thead>
    <tr>
      <th style="text-align: center">Event ID</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><strong>5</strong></td>
      <td>Control Flow Guard (CFG) Block</td>
    </tr>
  </tbody>
</table>

<p><strong>Win32k &gt; Operational</strong></p>

<table>
  <thead>
    <tr>
      <th style="text-align: center">Event ID</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><strong>260</strong></td>
      <td>Untrusted Font</td>
    </tr>
  </tbody>
</table>

<p>The following XPath query will filter for all <strong>detection and block events</strong> for Exploit Protection:</p>

<pre><code class="language-xml">&lt;QueryList&gt;
  &lt;Query Id="0" Path="Microsoft-Windows-Security-Mitigations/KernelMode"&gt;
   &lt;Select Path="Microsoft-Windows-Security-Mitigations/KernelMode"&gt;
    *[System[(EventID &amp;gt;= 1 and EventID &amp;lt;= 24) or EventID=260]]
   &lt;/Select&gt;
   &lt;Select Path="Microsoft-Windows-Security-Mitigations/UserMode"&gt;
    *[System[(EventID &amp;gt;= 1 and EventID &amp;lt;= 24) or EventID=260]]
   &lt;/Select&gt;
   &lt;Select Path="Microsoft-Windows-Win32k/Operational"&gt;
    *[System[(EventID &amp;gt;= 1 and EventID &amp;lt;= 24) or EventID=260]]
   &lt;/Select&gt;
   &lt;Select Path="Microsoft-Windows-WER-Diagnostics/Operational"&gt;
    *[System[(EventID &amp;gt;= 1 and EventID &amp;lt;= 24) or EventID=260]]
   &lt;/Select&gt;
  &lt;/Query&gt;
&lt;/QueryList&gt;
</code></pre>

<p>For <strong>blocks only</strong>, the following XPath query can be used:</p>

<pre><code class="language-xml">&lt;QueryList&gt;
  &lt;Query Id="0" Path="Microsoft-Windows-Security-Mitigations/KernelMode"&gt;
    &lt;Select Path="Microsoft-Windows-Security-Mitigations/KernelMode"&gt;
      *[System[(EventID=2 or EventID=4 or EventID=6 or EventID=8 or
      EventID=10 or EventID=12 or EventID=14 or EventID=16 or
      EventID=18 or EventID=20 or EventID=22 or EventID=24)]]
    &lt;/Select&gt;
    &lt;Select Path="Microsoft-Windows-Security-Mitigations/UserMode"&gt;
      *[System[(EventID=2 or EventID=4 or EventID=6 or EventID=8 or
      EventID=10 or EventID=12 or EventID=14 or EventID=16 or
      EventID=18 or EventID=20 or EventID=22 or EventID=24)]]
    &lt;/Select&gt;
    &lt;Select Path="Microsoft-Windows-WER-Diagnostics/Operational"&gt;
      *[System[(EventID=5)]]
    &lt;/Select&gt;
    &lt;Select Path="Microsoft-Windows-Win32k/Operational"&gt;
      *[System[(EventID=260)]]
    &lt;/Select&gt;
  &lt;/Query&gt;
&lt;/QueryList&gt;
</code></pre>

<h3 id="network-protection-web-protection-and-smartscreen">Network Protection, Web Protection, and SmartScreen</h3>

<p>The combination of Network Protection, Web Protection, and SmartScreen provides the ability to <strong>block access to malicious websites, custom indicators, and web content filtering</strong>. For more info, click <a href="https://learn.microsoft.com/en-us/defender-endpoint/network-protection">here</a>.</p>

<p>When it comes to Windows Event logs, the Network Protection events are located in the Windows Event log under <strong>Applications and Services Logs &gt; Microsoft &gt; Windows &gt; Windows Defender &gt; Operational</strong>. The following event IDs are related to Network Protection:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center">Event ID</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><strong>1125</strong></td>
      <td>Event when network protection fires in <strong>audit</strong> mode</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>1126</strong></td>
      <td>Event when network protection fires in <strong>block</strong> mode</td>
    </tr>
  </tbody>
</table>

<p>The following XPath query can be used to detect these events:</p>

<pre><code class="language-xml">&lt;QueryList&gt;
 &lt;Query Id="0" Path="Microsoft-Windows-Windows Defender/Operational"&gt;
  &lt;Select Path="Microsoft-Windows-Windows Defender/Operational"&gt;
  *[System[(EventID=1125 or EventID=1126)]]
  &lt;/Select&gt;
 &lt;/Query&gt;
&lt;/QueryList&gt;
</code></pre>

<p>If only the <strong>block events</strong> are needed, the audit events can be omitted:</p>

<pre><code class="language-xml">&lt;QueryList&gt;
 &lt;Query Id="0" Path="Microsoft-Windows-Windows Defender/Operational"&gt;
  &lt;Select Path="Microsoft-Windows-Windows Defender/Operational"&gt;
  *[System[(EventID=1126)]]
  &lt;/Select&gt;
 &lt;/Query&gt;
&lt;/QueryList&gt;
</code></pre>

<h3 id="tamper-protection">Tamper Protection</h3>

<p><a href="https://learn.microsoft.com/en-us/defender-endpoint/prevent-changes-to-security-settings-with-tamper-protection">Tamper Protection</a> blocks any changes attempted to several MDAV settings.</p>

<p>When it comes to events generated, the only Event ID that matters for Tamper Protection is located under <strong>Applications and Services Logs &gt; Microsoft &gt; Windows &gt; Windows Defender &gt; Operational</strong>, with Event ID <strong>5013</strong>. The message of this Event ID is <strong>“Tamper protection blocked a change to Microsoft Defender Antivirus”</strong>, and states which setting change was blocked.</p>

<p>The XPath query for this Event ID is the following:</p>

<pre><code class="language-xml">&lt;QueryList&gt;
 &lt;Query Id="0" Path="Microsoft-Windows-Windows Defender/Operational"&gt;
  &lt;Select Path="Microsoft-Windows-Windows Defender/Operational"&gt;
    *[System[(EventID=5013)]]
  &lt;/Select&gt;
 &lt;/Query&gt;
&lt;/QueryList&gt;
</code></pre>

<h3 id="microsoft-defender-antivirus-mdav-detections">Microsoft Defender Antivirus (MDAV) Detections</h3>

<p>MDAV is the <strong>core component of Defender</strong>. With it, process creation events and file download events from the internet are monitored. It not only uses its signature-based engine, but also predictive technologies such as machine learning and cloud-delivered protection to find attacks. For more info, click <a href="https://learn.microsoft.com/en-us/defender-endpoint/microsoft-defender-antivirus-windows">here</a></p>

<p>Regarding Event IDs, there is a thorough documentation <a href="https://learn.microsoft.com/en-us/defender-endpoint/troubleshoot-microsoft-defender-antivirus">here</a>. The Event IDs which mostly relate to detections and blocks are the following:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center">Event ID</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><strong>1006/1116</strong></td>
      <td>The antimalware platform <strong>found</strong> malware or other potentially unwanted software.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>1007/1117</strong></td>
      <td>The antimalware platform <strong>performed an action</strong> to protect your system from malware or other potentially unwanted software.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>1008/1118</strong></td>
      <td>The antimalware platform attempted to perform an action to protect your system from malware or other potentially unwanted software, but the <strong>action failed</strong>.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>1011</strong></td>
      <td>The antimalware platform <strong>deleted an item from quarantine</strong>.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>1012</strong></td>
      <td>The antimalware platform <strong>couldn’t delete an item from quarantine</strong>.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>1015</strong></td>
      <td>The antimalware platform <strong>detected suspicious behavior</strong>.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>1119</strong></td>
      <td>The antimalware platform encountered a <strong>critical error when trying to take action</strong> on malware or other potentially unwanted software.</td>
    </tr>
  </tbody>
</table>

<p>Regarding the first pair of Event IDs, they log  the same event, but the 100x ones are older Event IDs for Windows 7, Windows 8, while the 111x ones are the new ones introduced in Windows 10. All these Event IDs are logged under <strong>Applications and Services Logs &gt; Microsoft &gt; Windows &gt; Windows Defender &gt; Operational.</strong></p>

<p>The XPath query to filter for those events is the following:</p>

<pre><code class="language-xml">&lt;QueryList&gt;
  &lt;Query Id="0" Path="Microsoft-Windows-Windows Defender/Operational"&gt;
   &lt;Select Path="Microsoft-Windows-Windows Defender/Operational"&gt;
    *[System[(EventID=1006 or EventID=1007 or EventID=1008 or
    EventID=1011 or EventID=1012 or EventID=1015 or EventID=1116 or
    EventID=1117 or EventID=1118 or EventID=1119)]]
   &lt;/Select&gt;
  &lt;/Query&gt;
&lt;/QueryList&gt;
</code></pre>

<p>As described in the Event IDs table, <strong>these events log also events of Potentially Unwanted Software/Applications (PUA)</strong>. If the events <strong>not</strong> related to PUA are needed, an additional filter is needed to be applied in the “Category Name” field of the EventData. The following XPath query can be used:</p>

<pre><code class="language-xml">&lt;QueryList&gt;
  &lt;Query Id="0" Path="Microsoft-Windows-Windows Defender/Operational"&gt;
   &lt;Select Path="Microsoft-Windows-Windows Defender/Operational"&gt;
    *[System[(EventID=1006 or EventID=1007 or EventID=1008 or EventID=1011 or 
    EventID=1012 or EventID=1015 or EventID=1116 or EventID=1117 or 
    EventID=1118 or EventID=1119)]]
   &lt;/Select&gt;
   &lt;Suppress Path="Microsoft-Windows-Windows Defender/Operational"&gt;
    *[EventData[Data[@Name='Category Name']='Potentially Unwanted Software']]
   &lt;/Suppress&gt;
  &lt;/Query&gt;
&lt;/QueryList&gt;
</code></pre>

<p>The reason that the <code>&lt;Suppress&gt;</code> tag is used instead of filtering with a <code>EventData[Data[@Name='Category Name']!='Potentially Unwanted Software']</code> condition with a <code>!=</code> is that if there is an event that does not have a “Category Name”, then the condition would output <code>false</code>, removing that log from the query results.</p>

<h3 id="potentially-unwanted-apps-pua">Potentially Unwanted Apps (PUA)</h3>

<p>PUA is a category of software that can:</p>

<ul>
  <li>Cause the machine to run slowly</li>
  <li>Display unexpected apps</li>
  <li>Install other software that might be unwanted</li>
</ul>

<p>For more info, click <a href="https://learn.microsoft.com/en-us/defender-endpoint/detect-block-potentially-unwanted-apps-microsoft-defender-antivirus">here</a>.</p>

<p>As mentioned in the previous section, <strong>PUA is logged under the same Event IDs as MDAV</strong>. Therefore, if events related only PUA are needed, an additional filter is needed to be applied in the “Category Name” field of the EventData. The XPath query that can be used is the following:</p>

<pre><code class="language-xml">&lt;QueryList&gt;
  &lt;Query Id="0" Path="Microsoft-Windows-Windows Defender/Operational"&gt;
   &lt;Select Path="Microsoft-Windows-Windows Defender/Operational"&gt;
    *[EventData[Data[@Name='Category Name']='Potentially Unwanted Software'] and 
    System[(EventID=1006 or EventID=1007 or EventID=1008 or EventID=1011 or 
    EventID=1012 or EventID=1015 or EventID=1116 or EventID=1117 or 
    EventID=1118 or EventID=1119)]]
   &lt;/Select&gt;
  &lt;/Query&gt;
&lt;/QueryList&gt;
</code></pre>

<h3 id="app-control-for-business-and-applocker">App Control for Business and AppLocker</h3>

<p><strong><a href="https://learn.microsoft.com/en-us/windows/security/application-security/application-control/app-control-for-business/applocker/applocker-overview">AppLocker</a></strong> helps control which apps and files users can run. <strong><a href="https://learn.microsoft.com/en-us/windows/security/application-security/application-control/app-control-for-business/appcontrol-and-applocker-overview">App Control for Business</a></strong> also known as <strong>Windows Defender Application Control</strong> (WDAC), formerly known as <strong>Configurable Code Integrity</strong> (CCI), is a newer solution for the same purpose, and provides more granular controls.</p>

<p>When it comes to Windows Events, <strong>AppLocker</strong> logs events under <strong>Application and Services Logs &gt; Microsoft &gt; Windows &gt; AppLocker</strong>, while <strong>App Control</strong> logs its events under the AppLocker path as well as <strong>Application and Services Logs &gt; Microsoft &gt; Windows &gt; CodeIntegrity &gt; Operational</strong>.</p>

<p>The events are quite granular, with categories based on:</p>

<ul>
  <li>the file type of the event</li>
  <li>whether the file that is about to run is there because of a Managed Installer (Intune, SCCM, etc.)</li>
  <li>the feature that is controlling the file execution (AppLocker or WDAC)</li>
</ul>

<p>More info:</p>

<ul>
  <li><a href="https://learn.microsoft.com/en-us/windows/security/application-security/application-control/app-control-for-business/applocker/using-event-viewer-with-applocker">AppLocker events</a></li>
  <li><a href="https://learn.microsoft.com/en-us/windows/security/application-security/application-control/app-control-for-business/operations/event-id-explanations">App Control for Business events</a></li>
</ul>

<p>For <strong>AppLocker only</strong>, the following Event IDs are of interest:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center">Event ID</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><strong>8000</strong></td>
      <td>AppID policy conversion failed. This indicates that the policy wasn’t applied correctly to the computer.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8001</strong></td>
      <td>The AppLocker policy was applied successfully to this computer.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8002</strong></td>
      <td>File was <strong>allowed</strong> to run. (<strong>.exe and .dll</strong> files.). When a process launches that matches a managed installer rule, this event is raised with PolicyName = MANAGEDINSTALLER found in the event Details.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8003</strong></td>
      <td>File was allowed to run but <strong>would have been prevented</strong> from running if the AppLocker policy were enforced. (<strong>.exe and .dll</strong> files.)</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8004</strong></td>
      <td>File was <strong>prevented</strong> from running. (<strong>.exe and .dll</strong> files.)</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8005</strong></td>
      <td>File was <strong>allowed</strong> to run. (<strong>script or .msi</strong> files)</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8006</strong></td>
      <td>File was allowed to run but <strong>would have been prevented</strong> from running if the AppLocker policy were enforced. (<strong>script or .msi</strong> files)</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8007</strong></td>
      <td>File was <strong>prevented</strong> from running. (<strong>script or .msi</strong> files)</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8008</strong></td>
      <td>File: AppLocker component not available on this SKU. Indicates that the Windows edition does not support AppLocker.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8020</strong></td>
      <td>File was <strong>allowed to run</strong>. (<strong>packaged apps .appx, .msix</strong>)</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8021</strong></td>
      <td>File was allowed to <strong>run</strong> but <strong>would have been prevented</strong> from running if the AppLocker policy were enforced. (<strong>packaged apps .appx, .msix</strong>)</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8022</strong></td>
      <td>File was <strong>prevented</strong> from <strong>running</strong>.  (<strong>packaged apps .appx, .msix</strong>)</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8023</strong></td>
      <td>File was <strong>allowed to be installed</strong>. (<strong>packaged apps .appx, .msix</strong>)</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8024</strong></td>
      <td>File was allowed to <strong>install</strong> but <strong>would have been prevented</strong> from running if the AppLocker policy were enforced. (<strong>packaged apps .appx, .msix</strong>)</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8025</strong></td>
      <td>File was <strong>prevented from installing</strong>. (<strong>packaged apps .appx, .msix</strong>)</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8027</strong></td>
      <td>No packaged apps can be executed while Exe rules are being enforced and no Packaged app rules have been configured.</td>
    </tr>
  </tbody>
</table>

<p>For <strong>App Control only</strong>, the following Event IDs are of interest (the table information was taken from the previously provided link regarding <a href="https://learn.microsoft.com/en-us/windows/security/application-security/application-control/app-control-for-business/operations/event-id-explanations">Understanding App Control events</a>:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center">Event ID</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><strong>8028</strong></td>
      <td>This event indicates that a script host, such as PowerShell, queried App Control about a file the script host was about to run. Since the policy was in <strong>audit</strong> mode, the script or MSI file should have run, but wouldn’t have passed the App Control policy if it was enforced.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8029</strong></td>
      <td>This event is the <strong>enforcement</strong> mode equivalent of event 8028.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8036</strong></td>
      <td>COM object was <strong>prevented from running</strong>.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8037</strong></td>
      <td>This event indicates that a script host checked whether to allow a script to run, and the file <strong>passed</strong> the App Control policy.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8038</strong></td>
      <td><strong>Signing information event</strong> correlated with either an 8028 or 8029 event. One 8038 event is generated for each signature of a script file. Contains the total number of signatures on a script file and an index as to which signature it is. Unsigned script files generate a single 8038 event with TotalSignatureCount 0. These events are correlated with 8028 and 8029 events and can be matched using the Correlation ActivityID found in the System portion of the event.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8039</strong></td>
      <td>This event indicates that a packaged app (MSIX/AppX) was allowed to install or run because the App Control policy is in <strong>audit</strong> mode. But, it <strong>would have been blocked</strong> if the policy was enforced.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8040</strong></td>
      <td>This event indicates that a packaged app was <strong>prevented from installing or running</strong> due to the App Control policy.</td>
    </tr>
  </tbody>
</table>

<p>There are also some logs under <strong>Applications and Services logs &gt; Microsoft &gt; Windows &gt; CodeIntegrity &gt; Operational</strong>, focused on App Control policy activation and the control of executables, dlls, and drivers.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center">Event ID</th>
      <th>Explanation</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><strong>3004</strong></td>
      <td>This event isn’t common and may occur with or without an App Control policy present. It typically indicates a <strong>kernel driver tried to load with an invalid signature</strong>. For example, the file may not be WHQL-signed (<a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/install/whql-test-signature-program">Windows Hardware Quality Labs</a>) on a system where WHQL is required.  This event is also seen for kernel- or user-mode code that the developer opted-in to <a href="https://learn.microsoft.com/en-us/cpp/build/reference/integritycheck-require-signature-check?view=msvc-170">/INTEGRITYCHECK</a> but isn’t signed correctly.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>3033</strong></td>
      <td>This event may occur with or without an App Control policy present and <strong>should occur alongside a 3077 event</strong> if caused by App Control policy. It often means the file’s signature is revoked or a signature with the Lifetime Signing EKU has expired. Presence of the Lifetime Signing EKU is the only case where App Control blocks files due to an expired signature. Try using option 20 Enabled:Revoked Expired As Unsigned in your policy along with a rule (for example, hash) that doesn’t rely on the revoked or expired cert.  This event also occurs if code compiled with Code Integrity Guard (CIG) tries to load other code that doesn’t meet the CIG requirements.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>3034</strong></td>
      <td>This event isn’t common. It’s the audit mode equivalent of event 3033.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>3076</strong></td>
      <td>This event is the main App Control block event for <strong>audit</strong> mode policies. It indicates that the file would have been blocked if the policy was enforced.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>3077</strong></td>
      <td>This event is the main App Control block event for <strong>enforced</strong> policies. It indicates that the file didn’t pass your policy and was blocked.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>3089</strong></td>
      <td>This event contains <strong>signature information for files that were blocked or audit blocked by App Control</strong>. One of these events is created for each signature of a file. Each event shows the total number of signatures found and an index value to identify the current signature. Unsigned files generate a single one of these events with TotalSignatureCount of 0. These events are correlated with 3004, 3033, 3034, 3076 and 3077 events. You can match the events using the Correlation ActivityID found in the System portion of the event.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>3090</strong></td>
      <td>Optional: This event indicates that a file was <strong>allowed</strong> to run based purely on <a href="https://learn.microsoft.com/en-us/windows/security/application-security/application-control/app-control-for-business/design/use-appcontrol-with-intelligent-security-graph">ISG</a> or managed installer.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>3091</strong></td>
      <td>This event indicates that a file didn’t have <a href="https://learn.microsoft.com/en-us/windows/security/application-security/application-control/app-control-for-business/design/use-appcontrol-with-intelligent-security-graph">ISG</a> or managed installer authorization and the App Control policy is in <strong>audit</strong> mode.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>3092</strong></td>
      <td>This event is the <strong>enforcement</strong> mode equivalent of 3091.</td>
    </tr>
  </tbody>
</table>

<p>Lastly, for running files installed via a <strong>Managed Installer</strong> (Intune, SCCM, etc.), there are some events which are AppLocker events, but may be triggered by either AppLocker or App Control. More specifically, the Managed Installer feature was initially built for AppLocker. The following two are AppLocker components:</p>

<ol>
  <li>The <a href="https://revertservice.com/10/applockerfltr/">SmartlockerFilter</a> driver that watches the installations and identifies files created by authorized installers</li>
  <li>The <a href="https://learn.microsoft.com/en-us/windows/security/application-security/application-control/app-control-for-business/applocker/configure-the-application-identity-service">AppID</a> service that verifies the tags.</li>
</ol>

<p>These components are used to trust Managed Installers, and track applications which exist on the machine because of a Managed Installer. They can then be used either by AppLocker itself or by App Control to allow or block the execution of Managed Installer managed applications. In this case, the following events are of interest:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center">Event ID</th>
      <th>Explanation</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><strong>8030</strong></td>
      <td>ManagedInstaller check SUCCEEDED during Appid verification of *. A user tried to run a program. Windows checked for the hidden stamp, found it, and confirmed that the trusted Managed Installed installed it. The program is <strong>allowed</strong> to run.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8031</strong></td>
      <td>SmartlockerFilter detected file * being written by process *. The trusted deployment tool is actively downloading or installing a program. The Smartlocker filter driver is watching the tool write the files to the hard drive and stamps them with the hidden “trusted” tag.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8032</strong></td>
      <td>ManagedInstaller check FAILED during Appid verification of *. A user tried to run a program. Windows checked for the hidden stamp, but it wasn’t there. This means the user downloaded it themselves from the internet or brought it in on a USB drive. Because the security policy is set to Enforce, the program is blocked.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8033</strong></td>
      <td>ManagedInstaller check FAILED during Appid verification of * . Allowed to run due to Audit AppLocker Policy. This is the equivalent of Event ID 8032, but the program is not blocked, but only audited.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8034</strong></td>
      <td>ManagedInstaller Script check FAILED during Appid verification of *. A script tried to run, but it lacked the hidden stamp from the trusted Managed Installer. The script is blocked.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>8035</strong></td>
      <td>ManagedInstaller Script check SUCCEEDED during Appid verification of *. A script tried to run, Windows found the hidden stamp by the trusted Managed Installer, and the script is allowed to run.</td>
    </tr>
  </tbody>
</table>

<p>For <strong>AppLocker</strong>, the XPath query that can be used is the following:</p>

<pre><code class="language-xml">&lt;QueryList&gt;
  &lt;Query Id="0" Path="Microsoft-Windows-AppLocker/EXE and DLL"&gt;
    &lt;Select Path="Microsoft-Windows-AppLocker/EXE and DLL"&gt;
      *[System[(EventID &amp;gt;= 8000 and EventID &amp;lt;= 8008) or 
      (EventID &amp;gt;= 8020 and EventID &amp;lt;= 8027) or 
      (EventID &amp;gt;= 8030 and EventID &amp;lt;= 8035)]]
    &lt;/Select&gt;
    &lt;Select Path="Microsoft-Windows-AppLocker/MSI and Script"&gt;
      *[System[(EventID &amp;gt;= 8000 and EventID &amp;lt;= 8008) or 
      (EventID &amp;gt;= 8020 and EventID &amp;lt;= 8027) or 
      (EventID &amp;gt;= 8030 and EventID &amp;lt;= 8035)]]
    &lt;/Select&gt;
    &lt;Select Path="Microsoft-Windows-AppLocker/Packaged app-Deployment"&gt;
      *[System[(EventID &amp;gt;= 8000 and EventID &amp;lt;= 8008) or 
      (EventID &amp;gt;= 8020 and EventID &amp;lt;= 8027) or 
      (EventID &amp;gt;= 8030 and EventID &amp;lt;= 8035)]]
    &lt;/Select&gt;
    &lt;Select Path="Microsoft-Windows-AppLocker/Packaged app-Execution"&gt;
      *[System[(EventID &amp;gt;= 8000 and EventID &amp;lt;= 8008) or 
      (EventID &amp;gt;= 8020 and EventID &amp;lt;= 8027) or 
      (EventID &amp;gt;= 8030 and EventID &amp;lt;= 8035)]]
    &lt;/Select&gt;
  &lt;/Query&gt;
&lt;/QueryList&gt;
</code></pre>

<p>For <strong>App Control</strong>, the XPath query that can be used is the following:</p>

<pre><code class="language-xml">&lt;QueryList&gt;
  &lt;Query Id="0" Path="Microsoft-Windows-CodeIntegrity/Operational"&gt;
    &lt;Select Path="Microsoft-Windows-CodeIntegrity/Operational"&gt;
      *[System[EventID=3004 or EventID=3033 or EventID=3034 or EventID=3076 or 
      EventID=3077 or (EventID &amp;gt;= 3089 and EventID &amp;lt;= 3092)]]
    &lt;/Select&gt;
    &lt;Select Path="Microsoft-Windows-AppLocker/MSI and Script"&gt;
      *[System[EventID=8028 or EventID=8029 or
      (EventID &amp;gt;= 8036 and EventID &amp;lt;= 8040)]]
    &lt;/Select&gt;
    &lt;Select Path="Microsoft-Windows-AppLocker/Packaged app-Deployment"&gt;
      *[System[EventID=8028 or EventID=8029 or
      (EventID &amp;gt;= 8036 and EventID &amp;lt;= 8040)]]
    &lt;/Select&gt;
    &lt;Select Path="Microsoft-Windows-AppLocker/Packaged app-Execution"&gt;
      *[System[EventID=8028 or EventID=8029 or
      (EventID &amp;gt;= 8036 and EventID &amp;lt;= 8040)]]
    &lt;/Select&gt;
  &lt;/Query&gt;
&lt;/QueryList&gt;
</code></pre>

<p>For <strong>blocks only</strong>, for both features, the following XPath query can be used:</p>

<pre><code class="language-xml">&lt;QueryList&gt;
  &lt;Query Id="0" Path="Microsoft-Windows-CodeIntegrity/Operational"&gt;
    &lt;Select Path="Microsoft-Windows-CodeIntegrity/Operational"&gt;
      *[System[(EventID=3004 or EventID=3033 or 
      EventID=3077 or EventID=3092)]]
    &lt;/Select&gt;
    &lt;Select Path="Microsoft-Windows-AppLocker/EXE and DLL"&gt;
      *[System[(EventID=8004 or EventID=8007 or EventID=8022 or 
      EventID=8025 or EventID=8027 or EventID=8029 or 
      EventID=8032 or EventID=8034 or EventID=8036 or EventID=8040)]]
    &lt;/Select&gt;
    &lt;Select Path="Microsoft-Windows-AppLocker/MSI and Script"&gt;
      *[System[(EventID=8004 or EventID=8007 or EventID=8022 or 
      EventID=8025 or EventID=8027 or EventID=8029 or 
      EventID=8032 or EventID=8034 or EventID=8036 or EventID=8040)]]
    &lt;/Select&gt;
    &lt;Select Path="Microsoft-Windows-AppLocker/Packaged app-Deployment"&gt;
      *[System[(EventID=8004 or EventID=8007 or EventID=8022 or 
      EventID=8025 or EventID=8027 or EventID=8029 or 
      EventID=8032 or EventID=8034 or EventID=8036 or EventID=8040)]]
    &lt;/Select&gt;
    &lt;Select Path="Microsoft-Windows-AppLocker/Packaged app-Execution"&gt;
      *[System[(EventID=8004 or EventID=8007 or EventID=8022 or 
      EventID=8025 or EventID=8027 or EventID=8029 or 
      EventID=8032 or EventID=8034 or EventID=8036 or EventID=8040)]]
    &lt;/Select&gt;
  &lt;/Query&gt;
&lt;/QueryList&gt;
</code></pre>

<h3 id="mde">MDE</h3>

<p>When it comes to Microsoft Defender for Endpoint (MDE) itself, SENSE is the internal name used to refer to the behavioral sensor that powers MDE. The Windows Events created for SENSE can be found under the path <strong>Applications and Services Logs &gt; Microsoft &gt; Windows &gt; SENSE &gt; Operational</strong>. Information about the events created can be found in Microsoft’s documentation: <a href="https://learn.microsoft.com/en-us/defender-endpoint/event-error-codes">Review events and errors using Event Viewer</a>. The events created are only useful for troubleshooting purposes related to the SENSE service not operating as expected, but they are not related to any blocking activities. Therefore, they will not be documented in this post.</p>

<h3 id="general-xpath-query-for-all-detections">General XPath query for all detections</h3>

<p>If there is no certainty as to which Defender feature may have blocked something, or the need may be to prove that Defender did not actually block anything, the following XPath query filters for any <strong>block or detection event</strong> done by any Defender feature:</p>

<pre><code class="language-xml">&lt;QueryList&gt;
  &lt;Query Id="0" Path="Microsoft-Windows-Windows Defender/Operational"&gt;
    &lt;!-- ASR blocks and detections --&gt;
    &lt;Select Path="Microsoft-Windows-Windows Defender/Operational"&gt;
      *[System[(EventID=1121 or EventID=1122 or EventID=1129)]]
    &lt;/Select&gt;
    &lt;!-- CFA blocks and detections --&gt;
    &lt;Select Path="Microsoft-Windows-Windows Defender/Operational"&gt;
      *[System[(EventID=1123 or EventID=1124 or EventID=1127 or EventID=1128)]]
    &lt;/Select&gt;
    &lt;!-- Device Control 1 --&gt;
    &lt;Select Path="Security"&gt;
        *[System[EventID=6423]]
    &lt;/Select&gt;
    &lt;!-- Device Control 2 --&gt;
    &lt;Select Path="Microsoft-Windows-PrintService/Admin"&gt;
       *[System[(EventID=871 or EventID=808 or EventID=215)]]
    &lt;/Select&gt;
    &lt;!-- Exploit Protection blocks and detections 1 --&gt;
    &lt;Select Path="Microsoft-Windows-Security-Mitigations/KernelMode"&gt;
      *[System[(EventID &amp;gt;= 1 and EventID &amp;lt;= 24) or EventID=260]]
    &lt;/Select&gt;
    &lt;!-- Exploit Protection blocks and detections 2 --&gt;
    &lt;Select Path="Microsoft-Windows-Security-Mitigations/UserMode"&gt;
      *[System[(EventID &amp;gt;= 1 and EventID &amp;lt;= 24) or EventID=260]]
    &lt;/Select&gt;
    &lt;!-- Exploit Protection blocks and detections 3 --&gt;
    &lt;Select Path="Microsoft-Windows-Win32k/Operational"&gt;
      *[System[(EventID &amp;gt;= 1 and EventID &amp;lt;= 24) or EventID=260]]
    &lt;/Select&gt;
    &lt;!-- Exploit Protection blocks and detections 4 --&gt;
    &lt;Select Path="Microsoft-Windows-WER-Diagnostics/Operational"&gt;
      *[System[(EventID &amp;gt;= 1 and EventID &amp;lt;= 24) or EventID=260]]
    &lt;/Select&gt;
    &lt;!-- Network Protection blocks and detections --&gt;
    &lt;Select Path="Microsoft-Windows-Windows Defender/Operational"&gt;
    *[System[(EventID=1125 or EventID=1126)]]
    &lt;/Select&gt;
    &lt;!-- Microsoft Defender Antivirus and PUA blocks and detections --&gt;
    &lt;Select Path="Microsoft-Windows-Windows Defender/Operational"&gt;
      *[System[(EventID=1006 or EventID=1007 or EventID=1008 or
      EventID=1011 or EventID=1012 or EventID=1015 or EventID=1116 or
      EventID=1117 or EventID=1118 or EventID=1119)]]
    &lt;/Select&gt;
    &lt;!-- AppLocker blocks and detections 1 --&gt;
    &lt;Select Path="Microsoft-Windows-AppLocker/EXE and DLL"&gt;
      *[System[(EventID &amp;gt;= 8000 and EventID &amp;lt;= 8008) or 
      (EventID &amp;gt;= 8020 and EventID &amp;lt;= 8027) or 
      (EventID &amp;gt;= 8030 and EventID &amp;lt;= 8035)]]
    &lt;/Select&gt;
    &lt;!-- AppLocker blocks and detections 2 --&gt;
    &lt;Select Path="Microsoft-Windows-AppLocker/MSI and Script"&gt;
      *[System[(EventID &amp;gt;= 8000 and EventID &amp;lt;= 8008) or 
      (EventID &amp;gt;= 8020 and EventID &amp;lt;= 8027) or 
      (EventID &amp;gt;= 8030 and EventID &amp;lt;= 8035)]]
    &lt;/Select&gt;
    &lt;!-- AppLocker blocks and detections 3 --&gt;
    &lt;Select Path="Microsoft-Windows-AppLocker/Packaged app-Deployment"&gt;
      *[System[(EventID &amp;gt;= 8000 and EventID &amp;lt;= 8008) or 
      (EventID &amp;gt;= 8020 and EventID &amp;lt;= 8027) or 
      (EventID &amp;gt;= 8030 and EventID &amp;lt;= 8035)]]
    &lt;/Select&gt;
    &lt;!-- AppLocker blocks and detections 4 --&gt;
    &lt;Select Path="Microsoft-Windows-AppLocker/Packaged app-Execution"&gt;
      *[System[(EventID &amp;gt;= 8000 and EventID &amp;lt;= 8008) or 
      (EventID &amp;gt;= 8020 and EventID &amp;lt;= 8027) or 
      (EventID &amp;gt;= 8030 and EventID &amp;lt;= 8035)]]
    &lt;/Select&gt;
    &lt;!-- App Control blocks and detections 1 --&gt;
    &lt;Select Path="Microsoft-Windows-CodeIntegrity/Operational"&gt;
      *[System[EventID=3004 or EventID=3033 or EventID=3034 or EventID=3076 or 
      EventID=3077 or (EventID &amp;gt;= 3089 and EventID &amp;lt;= 3092)]]
    &lt;/Select&gt;
    &lt;!-- App Control blocks and detections 2 --&gt;
    &lt;Select Path="Microsoft-Windows-AppLocker/MSI and Script"&gt;
      *[System[EventID=8028 or EventID=8029 or
      (EventID &amp;gt;= 8036 and EventID &amp;lt;= 8040)]]
    &lt;/Select&gt;
    &lt;!-- App Control blocks and detections 3 --&gt;
    &lt;Select Path="Microsoft-Windows-AppLocker/Packaged app-Deployment"&gt;
      *[System[EventID=8028 or EventID=8029 or
      (EventID &amp;gt;= 8036 and EventID &amp;lt;= 8040)]]
    &lt;/Select&gt;
    &lt;!-- App Control blocks and detections 4 --&gt;
    &lt;Select Path="Microsoft-Windows-AppLocker/Packaged app-Execution"&gt;
      *[System[EventID=8028 or EventID=8029 or
      (EventID &amp;gt;= 8036 and EventID &amp;lt;= 8040)]]
    &lt;/Select&gt;
  &lt;/Query&gt;
&lt;/QueryList&gt;
</code></pre>

<p>For <strong>only block</strong> events, use the following XPath query:</p>

<pre><code class="language-xml">&lt;QueryList&gt;
  &lt;Query Id="0" Path="Microsoft-Windows-Windows Defender/Operational"&gt;
    &lt;!-- ASR blocks --&gt;
    &lt;Select Path="Microsoft-Windows-Windows Defender/Operational"&gt;
        *[System[(EventID=1121)]]
    &lt;/Select&gt;
    &lt;!-- CFA blocks --&gt;
    &lt;Select Path="Microsoft-Windows-Windows Defender/Operational"&gt;
        *[System[(EventID=1123 or EventID=1127)]]
    &lt;/Select&gt;
    &lt;!-- Device Control 1 --&gt;
    &lt;Select Path="Security"&gt;
        *[System[EventID=6423]]
    &lt;/Select&gt;
    &lt;!-- Device Control 2 --&gt;
    &lt;Select Path="Microsoft-Windows-PrintService/Admin"&gt;
       *[System[(EventID=871 or EventID=808 or EventID=215)]]
    &lt;/Select&gt;
    &lt;!-- Exploit Protection 1 --&gt;
    &lt;Select Path="Microsoft-Windows-Security-Mitigations/KernelMode"&gt;
      *[System[(EventID=2 or EventID=4 or EventID=6 or EventID=8 or
      EventID=10 or EventID=12 or EventID=14 or EventID=16 or
      EventID=18 or EventID=20 or EventID=22 or EventID=24)]]
    &lt;/Select&gt;
    &lt;!-- Exploit Protection 2 --&gt;
    &lt;Select Path="Microsoft-Windows-Security-Mitigations/UserMode"&gt;
      *[System[(EventID=2 or EventID=4 or EventID=6 or EventID=8 or
      EventID=10 or EventID=12 or EventID=14 or EventID=16 or
      EventID=18 or EventID=20 or EventID=22 or EventID=24)]]
    &lt;/Select&gt;
    &lt;!-- Exploit Protection 3 --&gt;
    &lt;Select Path="Microsoft-Windows-WER-Diagnostics/Operational"&gt;
      *[System[(EventID=5)]]
    &lt;/Select&gt;
    &lt;!-- Exploit Protection 4 --&gt;
    &lt;Select Path="Microsoft-Windows-Win32k/Operational"&gt;
      *[System[(EventID=260)]]
    &lt;/Select&gt;
    &lt;!-- Network Protection --&gt;
    &lt;Select Path="Microsoft-Windows-Windows Defender/Operational"&gt;
      *[System[(EventID=1126)]]
    &lt;/Select&gt;
    &lt;!-- Microsoft Defender Antivirus Action Taken (both malware and PUA) --&gt;
    &lt;Select Path="Microsoft-Windows-Windows Defender/Operational"&gt;
      *[System[(EventID=1007 or EventID=1117)]]
    &lt;/Select&gt;
    &lt;!-- AppLocker and App Control 1 --&gt;
    &lt;Select Path="Microsoft-Windows-CodeIntegrity/Operational"&gt;
      *[System[(EventID=3004 or EventID=3033 or 
      EventID=3077 or EventID=3092)]]
    &lt;/Select&gt;
    &lt;!-- AppLocker and App Control 2 --&gt;
    &lt;Select Path="Microsoft-Windows-AppLocker/EXE and DLL"&gt;
      *[System[(EventID=8004 or EventID=8007 or EventID=8022 or 
      EventID=8025 or EventID=8027 or EventID=8029 or 
      EventID=8032 or EventID=8034 or EventID=8036 or EventID=8040)]]
    &lt;/Select&gt;
    &lt;!-- AppLocker and App Control 3 --&gt;
    &lt;Select Path="Microsoft-Windows-AppLocker/MSI and Script"&gt;
      *[System[(EventID=8004 or EventID=8007 or EventID=8022 or 
      EventID=8025 or EventID=8027 or EventID=8029 or 
      EventID=8032 or EventID=8034 or EventID=8036 or EventID=8040)]]
    &lt;/Select&gt;
    &lt;!-- AppLocker and App Control 4 --&gt;
    &lt;Select Path="Microsoft-Windows-AppLocker/Packaged app-Deployment"&gt;
      *[System[(EventID=8004 or EventID=8007 or EventID=8022 or 
      EventID=8025 or EventID=8027 or EventID=8029 or 
      EventID=8032 or EventID=8034 or EventID=8036 or EventID=8040)]]
    &lt;/Select&gt;
    &lt;!-- AppLocker and App Control 5 --&gt;
    &lt;Select Path="Microsoft-Windows-AppLocker/Packaged app-Execution"&gt;
      *[System[(EventID=8004 or EventID=8007 or EventID=8022 or 
      EventID=8025 or EventID=8027 or EventID=8029 or 
      EventID=8032 or EventID=8034 or EventID=8036 or EventID=8040)]]
    &lt;/Select&gt;
  &lt;/Query&gt;
&lt;/QueryList&gt;
</code></pre>

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

<p>Sometimes, it is complicated to understand what Defender blocked, when, and why the block happened. Having tools like Windows Events or Defender Advanced Hunting logs does help, but there is an overwhelming amount of logs.</p>

<p>Having a few queries ready to be used that filter for only the needed events cuts a few steps in the troubleshooting process. This post focused on making such queries for the Windows Events.</p>

<p>Hopefully these queries will help towards solving issues related to Defender, or to prove that Defender was not an issue.</p>

<p>Thank you for being here.</p>

<p>On to the next one!</p>]]></content><author><name>Kostas Koutroumpouchos</name></author><category term="Other" /><summary type="html"><![CDATA[Introduction]]></summary></entry><entry><title type="html">Cyber Range as Code - Part 2 - Automating the base - Hypervisor and Firewall</title><link href="https://kostaskoutrou.github.io/2026/03/20/proxmox-and-opnsense-as-code-cyber-range-as-code-part2.html" rel="alternate" type="text/html" title="Cyber Range as Code - Part 2 - Automating the base - Hypervisor and Firewall" /><published>2026-03-20T00:00:00+00:00</published><updated>2026-03-20T00:00:00+00:00</updated><id>https://kostaskoutrou.github.io/2026/03/20/proxmox-and-opnsense-as-code-cyber-range-as-code-part2</id><content type="html" xml:base="https://kostaskoutrou.github.io/2026/03/20/proxmox-and-opnsense-as-code-cyber-range-as-code-part2.html"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>Welcome to the 2nd part of the <strong>project “Cyber Range as Code”</strong>, where a Cyber Range is built using Infrastructure as Code and IT automation concepts.</p>

<p>If you want to see the concept as a whole, please take a look at <a href="https://kostaskoutrou.github.io/2026/02/02/cyber-range-as-code-part1.html">part 1</a>.</p>

<p>The basic idea is to use <strong>HashiCorp Packer, Terraform, and Ansible</strong> as the tools to build <strong>a fully functioning Cyber Range</strong>, for testing attack scenarios, implementing security standards, and architecting a secure infrastructure. The highlight of the concept is the IaC part. The whole point is to be able to <strong>recover seamlessly</strong>, i.e., <strong>all the infrastructure must be deployable automatically</strong>. If it is ever needed to redeploy the infrastructure on a new host server, it should be doable with minimal manual effort.</p>

<blockquote>
  <p>In <strong>Part 2</strong>, the base is built. This includes the <strong>Hypervisor</strong> itself, where <strong>Proxmox</strong> is used, as well as the central <strong>Firewall</strong> of the Cyber Range, where <strong>OPNSense</strong> is used.</p>
</blockquote>

<p>Now that the picture is painted, let’s begin.</p>

<hr />

<h2 id="contents">Contents</h2>

<ul id="markdown-toc">
  <li><a href="#introduction" id="markdown-toc-introduction">Introduction</a></li>
  <li><a href="#contents" id="markdown-toc-contents">Contents</a></li>
  <li><a href="#brief-reminder-of-project-architecture" id="markdown-toc-brief-reminder-of-project-architecture">Brief reminder of project architecture</a></li>
  <li><a href="#automate-proxmox" id="markdown-toc-automate-proxmox">Automate Proxmox</a>    <ul>
      <li><a href="#initial-setup" id="markdown-toc-initial-setup">Initial Setup</a></li>
      <li><a href="#proxmox-ansible" id="markdown-toc-proxmox-ansible">Proxmox Ansible</a>        <ul>
          <li><a href="#ansible-collection" id="markdown-toc-ansible-collection">Ansible Collection</a></li>
          <li><a href="#setting-up-ssh" id="markdown-toc-setting-up-ssh">Setting up SSH</a></li>
          <li><a href="#initializing-ansible-for-proxmox" id="markdown-toc-initializing-ansible-for-proxmox">Initializing Ansible for Proxmox</a></li>
          <li><a href="#running-ansible-to-configure-proxmox" id="markdown-toc-running-ansible-to-configure-proxmox">Running Ansible to configure Proxmox</a></li>
          <li><a href="#issues-met-and-resolutions" id="markdown-toc-issues-met-and-resolutions">Issues met and resolutions</a></li>
        </ul>
      </li>
    </ul>
  </li>
  <li><a href="#automate-opnsense" id="markdown-toc-automate-opnsense">Automate OPNSense</a>    <ul>
      <li><a href="#opnsense-packer" id="markdown-toc-opnsense-packer">OPNSense Packer</a>        <ul>
          <li><a href="#opnsense-packer-issues-and-resolutions" id="markdown-toc-opnsense-packer-issues-and-resolutions">OPNSense Packer Issues and resolutions</a></li>
        </ul>
      </li>
      <li><a href="#opnsense-terraform" id="markdown-toc-opnsense-terraform">OPNSense Terraform</a></li>
      <li><a href="#opnsense-ansible" id="markdown-toc-opnsense-ansible">OPNSense Ansible</a></li>
      <li><a href="#configuring-proxmox-for-opnsense-with-ansible" id="markdown-toc-configuring-proxmox-for-opnsense-with-ansible">Configuring Proxmox for OPNSense with Ansible</a></li>
    </ul>
  </li>
  <li><a href="#next-steps" id="markdown-toc-next-steps">Next Steps</a></li>
  <li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
</ul>

<hr />

<h2 id="brief-reminder-of-project-architecture">Brief reminder of project architecture</h2>

<p>Below is a brief reminder of the project architecture. For more details, see my previous post <a href="https://kostaskoutrou.github.io/2026/02/02/cyber-range-as-code-part1.html">Cyber Range as Code: Automating Security Lab with IaC - Part 1</a></p>

<p>The initial infrastructure architecture is the following. It is important to note here that this is the initial architecture idea, it is not final, and there may very well be changes during implementation.</p>

<p><img src="https://github.com/user-attachments/assets/daeb1cd9-9816-49d0-874e-2836c15d9d86" alt="NetworkDesign-FW Rules drawio" /></p>

<p>As shown above, the architecture is a relatively simple and typical network infrastructure, with the following components:</p>

<ul>
  <li>The central Firewall, controlling the network traffic among the four network zones:
    <ol>
      <li><strong>Demilitarized Zone (DMZ)</strong>: The zone that exposes services which are to be served to the Internet. In the context of the project, these will be served to the local network, and, most importantly for the project, will be accessible by the “External Attacker”, enabling for attack scenarios initiated from the “Internet”.</li>
      <li><strong>Internal Zone</strong>: The zone where all the internal servers and services will reside. This includes:
        <ul>
          <li>Any internal servers, e.g., SQL Servers and AD DC, hosting and serving information which is destined to be consumed by internal resources only.</li>
          <li>Security Tools, including the SIEM/XDR/Monitoring tools.</li>
        </ul>
      </li>
      <li><strong>End Users</strong>: The last zone will be for the End Users, where typical workstation VMs will reside, and have defined access to specific servers/services and to the internet.</li>
      <li><strong>WAN Zone</strong>: This is where the “Internet”, in the context of the Infrastructure, lives. This is where:
        <ul>
          <li>The PC from which the Proxmox management will be done, running the Packer, Terraform, and Ansible.</li>
          <li>The External Attacks will occur from.</li>
        </ul>
      </li>
    </ol>
  </li>
  <li>There will also be Internal Attack Simulations, executed from within the different zones, bypassing the firewall (“assume breach”).</li>
</ul>

<p>The code of the project can be found in the project’s <a href="https://github.com/KostasKoutrou/kostas-seclab">GitHub Repository</a></p>

<h2 id="automate-proxmox">Automate Proxmox</h2>

<p>Regarding automating the configuration of Proxmox, this essentially includes:</p>

<h3 id="initial-setup">Initial Setup</h3>

<p>This step includes the <strong>installation of Proxmox on a host machine</strong>, setting up the <strong>network configuration</strong>, i.e., IP Address, Gateway, DNS, and setting up the <strong>root user credentials</strong>. One point of improvement here is to potentially automate the parameters during the initial installation of Proxmox. This is supported by Proxmox with the <a href="https://pve.proxmox.com/wiki/Automated_Installation">Automated Installation</a>. This would be a very interesting improvement point towards a fully hands-off installation and configuration. But this will be left as a future exercise. For now, these steps were done manually, and can be found in my <a href="https://kostaskoutrou.github.io/2026/02/02/cyber-range-as-code-part1.html">previous post</a>.</p>

<p>After the initial setup and reaching the point of being able to sign in to the Proxmox GUI, the next step is to set up Proxmox so that Ansible can communicate with it.</p>

<p>This means:</p>

<ol>
  <li><strong>Verify SSH Access</strong>: A rule to allow SSH access is actually created by default when enabling the Proxmox firewall, so no manual rule creation is required here, see <a href="https://pve.proxmox.com/pve-docs/pve-admin-guide.html#pve_firewall_default_rules">Default firewall rules</a>.</li>
  <li>
    <p><strong>Create a role for all automation steps</strong>: The following permissions are required for all steps of this project:</p>

    <p><img alt="image" src="https://github.com/user-attachments/assets/65d09a28-516a-4173-928b-13e1692482c5" /></p>
  </li>
  <li>
    <p><strong>Create user</strong>: The packer user created in the initial lab (part 1) is used. It is a simple user and the above role is assigned to it.</p>

    <p><img alt="image" src="https://github.com/user-attachments/assets/84bc030f-e6b3-4e47-abd1-139a2d7e77f5" /></p>
  </li>
  <li>
    <p><strong>API key</strong>: An API key was generated and assigned to the above user, in order for Ansible to execute API calls to Proxmox.</p>

    <p><img alt="image" src="https://github.com/user-attachments/assets/d8031359-89ed-46b9-97b7-8933de6a5e9a" /></p>
  </li>
</ol>

<h3 id="proxmox-ansible">Proxmox Ansible</h3>

<p>In this step, the Ansible Control Node (i.e., the PC where all the code is written and executed) is prepared for Proxmox configurations.</p>

<h4 id="ansible-collection">Ansible Collection</h4>

<p><a href="https://docs.ansible.com/projects/ansible/latest/collections_guide/index.html">Ansible Collections</a> are a distribution format for Ansible content that can include playbooks, roles, modules, and plugins. Essentially, a collection is like a program library containing all the functionalities required for configuring a specific component.</p>

<p>The <strong>Community Proxmox</strong> Ansible Collection was used for this project, which can be found <a href="https://galaxy.ansible.com/ui/repo/published/community/proxmox/docs/">here</a>.</p>

<p>To install the collection the following command was executed:</p>

<pre><code class="language-bash">ansible-galaxy collection install community.proxmox
</code></pre>

<p><img alt="image" src="https://github.com/user-attachments/assets/c604ab1e-eb4b-4887-8687-82597fafe540" /></p>

<p>Additionally, the <code>community.proxmox</code> ansible collection requires the Python libraries <code>proxmoxer</code> and <code>requests</code> to talk to the Proxmox API. To install them, run one of the following:</p>

<pre><code class="language-bash"># If you are on Ubuntu/Debian in WSL. Since the project is ran on WSL, this was selected:
sudo apt update
sudo apt install python3-proxmoxer python3-requests

# OR if you prefer pip:
pip3 install proxmoxer requests
</code></pre>

<p>Without these libraries, the following error is shown when trying to run the Ansible playbook:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/fb69ef22-ac75-41a8-bc5f-1d0bc0032e24" /></p>

<h4 id="setting-up-ssh">Setting up SSH</h4>

<p>Since Ansible executes SSH using certificate-based authentication, <strong>the public key of the Ansible Control Node must be added to Proxmox’s trusted keys</strong>. Since the public key is the one that exists on the physical PC currently running all the commands, the easiest way to do this task is with the following command:</p>

<pre><code class="language-bash">ssh-copy-id -i ~/.ssh/id_rsa.pub root@192.168.0.50
</code></pre>

<p>After running the command, it is now possible to ping Proxmox via Ansible:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/4dd29f39-14cb-4b8a-9f4e-93211562291d" /></p>

<p>In the above command, the <code>inventory.ini</code> file includes machines which are in the scope of configurations via Ansible. For now it only includes the Proxmox host:</p>

<pre><code class="language-ini">[proxmox]
192.168.0.50 ansible_user=root
</code></pre>

<h4 id="initializing-ansible-for-proxmox">Initializing Ansible for Proxmox</h4>

<p>The Community Proxmox Ansible Collection requires a few specific <a href="https://galaxy.ansible.com/ui/repo/published/community/proxmox/content/module/proxmox_node/#parameters">parameters</a> for every task being run, and it is also supported to configure these parameters as Environment Variables:</p>

<table>
  <thead>
    <tr>
      <th>Parameter</th>
      <th>Comments</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>api_host (required)</td>
      <td>Specify the target host of the Proxmox VE cluster.<br />Uses the PROXMOX_HOST environment variable if not specified.</td>
    </tr>
    <tr>
      <td>api_password</td>
      <td>Specify the password to authenticate with.<br />Uses the PROXMOX_PASSWORD environment variable if not specified.</td>
    </tr>
    <tr>
      <td>api_port</td>
      <td>Specify the target port of the Proxmox VE cluster.<br />Uses the PROXMOX_PORT environment variable if not specified.</td>
    </tr>
    <tr>
      <td>api_token_id</td>
      <td>Specify the token ID.<br />Uses the PROXMOX_TOKEN_ID environment variable if not specified.</td>
    </tr>
    <tr>
      <td>api_token_secret</td>
      <td>Specify the token secret.<br />Uses the PROXMOX_TOKEN_SECRET environment variable if not specified.</td>
    </tr>
    <tr>
      <td>api_user (required)</td>
      <td>Specify the user to authenticate with.<br />Uses the PROXMOX_USER environment variable if not specified.</td>
    </tr>
  </tbody>
</table>

<p>Therefore, the following Envars are set before running Proxmox Ansible playbooks:</p>

<pre><code class="language-bash">export PROXMOX_HOST="192.168.0.50"
export PROXMOX_USER="packer@pve"
export PROXMOX_TOKEN_ID="packer-token"
export PROXMOX_TOKEN_SECRET="&lt;token_secret&gt;"
</code></pre>

<h4 id="running-ansible-to-configure-proxmox">Running Ansible to configure Proxmox</h4>

<p>After setting up Ansible, the next step is to write and <strong>execute the Ansible Playbooks to apply the configurations to Proxmox</strong>, which were initially applied manually and described on my <a href="https://kostaskoutrou.github.io/2026/02/02/cyber-range-as-code-part1.html">previous post</a> and part 1 of this series. The Ansible playbook can be found <a href="https://github.com/KostasKoutrou/kostas-seclab/blob/master/ansible/proxmox_config.yml">here</a>, and is also depicted below, with more details in the comments and afterwards:</p>

<pre><code class="language-yaml">---
#before running, run the following:
# export PROXMOX_HOST="192.168.0.50"
# export PROXMOX_USER="packer@pve"
# export PROXMOX_TOKEN_ID="packer-token"
# export PROXMOX_TOKEN_SECRET="&lt;token_secret&gt;"

- name: Configure Proxmox via API
  hosts: proxmox
  # because this play configures Proxmox via API, with the keyword below
  # the playbook is executed at the ansible control node instead of
  # SSH'ing and running it locally on the managed node.
  connection: local
  gather_facts: false

  tasks:
    # Task to configure the central DNS settings of Proxmox
    - name: Set DNS
      community.proxmox.proxmox_node:
        node_name: kkproxmox
        dns:
          dns1: 1.1.1.1
          dns2: 8.8.8.8
          search: kostas.local
      # delegate_to: localhost # this is not needed if the
      # "connection: local" is at the start of the playbook

    # Task to create the Virtual Network Bridges in Proxmox
    - name: Set Network
      community.proxmox.proxmox_node_network:
        node: kkproxmox
        autostart: true
        iface_type: bridge
        iface: "{{ item.iface }}"
        cidr: "{{ item.cidr }}"
        comments: "{{ item.comments }}"
        bridge_ports: "{{ item.bridge_ports }}"
        gateway: "{{ item.gateway }}"
      loop:
        - { iface: "vmbrWAN10", cidr: "10.0.10.2/24", comments: "WAN" , bridge_ports: "" , gateway: "" }
        - { iface: "vmbrDMZ20", cidr: "10.0.20.2/24", comments: "DMZ" , bridge_ports: "" , gateway: "" }
        - { iface: "vmbrIZ30", cidr: "10.0.30.2/24", comments: "Internal Zone" , bridge_ports: "" , gateway: "" }
        - { iface: "vmbrEUZ40", cidr: "10.0.40.2/24", comments: "End User Zone" , bridge_ports: "" , gateway: "" }
        - { iface: "vmbr0", cidr: "192.168.0.50/24", comments: "Bridge to home router" , bridge_ports: "nic1" , gateway: "192.168.0.1" }
      
    # Task to connect the physical interface to the physical network
    - name: Set Network interfaces
      community.proxmox.proxmox_node_network:
        node: kkproxmox
        iface_type: eth
        iface: nic1
        comments: "Physical Interface of Proxmox to home router"

    # Apply the above network configurations
    - name: Apply Network
      community.proxmox.proxmox_node_network:
        node: kkproxmox
        state: "apply"

    # Set firewall aliases to be used for the firewall rules below
    - name: Set Firewall Aliases
      community.proxmox.proxmox_firewall:
        level: cluster
        aliases:
          - name: subnet10
            cidr: "10.0.0.0/8"
          - name: subnet172
            cidr: "172.16.0.0/12"
          - name: subnet192
            cidr: "192.168.0.0/16"
    
    # security groups to be applied on the different Proxmox levels
    - name: Create Firewall Security Groups
      community.proxmox.proxmox_firewall:
        level: cluster
        group_conf: true # Whether security group should be created or deleted
        state: present # create the group
        group: "{{ item.group }}"
      loop:
        - { group: "allowguesttraffic" }
        - { group: "blockhomenwtraffic" }
    
    # configure rules of the above security groups
    - name: Set Firewall Security Groups rules
      community.proxmox.proxmox_firewall:
        level: group
        state: present # Create/update/delete firewall rules or security group.
        update: true # If state=present and if one or more rule/alias/ipset already exists it will update them.
        group: "{{ item.group }}"
        rules: "{{ item.rules }}"
      loop: 
        - group: allowguesttraffic # allow/not control traffic from the lab's subnets.
          rules:
            - type: in
              action: ACCEPT
              source: dc/subnet10
              pos: 0
              log: nolog
              enable: true
        - group: blockhomenwtraffic # block traffic to the home physical network.
          rules:
            - type: in
              action: ACCEPT
              source: dc/subnet192
              pos: 0
              log: nolog
              enable: false # disabled but kept just in case, because proxmox has this rule by default
              comment: "Allow ProxMox Management"
              dest: 192.168.0.50
              dport: 8006
              proto: tcp
            - type: in
              action: ACCEPT
              source: dc/subnet192
              pos: 1
              log: nolog
              enable: true
              comment: "Allow OPNSense Management"
              dest: 192.168.0.51
              dport: 443
              proto: tcp
            - type: out
              action: REJECT
              pos: 2
              log: nolog
              enable: true
              comment: "Block Local Traffic"
              dest: dc/subnet192

    # apply security groups for both cluster and node levels
    - name: Apply Security Groups
      community.proxmox.proxmox_firewall:
        level: "{{ item.level }}"
        node: "{{ item.node }}"
        update: true
        state: present
        rules:
          - action: blockhomenwtraffic # same rule for both cluster and node levels
            pos: 0
            type: group
            enable: true
      loop:
        - { level: cluster, node: "" }
        - { level: node, node: kkproxmox }

    # - name: Configure Update Repositories - left for future update

    #The below 2 tasks are useful if it is needed to print the current configuration
    # - name: Get Firewall Config
    #   community.proxmox.proxmox_node_info:
    #     # level: node
    #     # node: kkproxmox
    #   register: debug_data

    # - name: Show debug data
    #   debug:
    #     var: debug_data

- name: Configure Proxmox via SSH
  hosts: proxmox
  gather_facts: false

  tasks:
    - name: Check current Cluster Firewall status
      ansible.builtin.command: pvesh get /cluster/firewall/options --output-format json
      register: cluster_fw_status
      changed_when: false

    - name: Enable Firewall on Cluster
      ansible.builtin.command: pvesh set /cluster/firewall/options -enable 1
      # Only run this command IF the 'enable' value is not 1 (or if it doesn't exist yet)
      when: (cluster_fw_status.stdout | from_json).enable | default(0) | int != 1

    - name: Check current Node Firewall status
      ansible.builtin.command: pvesh get /nodes/kkproxmox/firewall/options --output-format json
      register: node_fw_status
      changed_when: false

    - name: Enable Firewall on Node
      ansible.builtin.command: pvesh set /nodes/kkproxmox/firewall/options -enable 1
      # Only run this command IF the 'enable' value is not 1 (or if it doesn't exist yet)
      when: (node_fw_status.stdout | from_json).enable | default(0) | int != 1
</code></pre>

<p>With this playbook, the following Proxmox settings are configured:</p>

<ol>
  <li>DNS Settings</li>
  <li>Network Interfaces</li>
  <li>Network Configuration</li>
  <li>Firewall Rules</li>
  <li>Enabling Firewall</li>
</ol>

<p>One point to note is that the playbook consists of two plays:</p>

<ol>
  <li><strong>Configuring Proxmox via API</strong>: This is the bulk of the configuration, because the Proxmox Community Ansible collection supports most of the configurations required.</li>
  <li><strong>Configuring Proxmox via SSH</strong>: This contains tasks that cannot be executed using the avaialble Ansible collection. It includes executing manual commands on Proxmox using its CLI <code>pvesh</code>.</li>
</ol>

<p>To run this playbook, the following command is executed:</p>

<pre><code class="language-bash">ansible-playbook -i inventory.ini proxmox_config.yml
</code></pre>

<p>Running the playbook outputs the following:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/e19c0be3-adcb-4af4-a3b1-30725f455332" /></p>

<p>As shown in the screenshot, the configuration is already applied, and all tasks output status <code>ok</code>. The two tasks that output <code>skipped</code> are the ones executed via SSH. Because they manually set values on Proxmox, the <code>when</code> keyword is used to skip the execution if the value is already set. Otherwise, the task would always output status <code>changed</code>, making the total output of the playbook confusing to read.</p>

<h4 id="issues-met-and-resolutions">Issues met and resolutions</h4>

<p>While developing the Ansible playbook, the follow issues were faced:</p>

<p><strong>CRLF vs LF in VS Code</strong></p>

<p>In the current setup of writing the code, VS Code is used on Windows (PC) and  Windows Subsystem for Linux (WSL) is used to run all the code. There is a discrepancy on how the ‘new line’ character is saved in Windows and expected to be read in Linux. More specifically:</p>

<ol>
  <li>Windows saves a new line as <code>CRLF</code> (\r\n).</li>
  <li>Linux expects a new line to be <code>LF</code> (\n).</li>
</ol>

<p>When Ansible (running in Linux) reads the <code>inventory.ini</code> file, it sees the carriage return as part of the text. It reads the group name as <code>[proxmox\r]</code>. The playbook, on the other hand asks for <code>proxmox</code>. Since <code>proxmox</code> does not equal <code>proxmox\r</code>, Ansible skips it.</p>

<p>To fix this, in VS Code, the new line character needs to be changed on the bottom right of the window for <code>CRLF</code> to <code>LF</code>:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/350a5d79-058d-419c-965f-f55f6dbe0adc" /></p>

<p>After configuring Proxmox, the next step is to <strong>automatically configure the central Firewall</strong>, which is OPNSense.</p>

<h2 id="automate-opnsense">Automate OPNSense</h2>

<h3 id="opnsense-packer">OPNSense Packer</h3>

<p>As described on the first post of the series, the idea of this lab is to build VMs automatically in 3 steps:</p>

<ol>
  <li>Using <strong>Packer</strong>, build a relatively blank template (more on the “relatively” part later)</li>
  <li>Use that template with <strong>Terraform</strong> to provision VMs ready to be configured by</li>
  <li><strong>Ansible</strong>, where the rest of the configuration and final touches happen.</li>
</ol>

<p>Regarding OPNSense specifically, which is actually a <strong>custom machine based on FreeBSD</strong>, unfortunately it is <strong>not fully designed to be deployed and configured using automation tools</strong>. Therefore, several <strong>workarounds were needed in order to achieve an automated deployment</strong>.</p>

<p>First of all, in order to get the initial “blank” state of the machine, the most controllable way to do that was by configuring an OPNSense VM manually exactly up to and not any further than the point where it functions and is ready to be used by Terraform, without any more settings configured. These configurations are:</p>

<ol>
  <li>Assigning the WAN interface</li>
  <li>Assigning the WAN interface IP with DHCP (this will be changed with Terraform)</li>
  <li>Enabling SSH for management</li>
  <li>Installing the QEMU agent so that Proxmox will be able to read the IP address assigned to the VM</li>
  <li>Auto-start the QEMU agent service</li>
</ol>

<p>After applying the above configurations manually to the OPNSense VM, the configuration of the machine was exported to a file <code>config.xml</code>, which can be found <a href="https://github.com/KostasKoutrou/kostas-seclab/blob/master/packer/opnsense/conf/config.xml">here</a>. This XML file is the configuration file of OPNSense, contains all the settings previously configured, and can be imported on a clean-slate OPNSense and have the configuration applied to it.</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/163b3fef-0b37-4b4d-8afe-c9cc63b4a9af" /></p>

<p>This <code>config.xml</code> file contains all the information required for a VM ready to be used by Terraform.</p>

<p>The idea is to use the <code>config.xml</code> and import it on a new OPNSense VM by utilizing its <a href="https://docs.opnsense.org/manual/install.html#opnsense-importer">Configuration Importer</a> feature, where during boot time there is the option to select to import a <code>config.xml</code> file from an external drive (in this case a CD created by Packer containing the above exported <code>config.xml</code>).</p>

<p>The packer template can be found under the <a href="https://github.com/KostasKoutrou/kostas-seclab/blob/master/packer/opnsense/opnsense.pkr.hcl">project’s repository</a>, and is also presented below, with several comments to explain how it works:</p>

<pre><code class="language-terraform">packer {
  required_plugins {
    name = {
      version = "~&gt; 1"
      source  = "github.com/hashicorp/proxmox"
    }
  }
}

# Declare variables, we will pull them later in the packer build command
variable "proxmox_api_url" { type = string }
variable "proxmox_api_token_id" { type = string }
variable "proxmox_api_token_secret" {
  type      = string
  sensitive = true
}

source "proxmox-iso" "opnsense" { #Resource type and local name
  proxmox_url = var.proxmox_api_url
  username    = var.proxmox_api_token_id
  token       = var.proxmox_api_token_secret
  # Skip TLS Verification for self-signed certificates
  insecure_skip_tls_verify = true
  qemu_agent = true # Default is true anyway
  node = "kkproxmox"
  vm_id = 1001
  vm_name = "opnsense-template"
  ssh_username = "root"
  ssh_password = "opnsense" # Default root password, can be changed later.
  ssh_timeout = "20m"
  cores = 4
  memory = 4096 # RAM must be more than 3GB, otherwise the boot_command is different and will not work
  os = "other" # for FreeBSD, you choose "other"
  cpu_type = "host"
  scsi_controller = "virtio-scsi-single"

  boot_iso {
    # type = "scsi"
    type = "ide"
    # ISO stored locally on Proxmox. In the future this can be changed to downloading from the internet.
    iso_file = "local:iso/OPNsense-25.7-dvd-amd64.iso"
    iso_checksum = "sha256:e4c178840ab1017bf80097424da76d896ef4183fe10696e92f288d0641475871"
    unmount = true
  }

  # this will created a cd in "cd1", which will be selected in the boot_command
  additional_iso_files {
    cd_content = {
    "conf/config.xml" = templatefile("${path.root}/conf/config.xml", {
      # pull the public SSH key, base64 encode it, and write it in config.xml
      dynamic_ssh_key = base64encode(file("~/.ssh/id_rsa.pub"))})
    }
    cd_label = "config"
    iso_storage_pool = "local"
  }
  
  network_adapters {
    model  = "virtio"
    bridge = "vmbr0" # Will change it in the Terraform configuration, this is only for packer.
  }

  disks {
    disk_size    = "20G"
    storage_pool = "local-lvm"
    type         = "scsi"
    ssd          = true
  }

  boot_command = [
    # There is already a 10 sec wait for boot, adding another 12.
    # Start configuration importer and select cd1 where the cd_content is stored.
    "&lt;wait12s&gt;&lt;enter&gt;&lt;wait5s&gt;cd1&lt;enter&gt;&lt;wait25s&gt;",

    # Put the default credentials to install the OS.
    "installer&lt;enter&gt;&lt;wait2s&gt;",
    "opnsense&lt;enter&gt;&lt;wait10s&gt;",
    
    # Going through the installation options:
    # Accept default Keymap and ZFS installation.
    "&lt;enter&gt;&lt;wait2s&gt;&lt;enter&gt;&lt;wait10s&gt;&lt;enter&gt;&lt;wait2s&gt;&lt;spacebar&gt;&lt;wait1s&gt;&lt;enter&gt;&lt;wait1s&gt;",
    
    # Confirm formatting (Yes) and wait 2 minutes for installation.
    "&lt;left&gt;&lt;wait1s&gt;&lt;enter&gt;&lt;wait120s&gt;",
    
    # Do not change password and reboot.
    "&lt;down&gt;&lt;wait1s&gt;&lt;enter&gt;&lt;wait1s&gt;&lt;enter&gt;",

    # Enable qemu agent service autostart and Update from console to latest version,
    # because qemu requires the latest OPNSense version.
    "&lt;wait45s&gt;root&lt;enter&gt;&lt;wait2s&gt;opnsense&lt;enter&gt;&lt;wait5s&gt;",
    "8&lt;enter&gt;&lt;wait2s&gt;sysrc qemu_guest_agent_enable='YES'&lt;wait1s&gt;&lt;enter&gt;&lt;wait1s&gt;exit&lt;enter&gt;&lt;wait1s&gt;",
    "12&lt;enter&gt;&lt;wait6s&gt;y&lt;enter&gt;&lt;wait3s&gt;q"
  ]
}

build {
  sources = ["source.proxmox-iso.opnsense"]
}
</code></pre>

<p>Some interesting notes to point out:</p>

<p><strong>1. config.xml import</strong></p>

<p>In the CD that is being mounted which contains the <code>config.xml</code> file, the <strong>SSH public key</strong> of the host machine is being written to the config by creating a <strong>dynamic variable</strong> in the <code>config.xml</code>. This is done by using the <code>templatefile</code> <a href="https://developer.hashicorp.com/terraform/language/functions/templatefile">function</a>, and providing the path of the public key to it, as shown in the code snippet below:</p>

<pre><code class="language-terraform">cd_content = {
"conf/config.xml" = templatefile("${path.root}/conf/config.xml", {
  # pull the public SSH key, base64 encode it, and write it in config.xml
  dynamic_ssh_key = base64encode(file("~/.ssh/id_rsa.pub"))})
}
</code></pre>

<p>Note that <code>cd_content</code> requires one of the following tools to create the CD: xorriso, mkisofs, hdiutil, oscdimg</p>

<p>In this case, <code>xorriso</code> was installed</p>

<pre><code class="language-bash">sudo apt install xorriso
</code></pre>

<p>In the <code>config.xml</code> file, in order to make it so that it expects a variable in the SSH key field <authorizedkeys>, the dynamic variable was defined as follows:</authorizedkeys></p>

<pre><code class="language-xml">&lt;user uuid="083dd1a5-394d-441f-aae3-481a4ce478c5"&gt;
  &lt;uid&gt;0&lt;/uid&gt;
  &lt;name&gt;root&lt;/name&gt;
  &lt;disabled&gt;0&lt;/disabled&gt;
  &lt;scope&gt;system&lt;/scope&gt;
  &lt;expires/&gt;
  &lt;authorizedkeys&gt;${dynamic_ssh_key}&lt;/authorizedkeys&gt;
  &lt;otp_seed/&gt;
  &lt;shell/&gt;
  &lt;password&gt;$2y$10$YRVoF4SgskIsrXOvOQjGieB9XqHPRra9R7d80B3BZdbY/j21TwBfS&lt;/password&gt;
  &lt;pwd_changed_at/&gt;
  &lt;landing_page/&gt;
  &lt;comment/&gt;
  &lt;email/&gt;
  &lt;apikeys/&gt;
  &lt;priv/&gt;
  &lt;language/&gt;
  &lt;descr&gt;System Administrator&lt;/descr&gt;
  &lt;dashboard/&gt;
&lt;/user&gt;
</code></pre>

<p>As shown, for SSH keys, the dynamic variable <code>${dynamic_ssh_key}</code> is provided instead of a static one.</p>

<p><strong>2. boot command</strong></p>

<p>Additionally, the <code>boot_command</code> includes all the commands executed via the keyboard during the installation of OPNSense using the configuration importer feature. These commands are described below:</p>

<ol>
  <li><strong>Wait</strong> enough time to get to the option to use the <strong>configuration importer</strong>.</li>
  <li>Write the external drive’s name (cd1) to pull the <code>config.xml</code> file from.</li>
  <li><strong>Install the configuration</strong> by logging in with the <code>installer</code> user.</li>
  <li>Go through the <strong>installation steps</strong> (accept default keymap, select ZFS installation, select disk, confirm format and reboot)</li>
  <li><strong>Wait</strong> for the installation and reboot to complete.</li>
  <li>Open a shell (press option “8”), and pass the variable <code>qemu_guest_agent_enable='YES'</code> to <strong>auto start the QEMU agent service</strong> on boot.</li>
  <li><strong>Update OPNSense</strong> to the latest version (press option “12”), because QEMU requires that.</li>
</ol>

<p>The commands to build the Packer template are:</p>

<pre><code class="language-bash">packer init .
packer validate -var-file="../credentials.pkrvars.hcl" .
packer build -var-file="../credentials.pkrvars.hcl" .
</code></pre>

<p>The file <code>credentials.pkrvars.hcl</code> contains the credentials used by Packer and the other components:</p>

<pre><code>proxmox_api_url          = "https://192.168.0.50:8006/api2/json"
proxmox_api_token_id     = "packer@pve!packer-token"
proxmox_api_token_secret = "&lt;proxmox_api_token&gt;"
ubuntu_pw = "lab-admin"
opnsense_pw = "opnsense-admin"
</code></pre>

<p>The output of running the Packer template build is the following:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/f3c1b178-711c-4a50-aa0e-5957f6d95aef" /></p>

<h4 id="opnsense-packer-issues-and-resolutions">OPNSense Packer Issues and resolutions</h4>

<p>During development of Packer OPNSense, the following issues were identified:</p>

<ol>
  <li><strong>The cloud-init limitation:</strong>
    <ul>
      <li><strong>The Issue</strong>: OPNsense is built on FreeBSD and functions as an appliance where the <code>/conf/config.xml</code> is the absolute source of truth. It does not officially support standard Linux cloud-init for initial provisioning (like setting IPs or users).</li>
      <li><strong>The Solution</strong>: OPNsense’s native Configuration Importer was utilized. By packaging a pre-configured config.xml file into a virtual CD drive, OPNsense digests the configuration during the boot sequence and permanently bakes it into the hard drive during the installation.</li>
    </ul>
  </li>
  <li><strong>QEMU guest agent issues with OPNSense version:</strong>
    <ul>
      <li><strong>The Issue</strong>: Packer needs the QEMU Guest Agent to discover the VM’s dynamic IP address to connect via SSH. However, the OPNsense base ISO (25.7) was <strong>out of date</strong> with the plugin repository, meaning the agent would install but would not start its service. It was a classic ‘chicken-and-egg’ scenario: Packer couldn’t SSH in to run the update because the missing QEMU agent meant the IP was undetectable, but the QEMU agent couldn’t be installed without first running the update.</li>
      <li><strong>The Solution</strong>: The setup was baked in the <code>boot_command</code> command list. Instead of stopping after the installation reboot, the command now continues typing in the console to drop into the shell, enabling the <strong>service to auto-start</strong> (sysrc qemu_guest_agent_enable=’YES’), and <strong>trigger a system update</strong> via console option 12. This updates the OS and pulls the agent before Packer ever tries to connect.</li>
    </ul>
  </li>
  <li><strong>OPNsense Firewall Blocking Packer</strong>
    <ul>
      <li><strong>The Issue</strong>: OPNsense is a default-deny firewall that drops all WAN traffic. Packer needs to connect via the WAN interface (mapped to your Proxmox bridge) to finalize the template.</li>
      <li><strong>The Solution</strong>: By ensuring no <code>lan</code> interface was strictly mapped initially, OPNsense’s safety mechanisms trigger the Anti-Lockout rule on the WAN interface, automatically opening port 22 and allowing Packer/Ansible to connect without the need to add a firewall rule to explicitly allow this traffic. This rule will be created on the next phases anyway.</li>
    </ul>
  </li>
</ol>

<p>After finishing with Packer, the result is a “blank” template, ready to be used by Terraform to provision a VM with the final settings.</p>

<h3 id="opnsense-terraform">OPNSense Terraform</h3>

<p>When it comes to using Terraform for provisioning OPNSense, the process was initially straightforward, but, during development, <strong>more and more settings were added to the Terraform phase</strong>, as it was discovered that <strong>Ansible for OPNSense does not provide enough flexibility</strong> to implement what was needed. Therefore, more implementation points were moved from the “Ansible phase” to the “Terraform phase”.</p>

<p>The code can be found in the <a href="https://github.com/KostasKoutrou/kostas-seclab/blob/master/terraform/main.tf">project’s repository</a> and is also written below:</p>

<pre><code class="language-terraform">resource "proxmox_vm_qemu" "c-opnsense" {
    name = "c-opnsense"
    target_node = "kkproxmox"
    clone = "opnsense-template"
    agent = 1 #enable QEMU guest agent
    memory = 4096
    balloon = 4096
    bios = "seabios"
    scsihw = "virtio-scsi-single"
    os_type = "other"
    skip_ipv6 = true

    cpu {
        cores = 4
        sockets = 1
        type = "host"
    }

    disk {
        slot = "scsi0"
        cache = "none"
        discard = true
        iothread = true
        emulatessd = true
        asyncio = "io_uring"
        size = "20G"
        type = "disk"
        storage = "local-lvm"
        format = "raw"
    }

    startup_shutdown {
        order = -1
        shutdown_timeout = -1
        startup_delay = -1
    }

    # Configure the network interfaces
    network {
        id = 0
        model = "virtio"
        bridge = "vmbr0"
        firewall = true
    }
    
    network {
        id = 1
        model = "virtio"
        bridge = "vmbrDMZ20"
        firewall = true
    }

    network {
        id = 2
        model = "virtio"
        bridge = "vmbrIZ30"
        firewall = true
    }

    network {
        id = 3
        model = "virtio"
        bridge = "vmbrEUZ40"
        firewall = true
    }

    # connection used for the provisioners below
    connection {
      type = "ssh"
      user = "root"
      private_key = file("~/.ssh/id_rsa")
      host = self.ssh_host
    }

    # replace the config.xml file with the one below.
    # This one uses dynamic variables to also configure the network interfaces IPs.
    provisioner "file" {
      content = templatefile("${path.module}/template_config_opnsense_lab.xml",
        {
            dynamic_ssh_key = base64encode(file("~/.ssh/id_rsa.pub"))
            wan_if = "vtnet0"
            wan_descr = "WAN"
            wan_ip = "192.168.0.51"
            wan_subnet = "24"
            wan_gw = "WAN_GW"
            dmz20_if = "vtnet1"
            dmz20_descr = "DMZ20"
            dmz20_ip = "10.0.20.1"
            dmz20_subnet = "24"
            iz30_if = "vtnet2"
            iz30_descr = "IZ30"
            iz30_ip = "10.0.30.1"
            iz30_subnet = "24"
            euz40_if = "vtnet3"
            euz40_descr = "EUZ40"
            euz40_ip = "10.0.40.1"
            euz40_subnet = "24"
        }
      )
      destination = "/conf/config.xml"
    }

    # reboot the machine so that the new config.xml is applied
    provisioner "remote-exec" {
      inline = [ 
        "echo 'Injecting Cyber Range Topology by restarting OPNSense. VM will restart in 3 seconds...'",
        "daemon -f /bin/sh -c 'sleep 3; /sbin/reboot'"
       ]
    }
}
</code></pre>

<p>To run the terraform configuration:</p>

<pre><code class="language-bash">terraform init
terraform plan -var-file=../packer/credentials.pkrvars.hcl
terraform apply -var-file=../packer/credentials.pkrvars.hcl
</code></pre>

<p>The var-file is the one used also in Packer and contains the connection variables so that Terraform can talk to Proxmox.</p>

<p>Running the terraform configuration outputs the following. Note that 2 additional test Linux VMs are also provisioned, as they were described in the previous post, but there is no reason to get into them in this post. <strong>The interesting lines are the ones starting with <code>proxmox_vm_qemu.c-opnsense</code></strong>, because these ones are about the OPNSense VM:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/994f387e-e7c4-4cb3-850d-fb33e57f7e5b" /></p>

<p>A few interesting notes to point out:</p>

<ol>
  <li>
    <p><strong>Configuring settings using the config.xml file</strong>: As mentioned previously, <strong>OPNSense is not made to be configured “as-code” natively</strong>, i.e., by using Ansible. Therefore, several settings cannot be configured using Ansible. There is effort put by the community towards achieving that, by implementing more API calls. However, for now, a few basic settings, like configuring the network interfaces, were configured using the <code>config.xml</code> file of OPNSense. All these workarounds made me question whether OPNSense is the right tool for an IaC pipeline, and I thought of switching to something more programmable like <a href="https://vyos.io/">VyOS</a>, but for now I pushed through with OPNSense. The <code>config.xml</code> file used can be found <a href="https://github.com/KostasKoutrou/kostas-seclab/blob/master/terraform/template_config_opnsense_lab.xml">here</a></p>
  </li>
  <li>
    <p><strong>Dynamic variables</strong>: As was the case with Packer, too, several variables in the <code>config.xml</code> file were written dynamically. The <code>config.xml</code> file part where these variables are expected is shown below:</p>

    <pre><code class="language-xml"> &lt;wan&gt;
   &lt;if&gt;${wan_if}&lt;/if&gt;
   &lt;descr&gt;${wan_descr}&lt;/descr&gt;
   &lt;enable&gt;1&lt;/enable&gt;
   &lt;spoofmac /&gt;
   &lt;blockbogons&gt;1&lt;/blockbogons&gt;
   &lt;ipaddr&gt;${wan_ip}&lt;/ipaddr&gt;
   &lt;subnet&gt;${wan_subnet}&lt;/subnet&gt;
   &lt;gateway&gt;${wan_gw}&lt;/gateway&gt;
 &lt;/wan&gt;
 &lt;opt1&gt;
   &lt;if&gt;${dmz20_if}&lt;/if&gt;
   &lt;descr&gt;${dmz20_descr}&lt;/descr&gt;
   &lt;enable&gt;1&lt;/enable&gt;
   &lt;spoofmac /&gt;
   &lt;ipaddr&gt;${dmz20_ip}&lt;/ipaddr&gt;
   &lt;subnet&gt;${dmz20_subnet}&lt;/subnet&gt;
 &lt;/opt1&gt;
 &lt;opt2&gt;
   &lt;if&gt;${iz30_if}&lt;/if&gt;
   &lt;descr&gt;${iz30_descr}&lt;/descr&gt;
   &lt;enable&gt;1&lt;/enable&gt;
   &lt;spoofmac /&gt;
   &lt;ipaddr&gt;${iz30_ip}&lt;/ipaddr&gt;
   &lt;subnet&gt;${iz30_subnet}&lt;/subnet&gt;
 &lt;/opt2&gt;
 &lt;opt3&gt;
   &lt;if&gt;${euz40_if}&lt;/if&gt;
   &lt;descr&gt;${euz40_descr}&lt;/descr&gt;
   &lt;enable&gt;1&lt;/enable&gt;
   &lt;spoofmac /&gt;
   &lt;ipaddr&gt;${euz40_ip}&lt;/ipaddr&gt;
   &lt;subnet&gt;${euz40_subnet}&lt;/subnet&gt;
 &lt;/opt3&gt;
</code></pre>
  </li>
  <li>
    <p><strong>config.xml firewall rules needed</strong>: The Ansible collections used in the next phase require API connectivity and SSH access. Therefore, two firewall rules needed to be injected into the <code>config.xml</code> file:</p>

    <pre><code class="language-xml"> &lt;rule uuid="cf7ad3fc-4924-4fa6-b9c5-77e6f5d81746"&gt;
   &lt;type&gt;pass&lt;/type&gt;
   &lt;interface&gt;wan&lt;/interface&gt;
   &lt;ipprotocol&gt;inet&lt;/ipprotocol&gt;
   &lt;statetype&gt;keep state&lt;/statetype&gt;
   &lt;direction&gt;in&lt;/direction&gt;
   &lt;log&gt;1&lt;/log&gt;
   &lt;quick&gt;1&lt;/quick&gt;
   &lt;protocol&gt;tcp&lt;/protocol&gt;
   &lt;source&gt;
     &lt;any&gt;1&lt;/any&gt;
   &lt;/source&gt;
   &lt;destination&gt;
     &lt;network&gt;wanip&lt;/network&gt;
     &lt;port&gt;22&lt;/port&gt;
   &lt;/destination&gt;
   &lt;description&gt;Allow SSH to OPNSense&lt;/description&gt;
 &lt;/rule&gt;
 &lt;rule uuid="7409d103-737c-4866-83d8-2c1adc5dbcf7"&gt;
   &lt;enabled&gt;1&lt;/enabled&gt;
   &lt;statetype&gt;keep&lt;/statetype&gt;
   &lt;state-policy/&gt;
   &lt;action&gt;pass&lt;/action&gt;
   &lt;quick&gt;1&lt;/quick&gt;
   &lt;interfacenot&gt;0&lt;/interfacenot&gt;
   &lt;interface&gt;wan&lt;/interface&gt;
   &lt;direction&gt;in&lt;/direction&gt;
   &lt;ipprotocol&gt;inet&lt;/ipprotocol&gt;
   &lt;protocol&gt;TCP&lt;/protocol&gt;
   &lt;source_net&gt;wan&lt;/source_net&gt;
   &lt;source_not&gt;0&lt;/source_not&gt;
   &lt;destination_net&gt;wanip&lt;/destination_net&gt;
   &lt;destination_not&gt;0&lt;/destination_not&gt;
   &lt;destination_port&gt;443&lt;/destination_port&gt;
   &lt;disablereplyto&gt;0&lt;/disablereplyto&gt;
   &lt;log&gt;1&lt;/log&gt;
   &lt;allowopts&gt;0&lt;/allowopts&gt;
   &lt;nosync&gt;0&lt;/nosync&gt;
   &lt;nopfsync&gt;0&lt;/nopfsync&gt;
   &lt;description&gt;Allow HTTPS to OPNSense WebUI&lt;/description&gt;
 &lt;/rule&gt;
</code></pre>
  </li>
  <li>
    <p><strong>config.xml API key needed</strong>: The Ansible collections used in the next phase require API connectivity, and therefore an API key. This was generated manually from OPNSense, and the configuration was added to the <code>config.xml</code> file:</p>

    <pre><code class="language-xml"> &lt;user uuid="083dd1a5-394d-441f-aae3-481a4ce478c5"&gt;
   &lt;uid&gt;0&lt;/uid&gt;
   &lt;name&gt;root&lt;/name&gt;
   &lt;disabled&gt;0&lt;/disabled&gt;
   &lt;scope&gt;system&lt;/scope&gt;
   &lt;expires/&gt;
   &lt;authorizedkeys&gt;${dynamic_ssh_key}&lt;/authorizedkeys&gt;
   &lt;otp_seed/&gt;
   &lt;shell/&gt;
   &lt;password&gt;$2y$10$YRVoF4SgskIsrXOvOQjGieB9XqHPRra9R7d80B3BZdbY/j21TwBfS&lt;/password&gt;
   &lt;pwd_changed_at/&gt;
   &lt;landing_page/&gt;
   &lt;comment/&gt;
   &lt;email/&gt;
   &lt;apikeys&gt;ooyOnJ5Y7NiPxeTXkTxEgM+9steLsU+I+UehjuiXtNdBL0ckTvUQM6PWa5AxpdnUXGLGLtyRQFNCnJI8|$6$$c9GWtGrIy25Ez358v52fDrfyTfD3Q5rnzVlc7je/MKL0EzxnTOrOL0mnSh78O.t6iA8hrtd4.OfsWUvUhJgCl0&lt;/apikeys&gt;
   &lt;priv/&gt;
   &lt;language/&gt;
   &lt;descr&gt;System Administrator&lt;/descr&gt;
   &lt;dashboard/&gt;
 &lt;/user&gt;
</code></pre>
  </li>
  <li>
    <p><strong>Applying the configuration - Reboot the machine</strong>: After provisioning the VM, it has the <strong>final configuration</strong> written to the <code>/conf/config.xml</code> file, but it <strong>has not pulled and applied it yet</strong>. It needs to restart to apply it. The provisioner <code>remote-exec</code> was used to execute a reboot command. However, if the reboot command is executed before the script and the terraform configuration finishes, then terraform will believe that the provisioning failed. Therefore, a daemon was started that will reboot the machine in 3 seconds. So, terraform will be able to finish successfully and consider the machine fully provisioned, and then the machine will reboot to pull the configuration.</p>

    <pre><code class="language-terraform"> provisioner "remote-exec" {
   inline = [ 
     "echo 'Injecting Cyber Range Topology by restarting OPNSense. VM will restart in 3 seconds...'",
     "daemon -f /bin/sh -c 'sleep 3; /sbin/reboot'"
    ]
 }
</code></pre>
  </li>
</ol>

<p>In conclusion, while there were not any noteworthy issues by using Terraform for OPNSense, Terraform was actually used to solve some issues presented by Ansible, which is described in the next section.</p>

<h3 id="opnsense-ansible">OPNSense Ansible</h3>

<p>As mentioned previously, OPNSense is not designed to be configured through command line automatically. <strong>It is a GUI-first firewall</strong>. However, efforts have been put towards improving that, and an API is built through which most of its settings can be set.</p>

<p>Considering that the API itself is not yet fully functioning, it was even more difficult to find an Ansible Collection utilizing the API and supporting all the configurations needed to be implemented for this project.</p>

<p>The following Ansible collections were found which were all used in combination:</p>

<ol>
  <li>
    <p><a href="https://ansible-opnsense.oxl.app/">oxlorg.opnsense</a>: most configurations supported</p>

    <p>To install it, run:</p>

    <pre><code class="language-bash"> sudo apt install python3-pip
 python3 -m pip install --upgrade httpx
 # latest version:
 ansible-galaxy collection install git+https://github.com/O-X-L/ansible-opnsense.git
    
 # stable/tested version:
 ansible-galaxy collection install git+https://github.com/O-X-L/ansible-opnsense.git,25.7.8
 ## OR
 ansible-galaxy collection install oxlorg.opnsense # This option was selected for this project.
</code></pre>
  </li>
  <li>
    <p><a href="https://puzzle.github.io/puzzle.opnsense/collections/puzzle/opnsense/index.html">puzzle.opnsense</a>: Does not have many configurations, but it supports network interface assignments.</p>

    <p>To install it, run:</p>

    <pre><code class="language-bash"> ansible-galaxy collection install puzzle.opnsense
</code></pre>
  </li>
</ol>

<p>The Ansible playbook for OPNSense with comments can be found <a href="https://github.com/KostasKoutrou/kostas-seclab/blob/master/ansible/opnsense_config.yml">here</a> and is provided below:</p>

<pre><code class="language-yaml">---
- name: Configure OPNSense via SSH
  hosts: opnsense
  # connection: local  # execute modules on firewall
  gather_facts: false
  
  tasks:
    - name: Assign Interfaces
      puzzle.opnsense.interfaces_assignments:
        description: "{{ item.description }}"
        device: "{{ item.device }}"
        identifier: "{{ item.identifier }}"
      loop:
        - { description: "DMZ20", device: "vtnet1", identifier: "opt1" }
        - { description: "IZ30", device: "vtnet2", identifier: "opt2" }
        - { description: "EUZ40", device: "vtnet3", identifier: "opt3" }
        - { description: "WAN", device: "vtnet0", identifier: "wan" }


- name: Configure OPNSense via API
  hosts: opnsense
  connection: local  # execute modules on controller
  gather_facts: false
  vars:
    # because this collection is designed to be ran on the OPNSense, it was trying to find
    # the default FreeBSD python path, instead of the one running locally. Setting this
    # variable defines the correct path for ansible.
    ansible_python_interpreter: "{{ ansible_playbook_python }}"
  module_defaults:
    group/oxlorg.opnsense.all:
      ssl_verify: false
      firewall: "{{ inventory_hostname }}" # pull the IP directly from invetory.ini
      # export of generated API keys from OPNSense
      api_credential_file: "{{ playbook_dir }}/OPNsense.internal_root_apikey.txt"

  tasks:
    # OPNSense Ansible and API do not support configuring ISC DHCP, so Kea DHCP was used instead
    - name: Enable Kea DHCP EUZ40
      oxlorg.opnsense.dhcp_general:
        enabled: true
        interfaces: ['opt3']
        fw_rules: true
        
    - name: Set Kea DHCP EUZ40 Subnet
      oxlorg.opnsense.dhcp_subnet:
        subnet: "10.0.40.0/24"
        pools: ["10.0.40.150-10.0.40.200"]
        dns: ["1.1.1.1", "8.8.8.8"]
        ipv: 4

    - name: Set Firewall Rules
      oxlorg.opnsense.rule_multi:
        rules:
        # This rule is not needed because it is needed and was added before Ansible, because
        # this Ansible collection talks via API, i.e., via port 443.
        # - description: 'Allow HTTPS to OPNSense WebUI' #name: 'Allow HTTPS to OPNSense WebUI'
        #   source_net: 'wan'
        #   destination_net: 'wanip'
        #   destination_port: 443
        #   interface: ['wan']
        #   protocol: 'TCP'
        #   action: 'pass'

        - description: 'EUZ40 allow all traffic' #name: EUZ40 allow all traffic
          source_net: 'any'
          destination_net: 'any'
          # destination_port: leave empty for all
          interface: ['opt3']
          protocol: 'any'
          action: 'pass'
          
        match_fields: ['description']
        reload: true

    # These tasks can be used to retrieve information
    # - name: Listing
    #   oxlorg.opnsense.list:
    #     target: 'rule'
    #   register: existing_entries

    # - name: Printing rules
    #   ansible.builtin.debug:
    #     var: existing_entries.data
</code></pre>

<p>A few interesting notes about the Ansible playbook:</p>

<p><strong>API key definition</strong>: the API key used to authenticate is pulled from the path <code>/OPNsense.internal_root_apikey.txt</code>. This file was exported manually from an OPNSense, as described in the previous section, and saved at the path above. It is noteworthy that it is not supported to modify this file at all. The ansible tasks are looking for the format below:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/dc8c25bf-00f9-4949-81ce-362e901d6ac3" /></p>

<p>Changing the format in any way results in the following error:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/cc474444-4f96-40dd-898b-c27a313a11ad" /></p>

<p>After configuring OPNSense with Ansible, the next step is to configure Proxmox itself for the OPNSense VM.</p>

<h3 id="configuring-proxmox-for-opnsense-with-ansible">Configuring Proxmox for OPNSense with Ansible</h3>

<p>In addition to configuring OPNSense itself, some settings need to be configured on Proxmox regarding the hosted OPNSense VM. More specifically, these include configuring the Proxmox firewall settings for the VM.</p>

<p>The Ansible playbook for this purpose is relatively short, can be found <a href="https://github.com/KostasKoutrou/kostas-seclab/blob/master/ansible/proxmox_opnsense_config.yml">here</a> and is the following:</p>

<pre><code class="language-yaml">---
- name: Config Proxmox for OPNSense
  hosts: proxmox
  connection: local
  gather_facts: false

  tasks:

    - name: Config Firewall OPNSense
      community.proxmox.proxmox_firewall:
        level: vm
        vmid: 101
        update: true
        state: present
        rules:
          - action: allowguesttraffic # same rule for both cluster and node levels
            pos: 0
            type: group
            enable: true
          - action: blockhomenwtraffic
            pos: 1
            type: group
            enable: true

    # These tasks can be used to retrieve the current config
    # - name: Config Firewall OPNSense
    #   community.proxmox.proxmox_firewall_info:
    #     level: vm
    #     vmid: 100
    #   register: debug_fw_opnsense

    # - name: Show debug
    #   debug:
    #     var: debug_fw_opnsense
</code></pre>

<p>The playbook is straightforward, and assigns the security groups (created in the previous sections) to the VM.</p>

<p>To run the playbook, as was the case with the playbook for proxmox itself, the envars described in the previous section need to be set firstly. To run the playbook, run:</p>

<pre><code class="language-bash">ansible-playbook -i inventory.ini proxmox_opnsense_config.yml 
</code></pre>
<p>Running the playbook outputs the following:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/00775ec7-ea55-49ef-ba79-fa437b9d9579" /></p>

<p>Now that Proxmox is configured for the hosted OPNSense VM, this part of the project is finished. Now let’s take a look a potential next steps.</p>

<h2 id="next-steps">Next Steps</h2>

<p>The next steps include implementing some not mandatory final touches on the automation part of Proxmox and OPNSense, including:</p>

<ol>
  <li>Automating the installation process of Proxmox itself. This is supported by Proxmox with the <a href="https://pve.proxmox.com/wiki/Automated_Installation">Automated Installation</a>.</li>
  <li>Automating the users creation to Proxmox</li>
  <li>Automating the changing of update repositories of Proxmox from the default ones which require a license to the free ones</li>
  <li>Adding the ISO files to Proxmox, or alternatively switching the Packer templates to download the ISOs from the internet directly instead of trying to grab them from Proxmox locally.</li>
  <li>Creating a file serving as the single source of truth. All the variables used in the project will be defined in that file, including IP addresses, usernames, etc. The file will be referenced by all code.</li>
</ol>

<p>Additionally, the next steps include building out the rest of the components described in the first post.</p>

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

<p>In this post, the base was built under the context and concept of the project, i.e. utilizing IaC.</p>

<p>As it is probably understood through the post, building each component of this project proved to be more complex than expected, as each component has its own peculiarities and workarounds required in order for it to work in the context of IaC and Configuration Management.</p>]]></content><author><name>Kostas Koutroumpouchos</name></author><category term="Other" /><summary type="html"><![CDATA[Introduction]]></summary></entry><entry><title type="html">Cyber Range as Code: Automating Security Lab with IaC - Part 1</title><link href="https://kostaskoutrou.github.io/2026/02/02/cyber-range-as-code-part1.html" rel="alternate" type="text/html" title="Cyber Range as Code: Automating Security Lab with IaC - Part 1" /><published>2026-02-02T00:00:00+00:00</published><updated>2026-02-02T00:00:00+00:00</updated><id>https://kostaskoutrou.github.io/2026/02/02/cyber-range-as-code-part1</id><content type="html" xml:base="https://kostaskoutrou.github.io/2026/02/02/cyber-range-as-code-part1.html"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>What is a Cyber Range?</p>

<blockquote>
  <p>A <a href="https://www.ibm.com/think/topics/cyber-range">Cyber Range</a> is a virtual environment for cybersecurity training, testing, and research that simulates real-world networks and cyberattacks.</p>
</blockquote>

<p>I always wanted to build a security homelab, where I will have the freedom and the infrastructure to build, secure, attack, monitor, and defend a full-on infrastructure. At the same time, I want to build all of this “as code”. I really believe in converting everything to “as code”, because this approach solves many issues of manual work, like manual maintenance, configuration drift, complex change history, and no version control. It also facilitates the ability to rebuild the infrastructure as quickly as possible. I was thinking of naming this project “Security Playground” at first or something like that, but I thought “Cyber Range as Code” was more catchy and it grasped the two main concepts that I want to focus on for this project.</p>

<p>The end goal of this project is to <strong>build and maintain a real-world infrastructure</strong> to:</p>

<ul>
  <li><strong>Test attack scenarios</strong> and
    <ul>
      <li>Monitor the result of the attack first-hand</li>
      <li>Review the generated logs to understand what happened on the backend, what could be detected and created as a detection rule</li>
    </ul>
  </li>
  <li>Implement <strong>security standards</strong> like ISO27001, NIS2, NIST, and CIS</li>
  <li><strong>Architect a secure infrastructure</strong>: This will be a never ending goal, as, even in a relatively simple infrastructure like the one which will be built under this project, there always is something to improve in terms of security architecture.</li>
  <li><strong>Recover seamlessly</strong>: <strong>All the infrastructure must be deployed automatically</strong>. The end goal is seamless recovery: If it is ever needed to redeploy the infrastructure on a new server, it can be done with minimal manual effort. This is where <strong>the project leans heavily into IaC</strong>, as it will be explained more in the next sections.</li>
</ul>

<p>This project covers a lot of concepts about IaC, security hardening, security standards, blue teaming, and red teaming. Hopefully the end result will allow other security professionals to deploy this infrastructure and easily get to security testing.</p>

<p>In this first post, a <strong>high level design</strong> of the end result is described, of the <strong>tech stack</strong> that is planned to be used, as well as <strong>an early PoC</strong> regarding the infrastructure and its deployment using IaC.</p>

<p>So let’s get started!</p>

<hr />

<h2 id="contents">Contents</h2>

<ul id="markdown-toc">
  <li><a href="#introduction" id="markdown-toc-introduction">Introduction</a></li>
  <li><a href="#contents" id="markdown-toc-contents">Contents</a></li>
  <li><a href="#automated-infrastructure" id="markdown-toc-automated-infrastructure">Automated Infrastructure</a></li>
  <li><a href="#tech-stack" id="markdown-toc-tech-stack">Tech stack</a></li>
  <li><a href="#architecture" id="markdown-toc-architecture">Architecture</a></li>
  <li><a href="#initial-configuration--poc" id="markdown-toc-initial-configuration--poc">Initial Configuration / PoC</a>    <ul>
      <li><a href="#proxmox-configuration" id="markdown-toc-proxmox-configuration">Proxmox Configuration</a>        <ul>
          <li><a href="#network-configuration" id="markdown-toc-network-configuration">Network Configuration</a></li>
          <li><a href="#proxmox-firewall" id="markdown-toc-proxmox-firewall">Proxmox Firewall</a></li>
        </ul>
      </li>
      <li><a href="#manual-opnsense-installation" id="markdown-toc-manual-opnsense-installation">Manual OPNsense Installation</a>        <ul>
          <li><a href="#vm-creation" id="markdown-toc-vm-creation">VM Creation</a></li>
          <li><a href="#vm-installation" id="markdown-toc-vm-installation">VM Installation</a></li>
          <li><a href="#opnsense-network-interfaces" id="markdown-toc-opnsense-network-interfaces">OPNsense network interfaces</a></li>
          <li><a href="#opnsense-firewall-rules" id="markdown-toc-opnsense-firewall-rules">OPNsense Firewall rules</a></li>
          <li><a href="#opnsense-dhcp" id="markdown-toc-opnsense-dhcp">OPNsense DHCP</a></li>
        </ul>
      </li>
      <li><a href="#packer-configuration" id="markdown-toc-packer-configuration">Packer Configuration</a>        <ul>
          <li><a href="#prepare-proxmox-for-packer" id="markdown-toc-prepare-proxmox-for-packer">Prepare Proxmox for Packer</a></li>
          <li><a href="#preparing-packer" id="markdown-toc-preparing-packer">Preparing packer</a></li>
          <li><a href="#packer-pitfalls-solutions-and-lessons-learnt" id="markdown-toc-packer-pitfalls-solutions-and-lessons-learnt">Packer pitfalls, solutions, and lessons learnt</a></li>
          <li><a href="#packer-build-and-poc-result" id="markdown-toc-packer-build-and-poc-result">Packer build and PoC result</a></li>
        </ul>
      </li>
      <li><a href="#terraform-configuration" id="markdown-toc-terraform-configuration">Terraform Configuration</a>        <ul>
          <li><a href="#prepare-proxmox-for-terraform" id="markdown-toc-prepare-proxmox-for-terraform">Prepare Proxmox for Terraform</a></li>
          <li><a href="#preparing-terraform" id="markdown-toc-preparing-terraform">Preparing Terraform</a></li>
          <li><a href="#terraform-pitfalls-solutions-and-lessons-learnt" id="markdown-toc-terraform-pitfalls-solutions-and-lessons-learnt">Terraform pitfalls, solutions, and lessons learnt</a></li>
          <li><a href="#terraform-apply-and-poc-result" id="markdown-toc-terraform-apply-and-poc-result">Terraform apply and PoC result</a></li>
        </ul>
      </li>
      <li><a href="#ansible" id="markdown-toc-ansible">Ansible</a>        <ul>
          <li><a href="#running-ansible" id="markdown-toc-running-ansible">Running Ansible</a></li>
        </ul>
      </li>
    </ul>
  </li>
  <li><a href="#next-steps" id="markdown-toc-next-steps">Next Steps</a></li>
  <li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
</ul>

<hr />

<h2 id="automated-infrastructure">Automated Infrastructure</h2>

<p>As mentioned above, one core aspect of this project is to be able to <strong>have the whole infrastructure deployed automatically</strong>. This means that the following practices will be utilized.</p>
<ul>
  <li><strong>Infrastructure as Code (IaC)</strong>: All the infrastructure will be managed and provisioned <strong>using code and configuration files</strong>, instead of manual processes. This enables automation, consistency, version control, and swift, repeatable deployments. In the case specifically of this Cyber Range project, this will also allow quick recovery in case of an attack resulting in part of the infrastructure or the whole infrastructure being compromised and damaged unrecoverably.</li>
  <li>For security hardening and implementing security standards, in addition to IaC, the following practices will also be utilized:
    <ul>
      <li><strong>Policy as Code (PaC)</strong>: PaC (<a href="https://www.paloaltonetworks.com/cyberpedia/what-is-policy-as-code">ref1</a>, <a href="https://developer.hashicorp.com/sentinel/docs/concepts/policy-as-code">ref2</a> , <a href="https://www.hashicorp.com/en/blog/policy-as-code-explained">ref3</a>) is <strong>the practice of defining, updating, sharing and enforcing policies using code</strong>. With this approach, when compared to manual processes to manage policies, the benefits are:
        <ul>
          <li><em>Sandboxing</em>: Policies provide guardrails for other automated systems. By defining Policies as Code, <strong>the verification by the policies is automated</strong>, reducing the time needed of manual work.</li>
          <li><em>Codification</em>: Because the policy is defined as code, it is possible to <strong>describe the logic about the policy directly on the code with comments</strong>, which results in better understand and knowledge sharing of these policies.</li>
          <li><em>Version Control</em>: The benefits of version control are well known (history, diffs, pull requests, etc.).</li>
          <li><em>Testing</em>: Because the policies are defined as code, they can be tested by utilizing <strong>automated testing</strong> such as through a CI/CD pipeline. This allows for testing if a policy will result in the expected outcome before deploying to production.</li>
          <li><em>Automation</em>: Similarly to IaC, with PaC, tools can be used to <strong>automatically deploy the policies to specified systems</strong>.</li>
          <li><em>Compliance as Code (CaC) / Security as Code (SaC)</em>: This is where the concepts of <a href="https://www.puppet.com/blog/compliance-as-code">CaC</a> and <a href="https://www.isaca.org/resources/news-and-trends/isaca-now-blog/2024/security-as-code-a-key-building-block-for-devsecops">SaC</a> are also related.</li>
          <li>Some <strong>examples</strong> of what rules can be enforced as policy with PaC are the following:
            <ul>
              <li>All ‘victim’ VMs must be on the isolated VLAN, not the management VLAN.</li>
              <li>No single VM can be assigned more than 4 CPU cores or 8GB RAM.</li>
              <li>All VMs must clone from the template X.</li>
              <li>Terraform must not use the default OS users to perform actions.</li>
              <li>Specific fields in Terraform must not be left empty.</li>
              <li>Every VM must have a description field explaining its purpose.
PaC is something that will be implemented at a later phase of the project, after the IaC phase is at a mature state.</li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li><strong>Detection as Code (DaC)</strong>: <a href="https://www.legitsecurity.com/aspm-knowledge-base/detection-as-code">DaC</a> enables the writing, maintenance, and automation of the threat detection logic as if it were software code, making security a built-in part of the development pipeline. Similar to PaC, DaC will start being implemented at a later phase of the project.</li>
</ul>

<h2 id="tech-stack">Tech stack</h2>

<p>For this project, a wide variety of systems and technologies will be used, as the purpose is, among others, to simulate a real-world IT infrastructure.</p>

<p>The following technologies, for now, are strong contenders to be deployed. Since the project is just starting now, some technologies are only thought of as ideas, and may or may not actually be deployed when the time comes. I am looking for recommendations, though. Send me a message if you have something to recommend:</p>

<ul>
  <li><a href="https://www.proxmox.com/en/"><strong>Proxmox</strong></a>: The whole infrastructure will be deployed on a physical server running Proxmox Virtual Environment (PVE) as a Type-1 Hypervisor. The infrastructure will be built with VMs and Containers.</li>
  <li>For the <strong>IaC</strong> aspect, the following 3 tools will be used:
    <ul>
      <li><a href="https://developer.hashicorp.com/packer"><strong>Packer</strong></a>: Packer is a community tool for <strong>creating identical machine images</strong> for multiple platforms from a single source configuration. Essential, Packer:
        <ol>
          <li>Takes an ISO file of an OS</li>
          <li>Installs it on a temporary VM</li>
          <li>Applies any defined action during the installation (language, locale, hard disk to install the OS, etc.)</li>
          <li>Applies any configuration defined (e.g., IP address)</li>
          <li>Installs any package defined and</li>
          <li>Converts that VM to a template.
That template is made for the platform on which the temporary VM was created on. In the case of this project, since the VM is created in Proxmox, the template will be a Proxmox VM template. <strong>This template is then used by Terraform to provision VMs which will have that configuration ready immediately</strong>. Packer provides the freedom of utilizing any ISO and converting it to a template exactly for the needs of the task at hand.</li>
        </ol>
      </li>
      <li><a href="https://developer.hashicorp.com/terraform"><strong>Terraform</strong></a>: HashiCorp Terraform is an <strong>infrastructure as code tool</strong> that enables the definition of both cloud and on-prem resources in human-readable configuration files that can be versioned, reused, and shared. Terraform can manage low-level components like compute, storage, and networking resources, as well as high-level components like DNS entries and SaaS features. <strong>In the context of the project, the templates created by Packer will used by Terraform to provision all the infrastructure defined</strong>. Note here that many of the configurations of the template defined by Packer can be changed during the provisioning by Terraform (e.g., RAM, CPU cores, installed packages, etc.), which provides more freedom during provisioning. But with Packer the benefit with the resulting template is that there is no need to install the packages already installed and execute any time consuming action every time a new VM is provisioned, because it was already done during the Packer VM template creation.</li>
      <li><a href="https://docs.ansible.com/"><strong>Ansible</strong></a>: Ansible is an automation language which allows for automating essentially any IT task. <strong>For the project, Ansible is used for further configuring all the VMs provisioned by Terraform</strong>, like installing additional packages, starting services, connections, etc. Ansible uses SSH or WinRM to execute remote commands on machines. One of the benefits of Ansible is what is called <strong>Idempotency</strong>. This means that Ansible only makes changes if necessary, preventing unintended side effects. An Ansible playbook is written in YAML, and in a playbook the final desired state of the target machine is described. Then it is up to Ansible to make any changes or no changes depending on whether the target machine already has the final desired state.</li>
    </ul>
  </li>
  <li><a href="https://opnsense.org/"><strong>OPNsense</strong></a>: OPNsense is an open-source next-gen grade firewall and routing platform, which brings all the features provided by commercial products to the open-source world. It is the most widely used open-source firewall, provides all the features necessary for this project, and <strong>will be used as the central firewall</strong>, controlling the traffic among the different subnets and machines.
    <ul>
      <li><a href="https://suricata.io/"><strong>Suricata</strong></a>: Suricata is an open-source network analysis and threat detection software. <strong>Suricata will be the network-based IDS/IPS solution of this project</strong>. <strong>OPNsense provides an integration with Suricata</strong>, which will allow for an experience of a next-gen firewall in just a few steps.</li>
    </ul>
  </li>
  <li><strong>SIEM/XDR</strong>: When it comes to the XDR solution to be deployed for this project, a few alternatives are taken into consideration:
    <ul>
      <li><a href="https://wazuh.com/"><strong>Wazuh</strong></a>: Wazuh is an open-source security platform, which brings a lot of features under the XDR and SIEM umbrella. This includes:
        <ul>
          <li>Configuration Assessment</li>
          <li>Malware Detection</li>
          <li>File Integrity Monitoring</li>
          <li>Threat Hunting</li>
          <li>Log Data Analysis</li>
          <li>Vulnerability Detection</li>
          <li>Incident Response</li>
          <li>Regulatory Compliance</li>
          <li>IT Hygiene</li>
          <li>Containers Security</li>
          <li>Posture Management</li>
          <li>Workload Protection</li>
        </ul>
      </li>
      <li><a href="https://www.microsoft.com/en-us/security/business/siem-and-xdr/microsoft-defender-xdr#capabilities"><strong>Microsoft Defender XDR</strong></a>: One of the most widely used XDR solutions in the market. It is a full commercial XDR solution, and it would require multiple posts to describe it fully. While this is not a free solution, hopefully I will figure out how to implement it in a lab environment because I would be really interested in including Microsoft Defender XDR in this Cyber Range project.</li>
      <li><a href="https://securityonionsolutions.com/"><strong>Security Onion</strong></a>: Security Onion is a free platform providing a series of features, including:
        <ul>
          <li>Network visibility using Suricata</li>
          <li>Intrusion detection honeypots based on <a href="https://github.com/thinkst/opencanary">OpenCanary</a></li>
          <li>Log management with the <a href="https://www.elastic.co/elastic-stack">Elastic Stack</a></li>
          <li>File extraction with <a href="https://zeek.org/">Zeek</a> or Suricata</li>
          <li>Full packet capture with <a href="https://docs.securityonion.net/en/2.4/stenographer.html">Stenographer</a></li>
          <li>File analysis with <a href="https://github.com/target/strelka">Strelka</a></li>
          <li>Host visibility with <a href="https://www.elastic.co/elastic-agent">Elastic agent</a></li>
          <li>Centralized management with <a href="https://www.elastic.co/docs/reference/fleet">Elastic Fleet</a></li>
        </ul>
      </li>
    </ul>
  </li>
  <li><a href="https://modsecurity.org/"><strong>ModSecurity</strong></a> + <a href="https://coreruleset.org/"><strong>OWASP Core Rule Set (CRS)</strong></a>: ModSecurity is an open source cross-platform <strong>Web Application Firewall (WAF)</strong>. Since its version 3 release, it now works as a standalone module which provides the capability to load/interpret rules written in the ModSecurity SecRules format and apply them to HTTP content provided by the web application via ModSecurity Connectors. This will prove useful, since in its previous module it worked as an Apache module only, while now it is more independent of the web server solution that it protects. ModSecurity on its own does not provide detection/protection rules. The OWASP CRS is a set of generic attack detection rules to be used with ModSecurity.</li>
  <li>The Actual <strong>Infrastructure</strong> that will be secured and attacked will include:
    <ul>
      <li><strong>Linux servers</strong>:
        <ul>
          <li><a href="https://github.com/digininja/DVWA"><strong>DVWA</strong></a>: Damn Vulnerable Web Application (DVWA) is a PHP/MariaDB web application which <strong>can be configured to be vulnerable against different types of web-based attacks</strong>. This will be used for simulating attacks, as well as attempting to protect the application with different measures even though it is vulnerable.</li>
          <li><a href="https://owasp.org/www-project-juice-shop/"><strong>OWASP Juice Shop</strong></a>: Similar to DVWA, it is another vulnerable web application.</li>
          <li><a href="https://owasp.org/www-project-webgoat/"><strong>OWASP WebGoat</strong></a>: Another Java-based vulnerable application.</li>
        </ul>
      </li>
      <li><strong>Windows</strong>: In order to get close to a real-world infrastructure, different Windows components will also need to be configured:
        <ul>
          <li>Active Directory Domain Services</li>
          <li>Windows Workstations</li>
          <li>Windows Servers with different configured roles</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>For the <strong>Red Team side</strong>, a few potential and very useful candidates are the following:
    <ul>
      <li><a href="https://www.atomicredteam.io/"><strong>Atomic Red Team</strong></a>: It is an open source library of tests designed to test the applied security controls.</li>
      <li><a href="https://caldera.mitre.org/"><strong>MITRE Caldera</strong></a>: Caldera is a much more complex framework than Atomic Red Team. It is a cybersecurity framework which can enable the following:
        <ul>
          <li><em>Autonomous Adversary Emulation</em>: It is possible to build a specific threat profile and launch it in a network to see vulnerable points.</li>
          <li><em>Test and Evaluation of Detection, Analytic and Response Platforms</em>: It provides automated testing of cyber defense measures.</li>
          <li><em>Manual Red-Team Engagements</em>: It augments existing offensive toolsets.</li>
        </ul>
      </li>
      <li><a href="https://www.purplesharp.com/en/latest/"><strong>PurpleSharp</strong></a>: It is an adversary simulation tool <strong>focused on Windows Active Directory</strong> Environments. It currently supports 47 MITRE ATT&amp;CK techniques.</li>
      <li><a href="https://github.com/guardicore/monkey"><strong>Injection Monkey</strong></a>: Maintained by Akamai (originally by GuardiCore), it is an adversary emulation platform, where the basic idea is that a “worm” is dropped on a machine, and it tries to spread to every other machine in the network using common exploits and weak passwords.</li>
    </ul>
  </li>
  <li><strong>Policy as Code</strong>: In the later stages of the project, after the IaC part is at a mature state, the next step will be to proceed with the Policy as Code. For now, the potential candidates for PaC are the following:
    <ul>
      <li><a href="https://www.openpolicyagent.org/"><strong>Open Policy Agent</strong></a>: OPA is a general-purpose policy engine that unifies policy enforcement across the stack. It uses a <strong>high-level declarative language that allows the specification of policies for a wide range of use cases</strong>. In the case of the project, it will mostly be used for security hardening and compliance aspects.</li>
      <li><a href="https://www.hashicorp.com/en/sentinel"><strong>HashiCorp Sentinel</strong></a>: Sentinel is a <strong>Policy as Code framework for HashiCorp products</strong>, defining what is allowed and what is prohibited. In the context of the project, Sentinel could be used for Packer and Terraform, but cannot be extended for Ansible playbooks.</li>
    </ul>
  </li>
  <li><strong>Detection as Code</strong>: Similarly to PaC, DaC is something that will be implemented later on. A few candidates identified are the following:
    <ul>
      <li><a href="https://sigmahq.io/docs/guide/about.html"><strong>Sigma Rules</strong></a>: Sigma rules are YAML files that contain all the information required to detect specified malicious behaviour when inspecting log files.</li>
      <li><a href="https://virustotal.github.io/yara/"><strong>YARA</strong></a>: YARA is a tool that assists in defining malware samples. A YARA rule includes meta-information about the malware, and a set of strings and conditions to detect the malware (signatures).</li>
    </ul>
  </li>
</ul>

<h2 id="architecture">Architecture</h2>

<p>The initial infrastructure architecture is the following. It is important to note here that this is the initial architecture idea, it is not final, and there may very well be changes during implementation.</p>

<p><img src="https://github.com/user-attachments/assets/daeb1cd9-9816-49d0-874e-2836c15d9d86" alt="NetworkDesign-FW Rules drawio" /></p>

<p>As shown above, the architecture is a relatively simple and typical network infrastructure, with the following components:</p>

<ul>
  <li>The central Firewall, controlling the network traffic among the four network zones:
    <ol>
      <li><strong>Demilitarized Zone (DMZ)</strong>: The zone that exposes services which are to be served to the Internet. In the context of the project, these will be served to the local network, and, most importantly for the project, will be accessible by the “External Attacker”, enabling for attack scenarios initiated from the “Internet”.</li>
      <li><strong>Internal Zone</strong>: The zone where all there internal servers and services will reside. This includes:
        <ul>
          <li>Any internal servers, e.g., SQL Servers and AD DC, hosting and serving information which is destined to be consumed by internal resources only.</li>
          <li>Security Tools, including the SIEM/XDR/Monitoring tools.</li>
        </ul>
      </li>
      <li><strong>End Users</strong>: The last zone will be for the End Users, where typical workstation VMs will reside, and have defined access to specific servers/services and to the internet.</li>
      <li><strong>WAN Zone</strong>: This is where the “Internet”, in the context of the Infrastructure, lives. This is where:
        <ul>
          <li>The PC from which the Proxmox management will be done, running the Packer, Terraform, and Ansible.</li>
          <li>The External Attacks will occur from.</li>
        </ul>
      </li>
    </ol>
  </li>
  <li>There will also be Internal Attack Simulations, executed from within the different zones, bypassing the firewall (“assume breach”).</li>
</ul>

<h2 id="initial-configuration--poc">Initial Configuration / PoC</h2>

<p>In this section, <strong>an initial configuration of the infrastructure is described, with only the bare minimum of components</strong>, as well as a <strong>PoC of the infrastructure</strong> working as described in the previous sections, i.e., with IaC practices.</p>

<p>All the code used in this PoC can be found under the project’s repository <a href="https://github.com/KostasKoutrou/kostas-seclab">kostas-seclab</a>.</p>

<h3 id="proxmox-configuration">Proxmox Configuration</h3>

<p>The installation of Proxmox is a typical installation of any OS. Because the physical machine is connected to the home router, the following network configuration was added:</p>

<ul>
  <li><strong>IP Address</strong>: Static 192.168.0.50/24 - Outside of the DHCP range of the home router, but in the same subnet.</li>
  <li><strong>Gateway</strong>: 192.168.0.1 - The IP Address of the home router.</li>
  <li><strong>DNS</strong>: 1.1.1.1 - Used Cloudflare’s general use DNS IP address.</li>
</ul>

<p><img alt="image" src="https://github.com/user-attachments/assets/d4e74abb-2a23-48a6-8d1a-d5d63a1840a8" /></p>

<h4 id="network-configuration">Network Configuration</h4>

<p>Proxmox VE is using the <a href="https://pve.proxmox.com/wiki/Network_Configuration">Linux network stack</a>. A Linux bridge interface (usually named <em>vmbrX</em>) is needed to connect guests to the underlying physical network. It can be thought of as a <strong>virtual switch</strong> which the <strong>guests</strong> and <strong>physical interfaces</strong> are connected to.</p>

<p>The network configuration for the project utilized Linux Bridges to create the defined zones.</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/8121de5d-694b-4668-a7d9-fabdb06bc79b" /></p>

<p>In the screenshot above, the utilized objects are the following:</p>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Type</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>nic1</td>
      <td>Network Device</td>
      <td>This is the <strong>physical interface</strong> which physically connects from the Proxmox VE server to the home router, enabling connectivity to the home PC and to the internet.</td>
    </tr>
    <tr>
      <td>vmbr0</td>
      <td>Linux Bridge</td>
      <td>It is through this Linux Bridge that the Proxmox VE server actually <strong>connects to the home router</strong>, as depicted in the <code>Ports/Slaves</code> column, where the <code>nic1</code> is defined. The default gateway for the Proxmox server is also defined through this bridge.</td>
    </tr>
    <tr>
      <td>vmbrDMZ20</td>
      <td>Linux Bridge</td>
      <td><strong>The Linux Bridge for the DMZ</strong>. The “20” in the name is there for convenience of knowing which subnet the zone has (10.0.20.0/24). The IP of the bridge is 10.0.20.2/24, because the .1 will be assigned to the OPNsense interface.</td>
    </tr>
    <tr>
      <td>vmbrEUZ40</td>
      <td>Linux Bridge</td>
      <td><strong>The Linux Bridge for the End Users zone</strong>. The “40” in the name is there for convenience of knowing which subnet the zone has (10.0.40.0/24). The IP of the bridge is 10.0.40.2/24, because the .1 will be assigned to the OPNsense interface.</td>
    </tr>
    <tr>
      <td>vmbrIZ30</td>
      <td>Linux Bridge</td>
      <td><strong>The Linux Bridge for the Internal Zone</strong>. The “30” in the name is there for convenience of knowing which subnet the zone has (10.0.30.0/24). The IP of the bridge is 10.0.30.2/24, because the .1 will be assigned to the OPNsense interface.</td>
    </tr>
  </tbody>
</table>

<h4 id="proxmox-firewall">Proxmox Firewall</h4>

<p>In addition to the OPNsense which will operate as the central Firewall of the infrastructure, <strong>the <a href="https://pve.proxmox.com/pve-docs/pve-admin-guide.html#chapter_pve_firewall">firewall</a> of Proxmox was also enabled</strong>, with the purpose of isolating the Infrastructure from being able to access the home router local network. Think of it as a “Defence in Depth” approach.</p>

<p>Before explaining how firewalling works in Proxmox, let’s briefly review how the VMs and nodes are structured in Proxmox:</p>

<p><img width="253" height="343" alt="image" src="https://github.com/user-attachments/assets/ffaf5587-0b5b-45f2-b509-ecace5c234fd" /></p>

<p>In the above screenshot this structure is depicted. More specifically, there is:</p>

<ul>
  <li><strong>Datacenter</strong>: This is the top level of abstraction. Under datacenter all the proxmox servers are listed. If a Proxmox cluster was created, there would be more than one, but since in the project only one physical server is used, there is only one entry.</li>
  <li><strong>Proxmox VE Nodes (Host)</strong>: The “kkproxmox” node is the physical server running the Proxmox VE.</li>
  <li><strong>VMs under each Proxmox VE Node</strong>: Under each Proxmox VE node, the different resources deployed are listed. This includes VMs, VM templates, storage, network, etc.</li>
</ul>

<p>The Proxmox VE firewall groups the network into the above <strong>logical zones</strong> (there is also a zone related to SDN which is out of scope for this PoC). The firewall as a functionality can be enabled on any of the zones described above. For example, if the firewall is needed to be enabled on a VM, then the firewall needs to be enabled on the Datacenter, the Proxmox node hosting that VM, the VM itself, and on each virtual network interface of that VM. Also, when a firewall rule is created on one of the levels described above, it applies to all the levels under it. For example, if a rule is create on the Proxmox VE Node, then it applies to all the VMs under that Node.</p>

<p>In Proxmox, firewall rules can be defined for different directions:</p>

<ul>
  <li><strong>In</strong>: Traffic that is arriving in a zone</li>
  <li><strong>Out</strong>: Traffic that is leaving a zone</li>
  <li><strong>Forward</strong>: Traffic that is passing through a zone. In the host zone this can be routed traffic (when the host is acting as a gateway or performing NAT). At a VNet-level this affects all traffic that is passing by a VNet, including traffic from/to bridged network interfaces.</li>
</ul>

<p>There are default rules applied when enabling firewall. For <code>In</code> traffic the default rule is <em>Deny</em> and for <code>Out</code> traffic, the default rule is <em>Allow</em>.</p>

<p>For the project, the following configuration is applied:</p>

<p>Firstly, on the <strong>Datacenter</strong> zone, Aliases were created for the private subnet IP ranges:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/888a88f4-2792-4810-8073-50c6801cc75d" /></p>

<p>Two <code>Security Groups</code> were then created, which contain groups of firewall rules, which can be enforced on any zone afterwards, for easier application to multiple zones.</p>

<p>The first <code>Security Group</code> is the following, and contains rules to allow access to the management IP of OPNsense (the creation of which is decribed on a later section), and to reject traffic to the local subnet of the home network. This is to isolate the infrastructure from being able to reach out to the local subnet directly. Access to the internet is still allowed. Rule No. 0 was initially created to allow access to the Proxmox management IP, but it was later found that Proxmox includes <a href="https://pve.proxmox.com/pve-docs/pve-admin-guide.html#pve_firewall_default_rules">Default firewall rules</a> that already permit this traffic, so the rule is now disabled.</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/c20b282f-9d3d-4619-a04e-6b94ac85624d" /></p>

<p>The second <code>Security Group</code> is the following, and contains a rule to allow traffic between the local subnets of the infrastructure. This traffic will be controlled further by the OPNsense.</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/02d58251-9355-420a-b321-d4c5b6c0dda2" /></p>

<p>After creating the two Security Groups, they were enforced on every zone:</p>

<p>On the <strong>Proxmox</strong> host only the first one is needed, because traffic towards the infrastructure local subnets does not enter or leave the host directly:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/7d5ad664-c918-4ba1-b93f-d9f9111dc5a4" /></p>

<p>On the <strong>OPNsense</strong>, the configuration of which will be shown in a later section, both rules were added.</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/40c41f29-6796-42b0-acf5-d12e8bf56e3f" /></p>

<h3 id="manual-opnsense-installation">Manual OPNsense Installation</h3>

<p>Before moving to full IaC mode, as a PoC, OPNsense was installed manually on Proxmox.</p>

<p>The first step is to download the ISO from the <a href="https://opnsense.org/download/">download page</a>. Afterwards, the ISO is needed to be uploaded to Proxmox:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/67ffe8d6-accb-4e93-b06d-3195513579db" /></p>

<h4 id="vm-creation">VM Creation</h4>

<p>When creating a VM in Proxmox, there are many options to select related to the OS, the system architecture, Disks architecture, etc. The configuration applied to OPNsense VM is shown in the following screenshots. Note that most selection were the default ones, because there is no significant difference to matter in the context of the project.</p>

<p>General Settings</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/cd0e382e-4f94-4795-9ca2-f3aa827ef87e" /></p>

<p>OS Settings: This is where the OPNsense ISO is selected, and also the guest OS type.</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/0b64421a-13e1-4701-9c52-d437e631da81" /></p>

<p>System Settings</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/2e577ee7-eac2-4616-8adf-2597fe1f3755" /></p>

<p>Disks Settings</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/aaa288a8-5220-4fff-a0bf-0f2faced53df" /></p>

<p>CPU Settings</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/ebd5a435-f05c-41af-a84b-478085422e48" /></p>

<p>Memory Settings: A useful setting which will be experimented with when deploying more VMs is the “<strong>Ballooning Device</strong>” setting along with the Minimum memory. <a href="https://pve.proxmox.com/wiki/Dynamic_Memory_Management#Ballooning">Memory ballooning</a> allows you to have your guest <strong>dynamically change its memory usage</strong> by evicting unused memory during run time. It reduces the impact your guest can have on memory usage of your host by giving up unused memory back to the host. The Proxmox VE host can loan ballooned memory to a busy VM. The VM decides which processes or cache pages to swap out to free up memory for the balloon. The VM (Windows or Linux) knows best which memory regions it can give up without impacting performance of the VM. The Minimum memory setting defines the minimum memory in MiB which will never be freed up for use by other VMs. For now the Minimum memory is set to the actual memory size, so essentially there is no memory ballooning for the VM.</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/fc30cda5-8ba9-4a3c-ba16-42e678646265" /></p>

<p>Network Settings: As mentioned before, the WAN interface of the OPNsense will be connected to the same Linux Bridge as the Proxmox host and the home router. After creating the VM, more network interfaces will be created.</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/3fb8f750-6eeb-42c6-9655-2531f4a7a7e7" /></p>

<h4 id="vm-installation">VM Installation</h4>

<p>Installation with ZFS. After reading about ZFS vs UFS, it seems the ZFS handles unexpected power loss better while UFS may lead to data corruption, but it has a slightly higher RAM usage.</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/651a47cf-bf7e-42b6-9267-8418c56907e2" /></p>

<p>ZFS Configuration: We have only 1 disk for now, so stripe it is:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/cf67261c-b823-48ee-b0fb-61770a7ebd58" /></p>

<p>Select the virtual disk created in the VM creation steps:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/dc3616c7-72c1-40b9-9229-20cc09be9df7" /></p>

<p>Changed the root password and completed the install:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/c178efdf-1d0f-4d36-bd2d-3d7cbf32457d" /></p>

<p>After installation, through the CLI, the interface and IP address assignments were completed:</p>

<p>Select option 1</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/0ea1c108-6d9a-4595-9ba7-724fa4e55197" /></p>

<p>The interface is recommended to be assigned automatically as it is the only interface at the moment.</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/a1423bc1-4028-44bd-880c-50ee19c3401a" /></p>

<p>After the interface assignment is completed, the IP address assignment is next (select option 2).</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/c9cca315-f284-453e-90ca-a499af903de0" /></p>

<p><img alt="image" src="https://github.com/user-attachments/assets/e80f61a4-681b-4327-adc5-a595977ce7d1" /></p>

<p>In order for Proxmox to be able to exchange information between the host and guest, and to execute commands in the guest, the <a href="https://pve.proxmox.com/wiki/Qemu-guest-agent">Qemu guest agent</a> needs to be installed.</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/77b766f5-c5ee-43e5-94cc-097b6ed2cc33" /></p>

<p>Then the service needs to be enabled:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/2e205c47-d277-4ae7-8e52-22959f646553" /></p>

<h4 id="opnsense-network-interfaces">OPNsense network interfaces</h4>

<p>As shown in the network diagram, 3 more network interfaces were created on OPNsense, and connected to the corresponding Linux Bridges:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/79b3738f-da4e-4b51-a154-1e853a8a411c" /></p>

<p>Then, each interface was assigned and enabled in OPNsense as well:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/2ad5f191-43f1-45a3-b8b3-4f0e73848451" /></p>

<p>Each interface was assigned its corresponding IP address:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/f9dca3e4-66eb-4122-9761-50a87c39dd5c" /></p>

<h4 id="opnsense-firewall-rules">OPNsense Firewall rules</h4>

<p>For firewall rules, since this is just a PoC, only 2 rules were created:</p>

<p>A rule to allow access to the Web GUI at the management IP of OPNsense.</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/2d9cf8b0-4864-48d2-97ca-decd084f5cae" /></p>

<p>A rule to allow machine in the End User Zone to access anything.</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/c16b0eb9-058e-43d4-bae4-6129eb3c430f" /></p>

<h4 id="opnsense-dhcp">OPNsense DHCP</h4>

<p>Additionally, DHCP was configured for the End User Zone:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/012f58c0-194b-4aba-bf85-b9b7acdd88ff" /></p>

<h3 id="packer-configuration">Packer Configuration</h3>

<p>In order for Packer to work, the following was implemented:</p>

<h4 id="prepare-proxmox-for-packer">Prepare Proxmox for Packer</h4>

<p><strong>Create “Packer” Role and User on Proxmox:</strong></p>

<p>For the Role:</p>

<ol>
  <li>Go to Datacenter -&gt; Permissions -&gt; Roles.</li>
  <li>Click <code>Create</code>. Name it <code>PackerRole</code>.</li>
  <li>Add these privileges: VM.Allocate, VM.Config.HWType, VM.Config.CPU, VM.Config.Memory, VM.Config.Network, VM.Config.Disk, VM.Monitor, VM.Audit, VM.PowerMgmt, Datastore.AllocateSpace, Datastore.Audit, VM.Config.Options, SDN.Use VM.Backup+Clone+Console, VM.Config.CDROM, VM.Config.CloudInit, and VM.GuestAgent.Audit</li>
</ol>

<p><img alt="image" src="https://github.com/user-attachments/assets/7353549f-d79e-4013-84b8-b7483de64cb5" /></p>

<p>For the User:</p>

<ol>
  <li>Go to Datacenter -&gt; Permissions -&gt; Users.</li>
  <li>Click <code>Add</code>. User name: <code>packer</code>, Realm: pve (more info at <a href="https://pve.proxmox.com/wiki/User_Management">Proxmox VE authentication</a> - Authentication Realms).</li>
</ol>

<p><img alt="image" src="https://github.com/user-attachments/assets/aad65f22-b8fa-4c5d-ab13-f9eeaafc5241" /></p>

<p>Assign permissions to the User:</p>

<ol>
  <li>Go to Datacenter -&gt; Permissions.</li>
  <li>Click <code>Add</code> -&gt; User Permission.</li>
  <li>Path: / (This gives permission for the whole datacenter).</li>
  <li>User: packer@pve.</li>
  <li>Role: PackerRole.</li>
</ol>

<p><img alt="image" src="https://github.com/user-attachments/assets/54ed22d9-b447-4fc4-81f6-e0774f71a938" /></p>

<p><strong>Generate API Token</strong></p>

<p>This is the “Password” Packer will use to talk to the Proxmox API.</p>
<ol>
  <li>Go to Datacenter -&gt; Permissions -&gt; API Tokens.</li>
  <li>Click <code>Add</code>.</li>
  <li>Select User: packer@pve.</li>
  <li>Token ID: packer-token.</li>
  <li>Uncheck “Privilege Separation” (this ensures the token has the same rights as the user <code>packer</code>).</li>
  <li>Click <code>Add</code>.
CRITICAL: Proxmox will show you the Token Secret <strong>only</strong> at the time of its creation. If it is missed at that time, a new token must be created.</li>
</ol>

<p><img alt="image" src="https://github.com/user-attachments/assets/8058d9e5-93be-4788-aa32-8612f96b8f12" /></p>

<h4 id="preparing-packer">Preparing packer</h4>

<p>To install Packer, the <a href="https://developer.hashicorp.com/packer/tutorials/docker-get-started/get-started-install-cli">official method</a> resulted in some errors, and the method describe in this <a href="https://stackoverflow.com/questions/75254685/gpg-error-https-apt-releases-hashicorp-com-bionic-inrelease-the-following-si">stackoverflow post</a> worked properly, which includes running the following commands and will be used to install Terraform, too.</p>

<pre><code class="language-bash"># Source - https://stackoverflow.com/a
# Posted by Thilina Ashen Gamage
# Retrieved 2026-01-28, License - CC BY-SA 4.0

# GPG is required for the package signing key
sudo apt install gpg

# Download the signing key to a new keyring
wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg

# Verify the key's fingerprint
gpg --no-default-keyring --keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg --fingerprint

# The fingerprint must match 798A EC65 4E5C 1542 8C8E 42EE AA16 FCBC A621 E701, which can also be verified at https://www.hashicorp.com/security under "Linux Package Checksum Verification".

# Add the HashiCorp repo
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list

# apt update successfully
sudo apt update

sudo apt install packer
sudo apt install terraform
</code></pre>

<p>However, because I am currently running bash via Windows Subsystem for Linux (WSL), it is difficult to grab the IP address of the physical PC, which makes packer fail when opening the temporary web server to give the <code>user-data</code> file. Therefore, for this iteration we are running Packer on Windows, and for the next phase we will probably add the <code>user-data</code> file to Proxmox directly for more seamless implementation.</p>

<p>To install Packer for windows, you just download it from <a href="https://developer.hashicorp.com/packer/install">here</a>, unzip it, and add the path of the exe to the Path environment variable of the PC:</p>

<p><img width="630" height="720" alt="image" src="https://github.com/user-attachments/assets/38936949-0060-46ef-bbf5-1b4eea0b5021" /></p>

<p><img alt="image" src="https://github.com/user-attachments/assets/016dc5ee-a194-465e-b68e-22f942dc74f3" /></p>

<p>To organize the Packer directory, a <code>packer</code> path was created, under which all the other files and directories were created.</p>

<p>For the PoC, 2 Ubuntu VMs will be created. One will be under the <code>End user Zone</code> and the other under the <code>WAN Zone</code>. Because Packer creates ready-to-provision images, an image is to be created for every OS planned to be provisioned with Terraform. Therefore, under the <code>packer</code> path, a sub-path was created named <code>ubuntu-2404</code> for this purpose.</p>

<p>All the created files are found under the project’s repository <a href="https://github.com/KostasKoutrou/kostas-seclab">kostas-seclab</a></p>

<p><strong>Create Credentials</strong></p>

<p>The following <code>packer/credentials.pkrvars.hcl</code> file was created, which contains credentials to be used in Packer, and later on in Terraform as well.</p>

<p><img width="565" height="105" alt="image" src="https://github.com/user-attachments/assets/74f29d00-3b08-483e-b739-e6d59740de6d" /></p>

<p><strong>Create <code>user-data</code> file</strong></p>

<p>A YAML file <code>packer/ubuntu-2404/http/user-data</code> was created. When running Packer, the PC becomes a temporary web server, and the machine pulls the <code>user-data</code> file from the PC from the <code>http</code> path.</p>

<p>To better understand how the <code>user-data</code> and the next files are involved during the image building using Packer, this is a great point where the tool called <a href="https://cloudinit.readthedocs.io/en/latest/"><strong>Cloud-init</strong></a> should be briefly described. <strong>Cloud-init is the industry standard multi-distribution method for cross-platform cloud instance initialization</strong>. It is supported across all major public cloud providers, provisioning systems for private cloud infrastructure, and bare-metal installations.</p>

<p>When a VM boots, cloud-init runs and provides the necessary glue between launching a cloud instance and connecting to it so that it works as expected. It looks for metadata provided by proxmox or terraform and performs different initialization tasks automatically, including:</p>

<ul>
  <li>Network: Sets static IP or DHCP config</li>
  <li>Identity: changes the hostname from ubuntu-template to what is defined</li>
  <li>Security: injects public SSH keys to log in without password</li>
  <li>Growth: expands the disk partition to fill the size you gave it in proxmox.</li>
</ul>

<p>Cloud-init grabs the <code>ubuntu.pkr.hcl</code> which is described next for instructions for proxmox and the <code>user-data</code> file for instructions for the guest (the OS).</p>

<p>But first, the <code>user-data</code> file is shown below:</p>

<pre><code class="language-yaml">#cloud-config
autoinstall:
  version: 1
  identity:
    hostname: ubuntu-template
    username: lab-admin
    password: "$6$wFmQrqy8bMHGTQ.O$1WWGjLd3buuOov83OY7zJbdw5Z9Gx4C3ueH04GZGHzqz6h7Jy0TelUUOisEt/1GJwQifYKYKVfj17vkd0mk0f0"
  user-data:
    disable_root: false
  locale: en_US.UTF-8
  timezone: UTC
  keyboard:
    layout: us
  ssh:
    install-server: true #install openssh
    allow-pw: true #allow password authentication in ssh
  packages:
    - qemu-guest-agent #qemu guest agent so that proxmox retrieves info like IP address.
    - cloud-init #needed so the VM is configurable via Terraform.
  storage:
    layout:
      name: direct #use the entire virtual disk as one big partition.
</code></pre>

<p>In the <code>user-data</code> file, the <code>autoinstall</code> directive is the set of instructions for the Ubuntu Subiquity installer. It provides all the information required during the initial installation of the ubuntu server, including:</p>

<ul>
  <li>The hostname</li>
  <li>The admin user</li>
  <li>Local</li>
  <li>Timezone</li>
  <li>Keyboard layout</li>
  <li>SSH settings</li>
  <li>Packages to be installed</li>
  <li>Storage settings</li>
</ul>

<p>Let’s look now at the <code>ubuntu.pkr.hcl</code> file:</p>

<pre><code class="language-terraform">packer {
  required_plugins {
    name = {
      version = "~&gt; 1"
      source  = "github.com/hashicorp/proxmox"
    }
  }
}

# Declare variables, we will pull them later in the packer build command
variable "proxmox_api_url" { type = string }
variable "proxmox_api_token_id" { type = string }
variable "proxmox_api_token_secret" {
  type      = string
  sensitive = true
}
variable "ubuntu_pw" {
  type = string
  sensitive = true
}

source "proxmox-iso" "ubuntu-server" { #Resource type and local name
  proxmox_url = var.proxmox_api_url
  username    = var.proxmox_api_token_id
  token       = var.proxmox_api_token_secret

  # Skip TLS Verification for self-signed certificates
  insecure_skip_tls_verify = true
  # qemu_agent = true # Default is true anyway

  node    = "kkproxmox"
  vm_id   = 1000
  vm_name = "ubuntu-2404-template"

  # iso_file = "local:iso/ubuntu-24.04.3-live-server-amd64.iso"

  boot_iso {
    # type = "scsi"
    type = "ide"
    iso_file = "local:iso/ubuntu-24.04.3-live-server-amd64.iso"
    iso_checksum = "sha256:c3514bf0056180d09376462a7a1b4f213c1d6e8ea67fae5c25099c6fd3d8274b"
    unmount = true
  }

  cores = 4
  memory = 4096

  network_adapters {
    model  = "virtio"
    bridge = "vmbr0" # Will probably change it in the Terraform script, this is only for packer.
  }

  disks {
    disk_size    = "20G"
    storage_pool = "local-lvm"
    type         = "scsi"
    ssd          = true
  }

  cloud_init = true # add an empty Cloud-Init CDROM driver after the VM has been converted to a template.
  cloud_init_storage_pool = "local-lvm" # Name of the Proxmox storage pool to store the Cloud-Init CDROM on.

  boot_command = [
    "&lt;esc&gt;&lt;wait&gt;", "e&lt;wait&gt;",
    "&lt;down&gt;&lt;down&gt;&lt;down&gt;&lt;end&gt;",
    " autoinstall cloud-config-url=http://{{ .HTTPIP }}:{{ .HTTPPort }}/user-data ds='nocloud-net;s=http://{{.HTTPIP}}:{{.HTTPPort}}/'",
    "&lt;f10&gt;"
  ]

  http_directory = "http"
  ssh_username   = "lab-admin"
  ssh_password = "${var.ubuntu_pw}"
  ssh_timeout    = "20m"
}

build {
  sources = ["source.proxmox-iso.ubuntu-server"]

  provisioner "shell" {
    # execute_command = "echo ${var.ubuntu_pw}| sudo -S sh -c '{{ .Vars }} {{ .Path }}'"
    execute_command = "echo ${var.ubuntu_pw}| {{.Vars}} sudo -S -E sh -eux '{{.Path}}'"
    inline = [
      "echo 'Waiting for cloud-init to complete...'",
      "while [ ! -f /var/lib/cloud/instance/boot-finished ]; do echo 'Still waiting...'; sleep 2; done",
      "echo 'Cloud-init completed successfully'",
      "echo 'Cleaning up...'",
      "rm -rf /var/lib/apt/lists/*",
      "rm -rf /tmp/*",
      "rm -rf /var/tmp/*",
      "cloud-init clean --logs --machine-id --seed --configs all"
    ]
  }
}
</code></pre>

<p>This file is called the <code>Packer Template</code>, and include all the information for how to build the template. It includes:</p>

<ul>
  <li>The <strong>required packer plugins</strong> for this template. In this case, the Proxmox plugin is required only, to define the rest of the information and know how to communicate with the Proxmox API.</li>
  <li><strong>Variable declaration to be used in the packer build</strong> in the next lines of the file. Note here that at this point only the declaration of the variables is done. The actual value of the variables will be provided via the <code>credentials.pkrvars.hcl</code> file during the packer build execution.</li>
  <li>The <code>source</code> block is the core logic. It defines the different values that the VM should have, like CPU cores, memory size, ssh settings, the ISO file to use, the VM name, where to install the VM in proxmox, etc. The values that are not explicitly defined here have default values set provided by the proxmox plugin.
    <ul>
      <li><code>boot_command</code>: A very valuable part of the <code>source</code> block is the <code>boot_command</code>. In it, the actual keyboard presses for the installation are defined. With these boot commands, what is done is that the installation file is edited to pull the installation parameters from the <code>user-data</code> file described above. More specifically, it contains Linux Kernel Boot Parameters before the installation starts:
        <ul>
          <li><code>autoinstall</code>: This tells the Ubuntu installer (Subiquity) to run in automated mode, instead of going through the manual installation process of selecting language etc.</li>
          <li>The variables {{ .HTTPIP }} and {{ .HTTPPort }}: these are the IP of the PC and the port that temporarily serves the <code>user-data</code> file.</li>
          <li><code>cloud-config-url</code>: This parameter tells the installer where the file of <code>user-data</code> is.</li>
          <li><code>ds</code>: This stands for “Data Source” and Cloud-init uses its contents:
            <ul>
              <li><code>nocloud-net</code>: This tells Cloud-init that the data source is not Public cloud (Azure/AWS/GCP etc.) and to look at the local network for the file.</li>
              <li><code>s=</code>: This stand for “seed from”. It tells Cloud-init where to find the <code>user-data</code> and <code>meta-data</code> (another file not used for this PoC) files.</li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>The <code>build</code> block is the execution part of packer. The <code>source</code> block defines the VM, while the <code>build</code> block actually runs it. Here is where the <a href="https://developer.hashicorp.com/packer/docs/provisioners"><strong>Provisioners</strong></a> live, through which it is possible to execute actions on the machine image and configure it after booting. They can be used to install packages, patch the kernel, create users, download application code, etc. In the PoC, the <code>shell</code> provisioner is used to clean up the machine and reinitialize cloud-init so that it reruns during the Terraform provisioning of the VM (cloud-init runs only once on the machine unless its configuration is cleaned).</li>
</ul>

<h4 id="packer-pitfalls-solutions-and-lessons-learnt">Packer pitfalls, solutions, and lessons learnt</h4>

<p>Some tools that helped in troubleshooting Packer:</p>
<ol>
  <li>Set the envar <code>$env:PACKER_LOG=1</code> to make running packer verbose. This shows many hints as to what is going wrong.</li>
  <li>If the VM does not proceed with the autoinstaller, and reaches the manual installation flow, you can use <code>Ctrl + Alt + F2</code> in the Proxmox console to <strong>jump to a root shell</strong>. This allows to</li>
  <li><strong>review logs of the subiquity and cloud-init</strong>. Two useful paths for checking are <code>/var/log/installer/subiquity-server-debug.log</code> and <code>/var/log/syslog</code>.</li>
  <li><strong>test network connections</strong>, to confirm, for example, that you can reach the temporary packer server on the PC running packer, or that the internet is reachable.</li>
  <li><strong>Check the hardware resource usage of the VM</strong> being installed in Proxmox. Maybe there is overuse of the resource and may need to be increased.</li>
</ol>

<p>Some <strong>pitfalls and lessons</strong> learnt I met while setting up packer were the following:</p>

<ol>
  <li><strong>Proxmox 500 Error</strong>: After enabling verbose mode in packer (by setting <code>$env:PACKER_LOG=1</code>), a repeated message of 500 “Qemu must be running to read the IP address of the machine” kept popping up. I thought this meant that something was wrong, but actually this is expected and Packer is actually waiting for Qemu guest agent to be installed so that it can read the VM’s IP Address from proxmox so it can SSH to it. If it never moves that point, then the QEMU guest agent was not installed, or, most probably, the autoinstaller did not start.</li>
  <li><strong>The “Interactive Menu” problem</strong>:
    <ul>
      <li><strong>The issue</strong>: The VM kept reaching the manual language selection screen instead of starting the automated install.</li>
      <li><strong>The cause</strong>: The installer could not find or parse the autoinstall instructions.</li>
      <li><strong>The resolution</strong>: In the <code>boot_command</code>, an explicit definition of the autoinstall instructions needed to be defined using the <code>cloud-config-url</code> parameter.</li>
    </ul>
  </li>
  <li><strong>YAML formatting</strong>
    <ul>
      <li><strong>The issue</strong>: The installer found the user-data file but ignored the settings, falling back to manual installation.</li>
      <li><strong>The cause</strong>: Ubuntu ignores the user-data file if it does not have the “#cloud-config” comment at the first line.</li>
      <li><strong>The lesson</strong>: read the documentation of the requirements of different configuration files.</li>
    </ul>
  </li>
  <li><strong>Validation crashes</strong>:
    <ul>
      <li><strong>The issue</strong>: The installer crashed with an “Unknown keyboard layout ‘en’” error (1st screenshot), and crashed while attempting to run timedatectl list-timezones (2nd screenshot).</li>
      <li><strong>The cause and resolution</strong>: Some parameters in the <code>user-data</code> file are mandatory and do not have default values when not explicitly defined. These include the timezone and keyboard layout parameters, which were not initally defined. Once added to the <code>user-data</code> file, the crashes were fixed.
  Keyboard layout error:
  <img alt="image" src="https://github.com/user-attachments/assets/e1db920c-62d7-4366-95da-2dea2d3b3689" />
  Timezone error: 
  <img alt="image" src="https://github.com/user-attachments/assets/f58cd53a-93ec-4de2-8d53-074a1fc7733b" /></li>
    </ul>
  </li>
  <li><strong>Hardware resource constraints</strong>
    <ul>
      <li><strong>The issue</strong>: The installation appeared to hang or move extremely slowly without showing errors.</li>
      <li><strong>The cause</strong>: Initially the default memory and CPU values were left in the packer template file, which means 1 CPU core and 512MB memory. Especially for the memory size, this is a very low value for an installation of Ubuntu server.</li>
      <li><strong>The solution and lesson</strong>: Modern installers require more thant the default minimal resources to complete. Increase the values for 4 CPU cores and 4096MB memory resolved the issue.</li>
    </ul>
  </li>
  <li><strong>SSH Handshake and authentication</strong>
    <ul>
      <li>The issue: The OS installed successfully, but Packer failed to connect with the following repeating error:
        <pre><code class="language-plaintext">2026/01/26 10:59:47 packer-plugin-proxmox_v1.2.3_x5.0_windows_amd64.exe plugin: 2026/01/26 10:59:47 [INFO] Attempting SSH connection to 192.168.0.110:22...
2026/01/26 10:59:47 packer-plugin-proxmox_v1.2.3_x5.0_windows_amd64.exe plugin: 2026/01/26 10:59:47 [DEBUG] reconnecting to 
TCP connection for SSH
2026/01/26 10:59:47 packer-plugin-proxmox_v1.2.3_x5.0_windows_amd64.exe plugin: 2026/01/26 10:59:47 [DEBUG] handshaking with SSH
2026/01/26 10:59:47 packer-plugin-proxmox_v1.2.3_x5.0_windows_amd64.exe plugin: 2026/01/26 10:59:47 [DEBUG] SSH handshake err: ssh: handshake failed: ssh: unable to authenticate, attempted methods [none], no supported methods remain
2026/01/26 10:59:47 packer-plugin-proxmox_v1.2.3_x5.0_windows_amd64.exe plugin: 2026/01/26 10:59:47 [DEBUG] Detected authentication error. Increasing handshake attempts.
</code></pre>
      </li>
      <li><strong>The cause and resolution</strong>: the <code>ssh_password</code> value in the packer template is used for this connection, and was not initially defined. Upon its definition, the issue was resolved.</li>
    </ul>
  </li>
  <li><strong>sudo asking password on shell provisioner in packer</strong>
    <ul>
      <li><strong>The issue</strong>: When running the “shell” provisioner in Packer build, it asked for a manual input of the user password to execute the sudo commands.</li>
      <li><strong>The cause</strong>: the “shell” provisioner has a variable called <code>execute_command</code>. What shell provisioner does essential is that it converts all the commands provided to a script file, and then executes that script with a predetermined <code>execute_command</code>. This can be edited to satisfy any need.</li>
      <li><strong>The solution</strong>: The <code>execute_command</code> value was changed to <code>"echo ${var.ubuntu_pw}| {{.Vars}} sudo -S -E sh -eux '{{.Path}}'"</code>, which pulls the password and inputs it for when sudo asks for it.</li>
    </ul>
  </li>
  <li><strong>Cloud-init status –wait not working in shell provisioner</strong>
    <ul>
      <li><strong>The issue</strong>: In the shell provisioner, when trying to clean up the cloud-init state, at first it was tried to run the “cloud-init status –wait” command, to wait for cloud-init to complete before reinitializing. This command would return status code 2 which means that the cloud-init process has not finished, while shell provisioner only accepts status code 0 as a “non-error” status code, so Packer would crash and mention this as an error.</li>
      <li><strong>The Solution</strong>: The /var/lib/cloud/instance/boot-finished file is now being checked instead to verify that cloud-init finished running.</li>
    </ul>
  </li>
</ol>

<h4 id="packer-build-and-poc-result">Packer build and PoC result</h4>

<p>To run packer, the following commands are used, executed at the path of <code>packer/ubuntu-2404/</code>:</p>

<pre><code class="language-powershell">packer init .
packer validate -var-file="../credentials.pkrvars.hcl" . # To Validate the packer configuration before building
packer build -var-file="../credentials.pkrvars.hcl" . # To Build the template
</code></pre>

<p>Running the packer validate command results in the following:</p>

<p><img width="697" height="55" alt="image" src="https://github.com/user-attachments/assets/209c4b78-27e1-49e1-b411-b01bb6e044e8" /></p>

<p>Running the packer build command results in the following:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/acf0f158-9ece-4ab3-b59d-a71c2c222fcd" /></p>

<p>While the VM is being installed, in the Console window it is possible to review what is being run:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/3d421b7d-5c40-4d06-8eb4-f5d63302e1c9" /></p>

<p>After successfully running Packer, the result is a Proxmox template, ready to be cloned. This template is used by Terraform, as shown in the next section.</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/862f0292-2481-4aa2-8862-6ba033da0d88" /></p>

<h3 id="terraform-configuration">Terraform Configuration</h3>

<p>In order for Terraform to work, the following was implemented:</p>

<h4 id="prepare-proxmox-for-terraform">Prepare Proxmox for Terraform</h4>

<p><strong>Update “Packer” Role in Proxmox for Terraform</strong></p>

<p>For simplicity, the same user used in Packer is used for Terraform. Therefore, the permissions required for Terraform were provided to the previously created <code>PackerRole</code> role. The permissions required are described <a href="https://registry.terraform.io/providers/Telmate/proxmox/latest/docs">here</a>, and are the following:</p>

<ul>
  <li>Datastore.AllocateSpace</li>
  <li>Datastore.AllocateTemplate</li>
  <li>Datastore.Audit</li>
  <li>Pool.Allocate</li>
  <li>Pool.Audit</li>
  <li>Sys.Audit</li>
  <li>Sys.Console</li>
  <li>Sys.Modify</li>
  <li>VM.Allocate</li>
  <li>VM.Audit</li>
  <li>VM.Clone</li>
  <li>VM.Config.CDROM</li>
  <li>VM.Config.Cloudinit</li>
  <li>VM.Config.CPU</li>
  <li>VM.Config.Disk</li>
  <li>VM.Config.HWType</li>
  <li>VM.Config.Memory</li>
  <li>VM.Config.Network</li>
  <li>VM.Config.Options</li>
  <li>VM.Migrate</li>
  <li>VM.PowerMgmt</li>
  <li>SDN.Use</li>
</ul>

<h4 id="preparing-terraform">Preparing Terraform</h4>

<p>To organize the Terraform directory, a <code>terraform</code> path was created, under which all the other files and directories were created.</p>

<p><strong>For the PoC, 2 Ubuntu VMs will be created</strong>. One will be under the <code>End user Zone</code> and the other under the <code>WAN Zone</code>.</p>

<p>All the created files are found under the project’s repository <a href="https://github.com/KostasKoutrou/kostas-seclab">kostas-seclab</a></p>

<p>For terraform, the following files were created:</p>

<p>The <code>terraform/variables.tf</code> file contains <strong>variables to be used by terraform</strong>:</p>

<pre><code class="language-terraform">variable "proxmox_api_url" {
    type = string
    description = "The URL for the Proxmox API (e.g., https://192.168.0.50:8006/api2/json)"
}

variable "proxmox_api_token_id" {
    type = string
    sensitive = true
    description = "The API Token ID (e.g., root@pam!terraform)"
}

variable "proxmox_api_token_secret" {
    type = string
    sensitive = true
    description = "The API Token Secret (UUID)"
}
</code></pre>

<p>The <code>terraform/provider.tf</code> file contains information regarding <strong>what provider terraform will connect to</strong> (AWS, Azure Google Cloud, Proxmox, etc. - In our case it is proxmox, using the <a href="https://registry.terraform.io/providers/Telmate/proxmox/latest/docs">Telmate/proxmox</a> provider), as well as <strong>the configuration to connect to the provider</strong>. The variables for the provider are pulled from the <code>terraform/variables.tf</code> definitions. The execution of <code>terraform apply</code> is where the source and the actual values of the varaibles will be defined.</p>

<pre><code class="language-terraform">terraform {
  required_providers {
    proxmox = {
        source = "Telmate/proxmox"
        version = "3.0.2-rc07"
    }
  }
}

provider "proxmox" {
    pm_api_url = var.proxmox_api_url
    pm_api_token_id = var.proxmox_api_token_id
    pm_api_token_secret = var.proxmox_api_token_secret

    pm_tls_insecure = true
}
</code></pre>

<p>The <code>terraform/main.tf</code> file contains <strong>the blocks of what exactly to build</strong>. In our case of building 2 test Ubuntu servers using the template built by Packer, the main.tf looks like this:</p>

<pre><code class="language-terraform">resource "proxmox_vm_qemu" "test_server" { # Resource type and resource name
    name = "terraform-vm-01" # Name of the VM
    target_node = "kkproxmox" # Proxmox node to build the VM on

    clone = "ubuntu-2404-template" # Clone the ubuntu template built by Packer

    agent = 1 #enable QEMU guest agent
    
    cpu {
        cores = 2
        sockets = 1
        type = "host"
    }
    memory = 2048
    scsihw = "virtio-scsi-pci"
    # bootdisk = "scsi0"

    # This is added to the default ciuser "ubuntu". "ssh-keygen -t rsa" was used to generate the key pair. This is needed for Ansible to SSH with.
    sshkeys = "${file("~/.ssh/id_rsa.pub")}"

    disk { # The cloudinit disk needs to be defined explicitly for it to be mounted temporarily for cloud-init to run.
        slot    = "ide0"
        type    = "cloudinit"
        storage = "local-lvm"
    }

    disk {
        slot = "scsi0"
        size = "20G"
        type = "disk"
        storage = "local-lvm"
        format = "raw"
    }

    startup_shutdown { # These were defined because these are the values that Proxmox puts on the VMs by default while Terraform tried to put different values, so every time that "terraform apply" would run, it would try to change them, for Proxmox to change them back right after.
        order = -1
        shutdown_timeout = -1
        startup_delay = -1
    }

    network { # This VM connects to the WAN Zone Linux Bridge
        id = 0
        model = "virtio"
        bridge = "vmbr0"
    }

    os_type = "cloud-init" # This is used together with the "cloudinit" disk to apply the cloud-init related configuration (the following commands).
    ipconfig0 = "ip=dhcp" # get IP from the DHCP
    skip_ipv6 = true # do not get IPv6
}

resource "proxmox_vm_qemu" "test_server1" { # the 2nd VM
    name = "terraform-vm-02" # Name of the VM
    target_node = "kkproxmox" # Proxmox node to build the VM on

    clone = "ubuntu-2404-template" # Clone the ubuntu template built by Packer

    agent = 1 #enable QEMU guest agent
    
    cpu {
        cores = 2
        sockets = 1
        type = "host"
    }
    memory = 2048
    scsihw = "virtio-scsi-pci"
    # bootdisk = "scsi0"

    disk { # The cloudinit disk needs to be defined explicitly for it to be mounted temporarily for cloud-init to run.
        slot    = "ide0"
        type    = "cloudinit"
        storage = "local-lvm"
    }

    disk {
        slot = "scsi0"
        size = "20G"
        type = "disk"
        storage = "local-lvm"
        format = "raw"
    }

    startup_shutdown { # These were defined because these are the values that Proxmox puts on the VMs by default while Terraform tried to put different values, so every time that "terraform apply" would run, it would try to change them, for Proxmox to change them back right after.
        order = -1
        shutdown_timeout = -1
        startup_delay = -1
    }

    network { # This VM connects to the End User Zone Linux Bridge
        id = 0
        model = "virtio"
        bridge = "vmbrEUZ40"
    }

    os_type = "cloud-init" # This is used together with the "cloudinit" disk to apply the cloud-init related configuration (the following commands).
    ipconfig0 = "ip=dhcp" # get IP from the DHCP
    skip_ipv6 = true # do not get IPv6
}
</code></pre>

<h4 id="terraform-pitfalls-solutions-and-lessons-learnt">Terraform pitfalls, solutions, and lessons learnt</h4>

<p>Some pitfalls and lessons learnt I met while setting up Terraform were the following:</p>

<ol>
  <li><strong>Stale Cloud-Init State</strong>
    <ul>
      <li><strong>The issue</strong>: After deploying a VM with Terraform, Proxmox showed the correct name (e.g., terraform-vm-01), but <strong>the internal OS console still showed the template’s original name (ubuntu-server)</strong>. The VM ignored the new configuration.</li>
      <li><strong>The Cause</strong>: <strong>cloud-init is designed to run only once</strong>. During the Packer build process, cloud-init ran to set up the initial template and left a “marker file” indicating it was finished. When the template was cloned via Terraform, the new VM saw the marker and immediately went back to sleep, ignoring the new instructions from Terraform.</li>
      <li><strong>The Fix</strong>: <strong>A “cleanup” shell provisioner was added</strong> at the end of the Packer configuration (ubuntu.pkr.hcl), which runs the command <code>cloud-init clean --logs --machine-id --seed --configs all</code> to fully reinitialized the cloud-init status.</li>
      <li><strong>The Lesson</strong>: <strong>Templates must be stateless</strong>. If the state is not reset before saving a template, every clone will wake up thinking it has already been configured.</li>
    </ul>
  </li>
  <li><strong>Duplicate Machine IDs</strong>
    <ul>
      <li><strong>The issue (Potential)</strong>: While fixing the previous issue, a risk was identified where <strong>multiple VMs might end up with the same IP address from the DHCP server</strong>.</li>
      <li><strong>The Cause</strong>: <strong>Linux generates a unique /etc/machine-id upon installation</strong>. If this isn’t reset during the Packer build, every clone shares the same ID, causing network conflicts.</li>
      <li><strong>The Fix</strong>: In the <code>cloud-init clean</code> command in Packer, <strong>the flag <code>--machine-id</code> was added to reset the machine id as well</strong>.</li>
      <li><strong>The Lesson</strong>: Simply changing a hostname isn’t enough. <strong>Unique system identifiers (GUIDs, Machine IDs) must be regenerated for every new instance</strong> to avoid collisions on the network level.</li>
    </ul>
  </li>
  <li><strong>No Cloud-Init Drive</strong>
    <ul>
      <li><strong>The issue</strong>: Terraform was successfully creating the VM resource, but <strong>the OS configuration (hostname, user data) was never applied</strong>. When manually mounting a Cloud-Init drive fixed this issue.</li>
      <li><strong>The Cause</strong>: <strong>The Packer template was missing the <code>cloud_init = true</code> instruction</strong>. Consequently, the template was created without a Cloud-Init drive. Without this drive, Terraform had no physical medium to insert the configuration data for the OS to read. Additionally, in the Terraform main.tf file, the “cloudinit” was also needed to be explicitly defined.</li>
      <li><strong>The Fix</strong>: The source “proxmox-iso” block in Packer was updated to explicitly <strong>include the <code>cloud-init</code> parameter,</strong> and Terraform to include the “cloudinit” disk.</li>
    </ul>
  </li>
</ol>

<h4 id="terraform-apply-and-poc-result">Terraform apply and PoC result</h4>

<p>To run terraform, the following commands were used:</p>

<pre><code class="language-bash">terraform init # To initialize terraform
terraform plan -var-file=../packer/credentials.pkrvars.hcl # To validate that the terraform config is ready to be applied.
terraform apply -var-file=../packer/credentials.pkrvars.hcl # To apply the Terraform config and build the defined VMs.
</code></pre>

<p>The <code>credentials.pkrvars.hcl</code> file was used as the var-file, because it contains the same info required for Terraform to run.</p>

<p>Unlike Packer, Terraform was run in WSL, because the ssh key defined in the Terraform <code>main.tf</code> file will be used by Ansible in the next step, and Ansible cannot be run in Windows, only in Linux.</p>

<p>Running <code>terraform init</code> results in the following:</p>

<p><img width="565" height="275" alt="image" src="https://github.com/user-attachments/assets/a0dcbe99-67c6-485c-b3db-5b8faaf158d8" /></p>

<p>Running <code>terraform plan -var-file="../packer/credentials.pkrvars.hcl"</code> outputs many lines which show the configuration to be applied. Below is a screenshot of part of the output:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/7a5b9108-aeb8-46e5-b9ca-1a66b3a9fee5" /></p>

<p>Running the <code>terraform apply -var-file="../packer/credentials.pkrvars.hcl"</code> shows the same information as the <code>terraform plan</code>, which is the configuration to be applied, and asks for confirmation in the end. After typing “yes”, the VMs are being provisioned, and the final output is the following:</p>

<p><img width="593" height="523" alt="image" src="https://github.com/user-attachments/assets/f34fb350-5e3e-4e79-ad68-2372d76f4987" /></p>

<p>After 2 minutes, in Proxmox we can see the provisioned VMs up and runnning:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/dc1a165b-e520-4022-ab24-bec391d18158" /></p>

<p>Now that the VMs are provisioned, the next step is to apply further configurations using Ansible.</p>

<h3 id="ansible">Ansible</h3>

<p>Ansible cannot be run in Windows PowerShell, so WSL was used.</p>

<p>To install Ansible, use <code>sudo apt install ansible</code></p>

<p>Ansible uses ssh keys to SSH to machine and apply configurations. To generated the ssh key pair, run <code>ssh-keygen -t rsa</code>:</p>

<p><img width="808" height="605" alt="image" src="https://github.com/user-attachments/assets/ed8b1b3b-7b85-4118-a156-09add815c186" /></p>

<p>Then, in the Terraform main.tf file, the public key was added to one of the machines:</p>

<p><code>sshkeys = "${file("~/.ssh/id_rsa.pub")}"</code></p>

<p>For Ansible, the following files were created:</p>

<p><code>ansible/inventory.ini</code>: This file contains the list of machines with their IP addresses for Ansible to connect to them.</p>

<pre><code class="language-ini">[webservers]
192.168.0.104 ansible_user=ubuntu
</code></pre>

<p><code>ansible/install_nginx.yml</code>: This file contains the state for this PoC of the configuration to be applied to the machines of inventory.ini. For the purposes of the PoC, only an Nginx server is installed on one of the two Linux Servers.</p>

<pre><code class="language-yaml">---
- name: Configure Webservers
  hosts: all
  become: true #sudo

  tasks:
    - name: Update apt cache
      apt:
        update_cache: yes
        cache_valid_time: 3600

    - name: Install Nginx
      apt:
        name: nginx
        state: present

    - name: Start Nginx service
      service:
        name: nginx
        state: started
        enabled: yes
</code></pre>

<p><code>ansible/ansible.cfg</code>: This file override the global Ansible settings:</p>

<pre><code class="language-ini">[defaults]
inventory = ./inventory.ini
host_key_checking = False
</code></pre>

<p>The ansible.cfg file had to be set by setting the environment variable (because we are running with WSL): <code>export ANSIBLE_CONFIG=./ansible.cfg</code>. To confirm, run <code>ansible --version</code>, which should show the <code>config file</code> value:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/7ac8a653-ee6a-4c07-82fa-f6b17f0a801e" /></p>

<h4 id="running-ansible">Running Ansible</h4>

<p>To run Ansible with the configuration defined above, the following command was run: <code>ansible-playbook install_nginx.yml</code></p>

<p><img alt="image" src="https://github.com/user-attachments/assets/4a414b9c-6f01-4da1-9984-69ad5cf00a17" /></p>

<p>Due to Ansible’s idempotency, running the command again will show that everything is ok and no changes were applied:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/2fe0b7d3-f372-434c-94ec-3f3ab3f059d1" /></p>

<p>After running Ansible, the Nginx can be reached from the browser</p>

<p><img width="765" height="265" alt="image" src="https://github.com/user-attachments/assets/ae5c13d6-23e0-4c80-ae2f-b095bf0873f4" /></p>

<h2 id="next-steps">Next Steps</h2>

<p>So now the flow is complete. A VM template was created using Packer, VMs were provisioned with Terraform, and further configurations were applied using Ansible. This was a PoC to verify the flow can be used for the rest of the project.</p>

<p><strong>The next steps</strong> will be to reach a fully recoverable and fully IaC state from start up to the point which has been reached now manually.</p>

<p>This means:</p>

<ol>
  <li>Configuring all the <strong>Proxmox settings</strong> described in this post using either Ansible or any other automation tool (even a script).</li>
  <li>Configuring <strong>OPNsense</strong> using Packer, Terraform and Ansible. A Packer template will be built using OPNsense’s ISO, Terraform will be used to provision the OPNsense with the settings which were applied manually, and Ansible will be used to apply any configuration to it, including Firewall rules, DHCP, installing Packages, etc.</li>
</ol>

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

<p>Building a Cyber Range as Code is going to be a complex project to pull off fully “as Code”. It will require testing and several iterations in order for it work properly.</p>

<p>However, this project will be a great lesson towards:</p>

<ol>
  <li>Learning IaC (Packer, Terraform, Ansible).</li>
  <li>Applying security hardening and security standards to multiple machines using IaC practices.</li>
  <li>Developing methodologies for security testing and reviewing the results.</li>
</ol>

<p>This post included the plan for the project. It included different ideas for potential technologies to be used, as well as the architecture to be built. In the next posts the implementation will start.</p>

<p>Hopefully you picked something of interest and will follow along the progress of this project.</p>

<p>See you in the next one.</p>]]></content><author><name>Kostas Koutroumpouchos</name></author><category term="Other" /><summary type="html"><![CDATA[Introduction]]></summary></entry><entry><title type="html">MDE Performance Troubleshooting - Getting to know MDE Part 3</title><link href="https://kostaskoutrou.github.io/2026/01/17/performance-troubleshooting-mde.html" rel="alternate" type="text/html" title="MDE Performance Troubleshooting - Getting to know MDE Part 3" /><published>2026-01-17T00:00:00+00:00</published><updated>2026-01-17T00:00:00+00:00</updated><id>https://kostaskoutrou.github.io/2026/01/17/performance-troubleshooting-mde</id><content type="html" xml:base="https://kostaskoutrou.github.io/2026/01/17/performance-troubleshooting-mde.html"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>A frequently reported issue in MDE is its performance impact on machines. Many engineers have seen the task manager of a machine looking like the following, and are asked to fix it.</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/4e3f30e4-2b9c-4731-ab5b-989b04eb2b49" /></p>

<p>Welcome to the 3rd part of the blog post series “Getting to know MDE”!</p>

<p>In this series, each post contains a different component/feature/methodology when it comes to understanding and managing Microsoft Defender for Endpoint (MDE).</p>

<blockquote>
  <p>In this post, the focus is on troubleshooting issues that have a performance impact on machines caused by Defender processes.</p>
</blockquote>

<p>All the information is based on official documentation of Microsoft, which you can find <a href="https://learn.microsoft.com/en-us/defender-endpoint/">here</a>.</p>

<p>So let’s get started.</p>

<hr />

<h2 id="contents">Contents</h2>

<ul id="markdown-toc">
  <li><a href="#introduction" id="markdown-toc-introduction">Introduction</a></li>
  <li><a href="#contents" id="markdown-toc-contents">Contents</a></li>
  <li><a href="#common-reasons-for-higher-cpu-by-mdav" id="markdown-toc-common-reasons-for-higher-cpu-by-mdav">Common reasons for higher CPU by MDAV</a>    <ul>
      <li><a href="#binaries-not-signed" id="markdown-toc-binaries-not-signed">Binaries not signed</a></li>
      <li><a href="#obfuscated-scripts" id="markdown-toc-obfuscated-scripts">Obfuscated scripts</a></li>
      <li><a href="#not-letting-mdav-cache-finish-before-sealing-a-vdi-image" id="markdown-toc-not-letting-mdav-cache-finish-before-sealing-a-vdi-image">Not letting MDAV cache finish before sealing a VDI image</a></li>
      <li><a href="#misspelled-exclusions" id="markdown-toc-misspelled-exclusions">Misspelled exclusions</a></li>
      <li><a href="#path-exclusions-only-work-for-scanning-flows" id="markdown-toc-path-exclusions-only-work-for-scanning-flows">Path exclusions only work for scanning flows</a></li>
      <li><a href="#file-hash-computation" id="markdown-toc-file-hash-computation">File hash computation</a></li>
      <li><a href="#scheduled-scanning" id="markdown-toc-scheduled-scanning">Scheduled scanning</a></li>
      <li><a href="#scan-after-security-intelligence-updates" id="markdown-toc-scan-after-security-intelligence-updates">Scan after security intelligence updates</a></li>
      <li><a href="#conflicts-with-other-security-software" id="markdown-toc-conflicts-with-other-security-software">Conflicts with other security software</a></li>
    </ul>
  </li>
  <li><a href="#troubleshooting-mode" id="markdown-toc-troubleshooting-mode">Troubleshooting Mode</a></li>
  <li><a href="#performance-analyzer" id="markdown-toc-performance-analyzer">Performance Analyzer</a>    <ul>
      <li><a href="#run-performance-analyzer-via-live-response" id="markdown-toc-run-performance-analyzer-via-live-response">Run Performance Analyzer via Live Response</a></li>
    </ul>
  </li>
  <li><a href="#mplog-file-parsing-for-performance-impact" id="markdown-toc-mplog-file-parsing-for-performance-impact">MPLog file parsing for performance impact</a></li>
  <li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
</ul>

<hr />

<h2 id="common-reasons-for-higher-cpu-by-mdav">Common reasons for higher CPU by MDAV</h2>

<p>There may be several reasons which cause MDAV to utilize a higher percentage of CPU power. In this section, a few common reasons are briefly described.</p>

<h3 id="binaries-not-signed">Binaries not signed</h3>

<p>When a binary (exe, dll, etc.) that is not digitally signed is launched, MDAV will start a Real-Time Protection Scan.</p>

<p>Generally, properly identifying such cases is difficult. If you know a robust method, please let me know. The KQL table <code>DeviceFileCertificateInfo</code> can be used to identify certificate-related issues:</p>

<pre><code class="language-kql">DeviceFileCertificateInfo
| where TimeGenerated &gt; ago(30d)
| where IsTrusted == 0
</code></pre>

<p>The following KQL query can also be run to potentially identify cases of unsigned binaries being executed.</p>

<pre><code class="language-kql">DeviceProcessEvents
| where TimeGenerated &gt; ago(1d)
| where SHA1 != ""
| where FolderPath contains ":"
| project TimeGenerated, DeviceName, DeviceId, SHA1, FileName, FolderPath, ProcessCommandLine, InitiatingProcessFileName
| join kind=leftouter (
DeviceFileCertificateInfo
| where TimeGenerated &gt; ago(30d) //putting 30 days because these events are not generated for every execution of a binary.
| project SHA1, IsSigned, IsTrusted, Signer, Issuer, DeviceId
) on SHA1 and DeviceId
| project TimeGenerated, DeviceName, DeviceId, SHA1, FileName, FolderPath, ProcessCommandLine, IsSigned, IsTrusted, Signer, Issuer, InitiatingProcessFileName
| where isnull(IsSigned) or IsTrusted == 0 or IsSigned == 0
| summarize count() by DeviceName, DeviceId, SHA1, FileName, Signer, Issuer, FolderPath, IsSigned, IsTrusted
| sort by FileName asc, DeviceName asc
</code></pre>

<p>The results of the detected files should then be filtered to see if they are actually signed, and what Defender mentions about their signature information. This can be done by clicking on the hashes in the resulting table, and checking the signature information:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/e6b3f143-cade-4e43-aed4-d3b1449bd3d0" /></p>

<p>If the binaries have certificates signed by CAs not trusted by Microsoft or need to be excluded for any reason, they can be added to the Certificates Indicators allow list.</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/bb9da54c-ddbb-4e71-8ef4-6d311a207956" /></p>

<h3 id="obfuscated-scripts">Obfuscated scripts</h3>

<p>Obfuscated scripts require much more CPU to be scanned, so obfuscation should be used only if necessary.</p>

<h3 id="not-letting-mdav-cache-finish-before-sealing-a-vdi-image">Not letting MDAV cache finish before sealing a VDI image</h3>

<p>If creating a VDI image for a persistent or non-persistent image, ensure that the cache maintenance completes before sealing the image. To do this:</p>

<ol>
  <li>Open up the Task Scheduler mmc (taskschd.msc).</li>
  <li>Expand Task Scheduler Library &gt; Microsoft &gt; Windows &gt; Windows Defender, and then right-click on Windows Defender Cache Maintenance.</li>
  <li>Select Run, and let the scheduled task finish.</li>
</ol>

<h3 id="misspelled-exclusions">Misspelled exclusions</h3>

<p>Double check that the exclusions are spelled correctly.</p>

<h3 id="path-exclusions-only-work-for-scanning-flows">Path exclusions only work for scanning flows</h3>

<p>Behavior Monitoring and Network Real-time Inspection can still cause performance issues. As a workaround, one of the following can be done:</p>

<ul>
  <li>Add the exe or dll to the Indicators file hash allow list</li>
  <li>Add the certificate to the Indicators certificates allow list</li>
  <li>Add MDAV exclusions for the process, too.</li>
</ul>

<h3 id="file-hash-computation">File hash computation</h3>

<p>If the File Hash computation feature is enabled, file hashes for every executable file that is scanned is computed, if it wasn’t previously computed. This has a performance cost especially when copying large files from a network share. Keep in mind that this feature is a prerequisite for File Hash Indicators in Defender.</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/9628f6f6-4fcf-4d07-9b10-c1adf7119ec3" /></p>

<p>If it is decided that it is not needed, it can be disabled via PowerShell or any management solution used in the organization (Intune, AD GPO, Configuration Management, etc.). To disable it with PowerShell run the following cmdlet:</p>

<p><code>Set-MpPreference -EnableFileHashComputation $false</code></p>

<h3 id="scheduled-scanning">Scheduled scanning</h3>

<p>When it comes to scheduled scanning, the following scan settings should be checked, depending on the way they are pushed to machines:</p>

<ul>
  <li>Configure low CPU priority for scheduled scans. This lowers the scheduled scan thread priority from 9 to 8.</li>
  <li>Specify CPU usage limit per scan from 50 to 20 or 30.</li>
  <li>Start scheduled scan when the device is idle with <code>ScanOnlyIfIdle</code>.</li>
  <li>Specify the interval to run quick scans per day to <code>Not configured</code>.</li>
  <li>Specify the time for a daily quick scan to a time when the machine is least used.</li>
  <li>Consider disabling the Scheduled Scan settings. The Daily Quick Scan is enough:
    <ul>
      <li>Specify the scan type to use for a scheduled scan to <code>Not configured</code>.</li>
      <li>Specify the time of day to run scheduled scan to <code>Not configured</code>.</li>
      <li>Specify the day of the week to <code>Not configured</code>.</li>
    </ul>
  </li>
</ul>

<p>More information about how to configure the scheduled scan settings can be found in <a href="https://learn.microsoft.com/en-us/defender-endpoint/configure-advanced-scan-types-microsoft-defender-antivirus">Microsoft’s documentation</a>.</p>

<h3 id="scan-after-security-intelligence-updates">Scan after security intelligence updates</h3>

<p>By default, a scan happens after MDAV receives its latest security intelligence updates. If scheduled scans are enabled, maybe this is not needed and can be disabled.</p>

<p>To disable it in Group Policy (or another management tool, such as MDM), go to Computer Configuration &gt; Administrative Templates &gt; Microsoft Defender Antivirus &gt; Security Intelligence Updates, and set Turn on scan after security intelligence update to <code>Disabled</code>.</p>

<h3 id="conflicts-with-other-security-software">Conflicts with other security software</h3>

<p>If other security software, like AV, EDR, and DLP, are used, the proper exclusions need to be defined on both MDE and the other software’s side.</p>

<p>More info about using third party security software with MDE can be found in <a href="https://learn.microsoft.com/en-us/defender-endpoint/microsoft-defender-antivirus-compatibility">Microsoft Defender Antivirus compatibility with other security products</a> and exclusions that need to be added can be found in Microsoft’s <a href="https://learn.microsoft.com/en-us/defender-endpoint/switch-to-mde-phase-2?tabs=Windows#step-2-add-microsoft-defender-for-endpoint-to-the-exclusion-list-for-your-existing-solution">MDE Exclusion List</a></p>

<h2 id="troubleshooting-mode">Troubleshooting Mode</h2>

<p>An easy way to investigate if Defender could be a reason for performance issues is to disable Real-Time Protection. If Tamper Protection is on, then Troubleshooting mode can be used on the machine to allow Tamper Protection to be disabled.</p>

<p>After Troubleshooting Mode is turned on, the following cmdlets can be run to disable Real-Time Protection:</p>

<ol>
  <li>Disable Tamper Protection: <code>Set-MPPreference -DisableTamperProtection $true</code></li>
  <li>Confirm that Tamper Protection is disabled: <code>Get-MpPreference | fl DisableTamperProtection</code> - The output should have a value of “True”.</li>
  <li>Disable Real-Time Protection: <code>Set-MPPreference -DisableRealtimeMonitoring $true</code></li>
  <li>Confirm that Real-Time Protection is disabled: <code>Get-MpPreference | fl DisableRealtimeMonitoring</code> - The output should have a value of “True”.</li>
</ol>

<p>Afterwards, a check should be done on the machine again to see if the issue is resolved. If it is, then Defender Antivirus should be investigated further, by following the steps described in the next sections. Otherwise, there could be another reason for the CPU usage of Defender, and probably a ticket to Microsoft should be opened for further investigation.</p>

<p>More info on PowerShell cmdlet for configuring Defender settings can be found <a href="https://learn.microsoft.com/en-us/powershell/module/defender/set-mppreference?view=windowsserver2025-ps">here</a>.</p>

<p>More info on Troubleshooting Mode can be found <a href="https://learn.microsoft.com/en-us/defender-endpoint/enable-troubleshooting-mode">here</a>.</p>

<p>In the following link, additional scenarios where Troubleshooting Mode may help are described <a href="https://learn.microsoft.com/en-us/defender-endpoint/troubleshooting-mode-scenarios">Troubleshooting mode scenarios</a>.</p>

<h2 id="performance-analyzer">Performance Analyzer</h2>

<p>Performance Analyzer is a tool that helps in determining which files, file extensions, and processes may be causing performance issues in machines during Defender Antivirus scans. This information can be used as input towards potentially defining Defender exclusions.</p>

<p>Running the Performance Analyzer is straight-forward, by running the following cmdlet as an admin:</p>

<p><code>New-MpPerformanceRecording -RecordTo &lt;recording.etl&gt;</code></p>

<p>The <code>RecordTo</code> parameter defines the file where the Performance Analyzer recording is written.</p>

<p>The following is an example of the output of running that cmdlet:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/c843946d-e3ce-4175-a27b-8d109881f483" /></p>

<p>The idea is to “turn on” Performance Analyzer, and while it is running and monitoring, the problematic scenario is reproduced. If it is a general issue which cannot be reproduced on demand, but happens sporadically, the only way to analyze that is by leaving Performance Analyzer running and hoping that the problematic behavior reappears.</p>

<p>After capturing the problematic behavior, the recording can be stopped by pressing <code>Enter</code>, which results in the following output:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/1b914672-b7be-4e9c-8b72-02c369963feb" /></p>

<p>The next step is to parse the Performance Analyzer’s report. To do this the cmdlet <code>Get-MpPerformanceReport</code> is used. This cmdlet has a lot of different available parameters, which can be viewed at Microsoft’s <a href="https://learn.microsoft.com/en-us/defender-endpoint/performance-analyzer-reference">Performance Analyzer Reference</a>.</p>

<p>Below, a series of cmdlets which can help pinpoint the issue are depicted:</p>

<p>To view the overview of the recording:</p>

<p><code>Get-MpPerformanceReport -Path .\recording.etl -Overview</code></p>

<p><img alt="image" src="https://github.com/user-attachments/assets/ecaebaf3-2885-4a86-837c-54b80b1f7e09" /></p>

<p>To view the top 20 scans, paths, extensions, and processes:</p>

<p><code>Get-MpPerformanceReport -Path .\recording.etl -TopScans 20 -TopPaths 20 -TopExtensions 20 -TopProcesses 20</code></p>

<p>The screenshot is cut short because the output is too long:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/996b5778-0df9-4519-98c3-836e5a8cb373" /></p>

<p>After running the above command, if a specific category of top values or a specific value per-se is of interest, it is possible to then dive a bit deeper. For example, if a specific extension seems to be causing a lot of scans, to dive deeper the following cmdlet can be run:</p>

<p><code>Get-MpPerformanceReport -Path .\recording.etl -TopExtensions 20 -TopScansPerExtension 5 -TopPathsPerExtension 5 -TopScansPerPathPerExtension 5 -TopProcessesPerExtension 5 -TopScansPerProcessPerExtension 5 -TopScansPerFilePerExtension 5 -TopFilesPerExtension 5</code></p>

<p>This cmdlet uses all the parameters that end with “PerExtension”, and will produce a much longer report focused on the top extensions, and for each top extension, its top scans, paths, processes and files. This can help in pinpointing what is causing these scans, which may lead to either resolving the source of the issue or defining exclusions in MDE.</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/cb938f45-dc6a-409d-b87c-f8480b080082" /></p>

<p>The report can get lengthy. It is possible to make the output machine readable with the <code>-Raw</code> parameter, and then possibly convert it to exportable formats, like JSON, which can then be exported, saved, and analyzed:</p>

<p><code>Get-MpPerformanceReport -Path .\recording.etl -TopExtensions 20 -TopScansPerExtension 5 -TopPathsPerExtension 5 -TopScansPerPathPerExtension 5 -TopProcessesPerExtension 5 -TopScansPerProcessPerExtension 5 -TopScansPerFilePerExtension 5 -TopFilesPerExtension 5 -Raw | ConvertTo-Json</code></p>

<p><img alt="image" src="https://github.com/user-attachments/assets/e780314d-7a8d-477f-a74e-f717087d4fd5" /></p>

<h3 id="run-performance-analyzer-via-live-response">Run Performance Analyzer via Live Response</h3>

<p>It is also possible to run Performance Analyzer via Live Response, by creating a custom script and uploading it to the Live Response library.</p>

<p>A prerequisite is to allow unsigned scripts to run it, by going to the MDE Portal &gt; System &gt; Settings &gt; Endpoints &gt; Live Response unsigned script execution</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/f61f7ff8-c91d-4813-a938-67bb9652a91e" /></p>

<p>The following script will need to be saved in a file. For this post, the file will be named “LivePerfAnalyser.ps1”</p>

<pre><code class="language-powershell">param (
    [Parameter(Mandatory=$false)]
    [int]$Seconds = 120
)

$Hostname = $env:COMPUTERNAME
$Datetime = Get-Date -Format "yyyyMMdd_HHmmss"
$OutputPath = "C:\Temp\PerformanceRecording_${Hostname}_${Datetime}.etl"

Write-Host "Initiating recording for $Seconds seconds..."
New-MpPerformanceRecording -Seconds $Seconds -RecordTo $OutputPath

Write-Host "Performance Recording for $Hostname for $Seconds seconds was written at $OutputPath"
</code></pre>

<p>The steps to run Performance Analyzer via Live Response are:</p>

<ol>
  <li>Initiate a Live Response session for the endpoint that needs troubleshooting</li>
  <li>
    <p>Upload the script to library by clicking on:</p>

    <p><img alt="image" src="https://github.com/user-attachments/assets/cec0377f-98b5-4277-84d4-0b33f3cc6d58" /></p>
  </li>
  <li>Run the Performance Analyzer with the following command in Live Response: <code>run LivePerfAnalyzer.ps1 -parameters "-Seconds 120"</code>. You can change the value of <code>Seconds</code> to the time period in seconds that you need to run the analyzer, or skip the parameters completely, with the default value being 120 seconds.</li>
  <li>Try to reproduce the activity that causes performance issues on the endpoint. Wait for the designated time.</li>
  <li>
    <p>After the Performance Analyzer finishes, the following is printed:</p>

    <p><img alt="image" src="https://github.com/user-attachments/assets/bdffe3dd-437d-422e-b039-3ec73fb4fe35" /></p>
  </li>
  <li>Get the file from the path written in the script output: <code>getfile \&lt;recordingpath\recording.etl\&gt;</code></li>
  <li>After getting the file, the analysis steps described in the previous section can then be followed.</li>
</ol>

<h2 id="mplog-file-parsing-for-performance-impact">MPLog file parsing for performance impact</h2>

<p>The Microsoft Protection Log (MPLog) file is one of a few files found under the path <code>C:\ProgramData\Microsoft\Windows Defender\Support</code>. Under this path, a few files which contain different Defender logs can be found, including:</p>

<ul>
  <li>MPDetection</li>
  <li>MPDeviceControl</li>
  <li>MPDlpLog</li>
  <li>MPLog</li>
  <li>MPScanSkip</li>
  <li>MpWppCoreTracing</li>
  <li>MpWppTracing</li>
</ul>

<p>These files are very useful when investigating what Defender and its different detection mechanisms did. When it comes to performance troubleshooting, the <code>MPLog</code> file contains a few useful logs, among others, which can assist in detecting processes which experienced high scanning activity, potentially impacting their performance. Credits to the related <a href="https://www.crowdstrike.com/en-us/blog/how-to-use-microsoft-protection-logging-for-forensic-investigations/">article</a> by CrowdStrike explaining these logs. Remember that there is always the option of getting these files via Live Response and the <code>getfile</code> command.</p>

<p>These logs have the following format:</p>

<p><code>2025-12-31T15:45:19.964 ProcessImageName: Razer Synapse Service Process.exe, Pid: 9104, TotalTime: 810, Count: 224, MaxTime: 15, MaxTimeFile: \Device\HarddiskVolume3\Program Files (x86)\Razer\Synapse3\UserProcess\Razer Synapse Service Process.exe.config, EstimatedImpact: 53%</code></p>

<p>Let’s go through this line:</p>

<table>
  <thead>
    <tr>
      <th>Field Name</th>
      <th>Description</th>
      <th>Example</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Date (not stated)</td>
      <td>Timestamp of event</td>
      <td>2025-12-31T15:45:19.964</td>
    </tr>
    <tr>
      <td>ProcessImageName</td>
      <td>Process Image Name</td>
      <td>Razer Synapse Service Process.exe</td>
    </tr>
    <tr>
      <td>Pid</td>
      <td>Process ID of the Process</td>
      <td>9104</td>
    </tr>
    <tr>
      <td>TotalTime</td>
      <td>The summary of all time periods spent in scans of files accessed by this process (milliseconds)</td>
      <td>810</td>
    </tr>
    <tr>
      <td>Count</td>
      <td>The number of scanned files accessed by this process</td>
      <td>224</td>
    </tr>
    <tr>
      <td>MaxTime</td>
      <td>The longest time spent in scanning a single file accessed by this process</td>
      <td>15</td>
    </tr>
    <tr>
      <td>MaxTimeFile</td>
      <td>The file which was scanned for <code>MaxTime</code> milliseconds</td>
      <td>\Device\HarddiskVolume3\Program Files (x86)\Razer\Synapse3\UserProcess\Razer Synapse Service Process.exe.config</td>
    </tr>
    <tr>
      <td>EstimatedImpact</td>
      <td>This value shows the impact that MDAV had on the performance of the above process. It is the percentage of (Total time spent in scans of files accessed by this process)/(Total time which this process experienced scan activity). For example, if you open a large folder in File Explorer, all the files in it are being scanned via Real-Time Protection. The time it takes until the last file is finished being scanned was 10 seconds, but the time that MDAV was actually scanning files was 3 seconds. Here the Estimated Impact is about 3/10 = 30%.</td>
      <td>53%</td>
    </tr>
  </tbody>
</table>

<p>Therefore, using the above logs and the <code>EstimatedImpact</code> value, it is possible to review processes which experience heavy scanning load. Note that this does not mean necessarily that these processes result in higher CPU for the machine, but rather that that specific process experienced a performance hit due to scanning.</p>

<p>The following PowerShell script parses the MPLog file, takes only the logs which include the <code>EstimatedImpact</code> values, and exports it all to a CSV.</p>

<pre><code class="language-powershell">$logfile = Read-Host "Enter log MPLog file"

# Extract relevant log entries, remove whitespaces, and process data
$logs = Get-Content $logfile | `
    Select-String "EstimatedImpact" | ` # Select lines that contain the string "EstimatedImpact"
    Select-String -Pattern '%$' | ` # Select lines that end with "%". Sometimes the logs are not recorded properly and 2 logs are written in the same line, which makes it difficult to parse.
    ForEach-Object {
        # Remove all whitespace from the line
        $_ -replace '\s', ''
    } | `
    ForEach-Object {
        # Add a "Date:" prefix to each log entry
        "Date:" + $_
    } | `
    ForEach-Object {
        # Insert a comma after the 30th character for consistent formatting
        $_.Insert(30, ',')
    }

# Convert the cleaned logs into structured objects for CSV export
$logObjects = $logs | ForEach-Object {
    # Split the cleaned line into fields (key-value pairs separated by ':')
    $fields = $_ -split ',' | ForEach-Object {
        $pair = $_ -split ':'
        @{$pair[0] = $pair[1]}
    }

    # Combine all key-value pairs into a single hash table
    $object = @{}
    foreach ($field in $fields) {
        foreach ($key in $field.Keys) {
            $object[$key] = $field[$key]
        }
    }

    # Remove the '%' from the "EstimatedImpact" field if it exists
    if ($object["EstimatedImpact"]) {
        $object["EstimatedImpact"] = $object["EstimatedImpact"] -replace '%', ''
    }

    # Return the final object as a PSCustomObject
    [PSCustomObject]$object
}

# Export the processed data to a CSV file
$outfile = $logfile -replace '\.log$', '.csv'
$logObjects | Export-Csv -Path $outfile -NoTypeInformation -Encoding UTF8

Write-Host "Process Logs exported to $outfile"
Read-Host -Prompt "Press Enter to exit"
</code></pre>

<p>Below is an example output of the above script:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/3792a576-513c-4929-80a9-b7612bc16966" /></p>

<p>After exporting the CSV file, it is useful to sort by <code>TotalTime</code> to find processes that tend to result in a lot of scans. For example, below, after sorting by <code>TotalTime</code>, it can be seen that the Process <code>RazerCentral.exe</code> tends to access a lot of files which are scanned for a long time:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/624a4b94-b337-4628-bf57-491489d0dab7" /></p>

<p>Another option is to sort by <code>EstimatedImpact</code> to view processes which are affected a lot by MDAV scans:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/db1be4db-92a2-4453-99f3-583ab42d39f6" /></p>

<p>A similar analysis can also be done by inserting a Pivot Table, and adding <code>ProcessImageName</code> in Rows, and <code>TotalTime</code> or <code>EstimatedImpact</code> in Values, on which you can sort again by the <code>Sum of TotalTime</code> or <code>Sum of EstimatedImpact</code>. This will produce a table like the following:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/60a864b3-db49-42c4-8569-95d58670bac8" /></p>

<p>As shown above, the MPLog is yet another way of identifying processes which may impact the performance of the machine or may suffer from MDAV scans themselves.</p>

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

<p>Sometimes, MDE and its components may utilize a higher-than-normal CPU percentage. Identifying the root cause can prove difficult. Hopefully, with this post it will be a little bit easier.</p>

<p>Stick around for the next posts, where we will dive deep into looking Windows Event Logs, and also may move to other Microsoft Security products like Purview.</p>

<p>I hope to see you back in the next one!</p>]]></content><author><name>Kostas Koutroumpouchos</name></author><category term="Other" /><summary type="html"><![CDATA[Introduction]]></summary></entry><entry><title type="html">Using KQL to identify detections from MDE - Getting to know MDE Part 2</title><link href="https://kostaskoutrou.github.io/2026/01/06/using-kql-for-mde.html" rel="alternate" type="text/html" title="Using KQL to identify detections from MDE - Getting to know MDE Part 2" /><published>2026-01-06T00:00:00+00:00</published><updated>2026-01-06T00:00:00+00:00</updated><id>https://kostaskoutrou.github.io/2026/01/06/using-kql-for-mde</id><content type="html" xml:base="https://kostaskoutrou.github.io/2026/01/06/using-kql-for-mde.html"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>Welcome to the 2nd part of the blog post series “Getting to know MDE”!</p>

<p>In this series, each post contains a different component/feature/methodology when it comes to understanding and managing Microsoft Defender for Endpoint (MDE).</p>

<blockquote>
  <p>In this post, the focus is <strong>on KQL and what queries to run to investigate for blocks/detections done by MDE</strong>.</p>
</blockquote>

<p>In each section, a brief description of a different MDE capability is described, including what to look for when running KQL queries, and a simple KQL query searching for events of that capability.</p>

<p>If you want to get a tl;dr KQL query from this blog post which includes all the detections, check the last section.</p>

<p>All the information is based on official documentation of Microsoft, which you can find <a href="https://learn.microsoft.com/en-us/defender-endpoint/">here</a>.</p>

<p>So let’s get started.</p>

<hr />

<h2 id="contents">Contents</h2>

<ul id="markdown-toc">
  <li><a href="#introduction" id="markdown-toc-introduction">Introduction</a></li>
  <li><a href="#contents" id="markdown-toc-contents">Contents</a>    <ul>
      <li><a href="#the-deviceevents-table" id="markdown-toc-the-deviceevents-table">The DeviceEvents Table</a></li>
      <li><a href="#attack-surface-reduction-rules-detections" id="markdown-toc-attack-surface-reduction-rules-detections">Attack Surface Reduction Rules Detections</a></li>
      <li><a href="#controlled-folder-access-detections" id="markdown-toc-controlled-folder-access-detections">Controlled Folder Access Detections</a></li>
      <li><a href="#device-control" id="markdown-toc-device-control">Device Control</a></li>
      <li><a href="#exploit-protection" id="markdown-toc-exploit-protection">Exploit Protection</a></li>
      <li><a href="#network-protection-web-protection-and-smartscreen" id="markdown-toc-network-protection-web-protection-and-smartscreen">Network Protection, Web Protection, and SmartScreen</a></li>
      <li><a href="#tamper-protection" id="markdown-toc-tamper-protection">Tamper Protection</a></li>
      <li><a href="#mdav-detections" id="markdown-toc-mdav-detections">MDAV Detections</a></li>
      <li><a href="#potential-unwanted-apps-pua" id="markdown-toc-potential-unwanted-apps-pua">Potential Unwanted Apps (PUA)</a></li>
      <li><a href="#windows-defender-application-control-wdac-and-applocker" id="markdown-toc-windows-defender-application-control-wdac-and-applocker">Windows Defender Application Control (WDAC) and AppLocker</a></li>
      <li><a href="#mde-alerts" id="markdown-toc-mde-alerts">MDE Alerts</a></li>
      <li><a href="#general-kql-query-for-all-detections" id="markdown-toc-general-kql-query-for-all-detections">General KQL query for all detections</a></li>
    </ul>
  </li>
  <li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
</ul>

<hr />

<h3 id="the-deviceevents-table">The DeviceEvents Table</h3>

<p>Before starting with each MDE capability, it is important to focus on the Advanced Hunting table which includes all the MDE detection events.</p>

<p>The detections by MDE are reported under the <code>DeviceEvents</code> table in Advanced Hunting.</p>

<p>Two important columns of the DeviceEvents table are the following:</p>

<ul>
  <li><strong>ActionType</strong>: The <code>ActionType</code> column shows what action triggered the <code>DeviceEvents</code> event. When it comes to MDE, the <code>ActionType</code> column shows which MDE detection tirggered the event.</li>
  <li><strong>AdditionalFields</strong>: The <code>AdditionalFields</code> column contains, as its name suggests, additional information regarding the event, information which does not fit any of the other columns. When it comes to MDE detections, the <code>AdditionalFields</code> column contains necessary information about the detection, such as whether something was blocked or audited, or what policy triggered the event. It is in JSON format and, depending on what is being searched for, the <code>AdditionalFields</code> JSON data will need to be parsed in order to retrieve the correct information. In the queries below, there are some examples to get a better idea of how to parse the column.</li>
</ul>

<p>You can always refer to the <code>DeviceEvents</code> Schema reference directly from the Advanced Hunting page in Defender to review the different available columns and their values:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/d43c04c7-c212-49b4-8e5e-767a6b20decd" /></p>

<h3 id="attack-surface-reduction-rules-detections">Attack Surface Reduction Rules Detections</h3>

<p>When looking for ASR Rules detections, keep the following in mind:</p>

<p>For each ASR Rule detection, there can be one of three possible <code>ActionType</code> values in the <code>DeviceEvents</code> table, depending on the enabled state of the ASR Rule for the machine:</p>

<table>
  <thead>
    <tr>
      <th>ActionType</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Asr&lt;RuleName&gt;Audited</td>
      <td>ASR Rule of &lt;RuleName&gt; was triggered but did not block.</td>
    </tr>
    <tr>
      <td>Asr&lt;RuleName&gt;Blocked</td>
      <td>ASR Rule of &lt;RuleName&gt; was triggered and blocked.</td>
    </tr>
    <tr>
      <td>Asr&lt;RuleName&gt;WarnBypassed</td>
      <td>ASR Rule of &lt;RuleName&gt; was triggered in Warn mode, and the user excluded themselves from it.</td>
    </tr>
  </tbody>
</table>

<p>In the following table the ASR Rules action types are listed and the ASR rule that they correspond to.</p>

<table>
  <thead>
    <tr>
      <th>ActionType</th>
      <th>ASR Rule</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>AsrAbusedSystemToolAudited<br />AsrAbusedSystemToolBlocked<br />AsrAbusedSystemToolWarnBypassed</td>
      <td>Block use of a copied or impersonated system tools</td>
    </tr>
    <tr>
      <td>AsrAdobeReaderChiledProcessAudited<br />AsrAdobeReaderChiledProcessBlocked<br />AsrAdobeReaderChiledProcessWarnBypassed</td>
      <td>Block Adobe Reader from creating child processes</td>
    </tr>
    <tr>
      <td>AsrExecutableEmailContentAudited<br />AsrExecutableEmailContentBlocked<br />AsrExecutableEmailContentWarnBypassed</td>
      <td>Block Launching of executable content from email attachment</td>
    </tr>
    <tr>
      <td>AsrExecutableOfficeContentAudited<br />AsrExecutableOfficeContentBlocked<br />AsrExecutableOfficeContentWarnBypassed</td>
      <td>Block Office applications from creating executable content</td>
    </tr>
    <tr>
      <td>AsrLsassCredentialTheftAudited<br />AsrLsassCredentialTheftBlocked<br />AsrLsassCredentialTheftWarnBypassed</td>
      <td>Block credential stealing from the Windows local security authority subsystem (lsass.exe)</td>
    </tr>
    <tr>
      <td>AsrObfuscatedScriptAudited<br />AsrObfuscatedScriptBlocked<br />AsrObfuscatedScriptWarnBypassed</td>
      <td>Block execution of potentially obfuscated scripts</td>
    </tr>
    <tr>
      <td>AsrOfficeChildProcessAudited<br />AsrOfficeChildProcessBlocked<br />AsrOfficeChildProcessWarnBypassed</td>
      <td>Block all Office applications from creating child processes</td>
    </tr>
    <tr>
      <td>AsrOfficeCommAppChildProcessAudited<br />AsrOfficeCommAppChildProcessBlocked<br />AsrOfficeCommAppChildProcessWarnBypassed</td>
      <td>Block Office communication application from creating child processes</td>
    </tr>
    <tr>
      <td>AsrOfficeMacroWin32ApiCallsAudited<br />AsrOfficeMacroWin32ApiCallsBlocked<br />AsrOfficeMacroWin32ApiCallsWarnBypassed</td>
      <td>Block Win32 API calls from Office macro</td>
    </tr>
    <tr>
      <td>AsrOfficeProccessInjectionAudited<br />AsrOfficeProccessInjectionBlocked<br />AsrOfficeProccessInjectionWarnBypassed</td>
      <td>Block Office applications from injecting code into other processes</td>
    </tr>
    <tr>
      <td>AsrPersistenceThroughWmiAudited<br />AsrPersistenceThroughWmiBlocked<br />AsrPersistenceThroughWmiWarnBypassed</td>
      <td>Block persistence through WMI event subscription</td>
    </tr>
    <tr>
      <td>AsrPsexecWmiChildProcessAudited<br />AsrPsexecWmiChildProcessBlocked<br />AsrPsexecWmiChildProcessWarnBypassed</td>
      <td>Block Process Creations originating from PSExec &amp; WMI commands</td>
    </tr>
    <tr>
      <td>AsrRandomwareAudited<br />AsrRandomwareBlocked<br />AsrRandomwareWarnBypassed</td>
      <td>Use advanced protection against ransomware</td>
    </tr>
    <tr>
      <td>AsrSafeModeRebootAudited<br />AsrSafeModeRebootBlocked<br />AsrSafeModeRebootWarnBypassed</td>
      <td>Block rebooting machine in Safe Mode</td>
    </tr>
    <tr>
      <td>AsrScriptExecutableDownloadAudited<br />AsrScriptExecutableDownloadBlocked<br />AsrScriptExecutableDownloadWarnBypassed</td>
      <td>Block JavaScript or VBScript from launching downloaded executable content</td>
    </tr>
    <tr>
      <td>AsrUntrustedExecutableAudited<br />AsrUntrustedExecutableBlocked<br />AsrUntrustedExecutableWarnBypassed</td>
      <td>Block executable files from running unless they meet a prevalence, age, or trusted list criteria</td>
    </tr>
    <tr>
      <td>AsrUntrustedUsbProcessAudited<br />AsrUntrustedUsbProcessBlocked<br />AsrUntrustedUsbProcessWarnBypassed</td>
      <td>Block untrusted and unsigned processes that run from USB</td>
    </tr>
    <tr>
      <td>AsrVulnerableSignedDriverAudited<br />AsrVulnerableSignedDriverBlocked<br />AsrVulnerableSignedDriverWarnBypassed</td>
      <td>Block abuse of in-the-wild exploited vulnerable signed drivers</td>
    </tr>
    <tr>
      <td>AsrWebShellOnServerAudited<br />AsrWebShellOnServerBlocked<br />AsrWebShellWarnBypassed (does not have the “OnServer” substring like the other 2)</td>
      <td>Block Webshell creation for Servers</td>
    </tr>
  </tbody>
</table>

<p>Each ASR Rule has its own GUID. GUIDs are needed when configuring, e.g., a GPO to enable ASR rules in Audit or Block mode for machines. The ASR Rule to GUID matrix can be found in Microsoft’s <a href="https://learn.microsoft.com/en-us/defender-endpoint/attack-surface-reduction-rules-reference#asr-rule-to-guid-matrix">Attack surface reduction rules reference</a></p>

<p>The KQL query to search for ASR Rule detections is the following:</p>

<pre><code class="language-kusto">DeviceEvents
| where ActionType startswith "Asr"
| extend RuleId=extractjson("$Ruleid", AdditionalFields, typeof(string)) //Parse
//the AdditionalFields column in order to extract the ASR Rule GUID.
</code></pre>

<h3 id="controlled-folder-access-detections">Controlled Folder Access Detections</h3>

<p>When looking for CFA detections, keep the following in mind:</p>

<p>CFA has only 2 <code>ActionType</code> possible values reported in <code>DeviceEvents</code>:</p>

<table>
  <thead>
    <tr>
      <th>ActionType</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ControlledFolderAccessViolationAudited</td>
      <td>Controlled folder accesses detected an attempt to modify a protected folder.</td>
    </tr>
    <tr>
      <td>ControlledFolderAccessViolationBlocked</td>
      <td>Controlled folder access blocked an attempt to modify a protected folder.</td>
    </tr>
  </tbody>
</table>

<p>The KQL query to search for CFA detections is the following:</p>

<pre><code class="language-kql">DeviceEvents
//If you want only blocks, remove the "ControlledFolderAccessViolationAudited".
| where ActionType in ("ControlledFolderAccessViolationAudited","ControlledFolderAccessViolationBlocked")
</code></pre>

<h3 id="device-control">Device Control</h3>

<p>When looking for Device Control detections, keep the following in mind:</p>

<p>Device Control has 7 <code>ActionType</code> possible values reported in <code>DeviceEvents</code>:</p>

<table>
  <thead>
    <tr>
      <th>DeviceEvents ActionType</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>BluetoothPolicyTriggered</td>
      <td>A Bluetooth service was allowed or blocked by a device control policy.</td>
    </tr>
    <tr>
      <td>PnpDeviceConnected</td>
      <td>This event appears when a PnP device is connected on an onboarded device, regardless of whether Device Control is enabled or not.</td>
    </tr>
    <tr>
      <td>PnPDeviceAllowed</td>
      <td>Device control allowed a trusted plug and play (PnP) device. Note that when a device installation restrictions are configured and a device is installed, an event with <code>ActionType</code> of <code>PnPDeviceAllowed</code> is created.</td>
    </tr>
    <tr>
      <td>PnPDeviceBlocked</td>
      <td>Device control blocked an untrusted plug and play (PnP) device.</td>
    </tr>
    <tr>
      <td>PrintJobBlocked</td>
      <td>Device control prevented an untrusted printer from printing.</td>
    </tr>
    <tr>
      <td>RemovableStorageFileEvent</td>
      <td>Removable storage file activity matched a device control removable storage access control policy.</td>
    </tr>
    <tr>
      <td>RemovableStoragePolicyTriggered</td>
      <td>Device control detected an attempted read/write/execute event from a removable storage device.</td>
    </tr>
  </tbody>
</table>

<p>The KQL query to search for Device Control detections is the following:</p>

<pre><code class="language-kql">DeviceEvents
| where ActionType in ("BluetoothPolicyTriggered","PnPDeviceBlocked",
"PrintJobBlocked","RemovableStorageFileEvent","RemovableStoragePolicyTriggered")
//You may want to search for "PnPDeviceAllowed" and "PnPDeviceConnected", too, after
//configuring Device Control policies, to make sure that the desired activities are
//allowed and to investigate further activities. But for checking for Device Control
//Blocks, it is not needed.
</code></pre>

<p>Keep in mind that even after excluding the <code>PnPDeviceAllowed</code> <code>ActionType</code>, the other possible <code>ActionType</code> values still contain activities which may not indicate blocking, but allowing.</p>

<p>For example, take a look at the following KQL query, taken from <a href="https://learn.microsoft.com/en-us/defender-endpoint/device-control-overview#control-access-to-removable-media-using-device-control">here</a>:</p>

<pre><code class="language-kql">DeviceEvents
| where ActionType == "RemovableStoragePolicyTriggered"
| extend parsed=parse_json(AdditionalFields)
| extend MediaClass = tostring(parsed.ClassName)
| extend MediaDeviceId = tostring(parsed.DeviceId)
| extend MediaDescription = tostring(parsed.DeviceDescription)
| extend SerialNumberId = tostring(parsed.SerialNumber)
| extend RemovableStoragePolicy = tostring(parsed.RemovableStoragePolicy)
| extend RemovableStorageAccess = tostring(parsed.RemovableStorageAccess)
| extend RemovableStoragePolicyVerdict = tostring(parsed.RemovableStoragePolicyVerdict)
| extend PID = tostring(parsed.ProductId)
| extend VID = tostring(parsed.VendorId)
| extend VID_PID = strcat(VID,"_",PID)
| extend InstancePathId = tostring(parsed.DeviceInstanceId)
| project Timestamp, RemovableStoragePolicy, RemovableStorageAccess,RemovableStoragePolicyVerdict, SerialNumberId,VID, PID, VID_PID, InstancePathId
| order by Timestamp desc
</code></pre>

<p>The <code>ActionType</code> <code>RemovableStoragePolicyTriggered</code> does not mean a block or an allow action by default. It means that a policy was triggered, and more details can be found in the <code>AdditionalFields</code> column.</p>

<h3 id="exploit-protection">Exploit Protection</h3>

<p>When looking for Exploit Protection detections, keep the following in mind:</p>

<p>Ignore <code>DeviceEvents</code> with <code>ActionType</code> <code>ExploitGuardNetworkProtectionAudited</code> or <code>ExploitGuardNetworkProtectionBlocked</code> when checking for Exploit Protection, as these Action Types correspond to detection done by Network Protection, which is a different ASR, and will be described in the next section.</p>

<p>Some Exploit Protection measures do not create events, because they do not detect. For example, with Mandatory ASLR and Bottom-up ASLR, a program’s code and libraries and loaded at a random memory address instead of a predictable one. This measure does not detect anything to create an event for.</p>

<p>Microsoft’s <a href="https://learn.microsoft.com/en-us/defender-endpoint/exploit-protection#exploit-protection-and-advanced-hunting">documentation</a> does not include the Control Flow Guard (CFG) DeviceEvents <code>ActionType</code>, it only includes the Exploit Protection measures Action Types actually with the substring <code>ExploitGuard</code>. The complete list of the possible <code>ActionType</code> values for Exploit Protection is depicted in the following table.</p>

<table>
  <thead>
    <tr>
      <th>DeviceEvents ActionType</th>
      <th>Description</th>
      <th>Exploit Protection Measure</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ExploitGuardAcgAudited<br />ExploitGuardAcgEnforced</td>
      <td>Arbitrary code guard (ACG) <strong>detected</strong> or <strong>blocked</strong> an attempt to modify code page permissions or create unsigned code pages.</td>
      <td>Arbitrary code guard (ACG)</td>
    </tr>
    <tr>
      <td>ExploitGuardChildProcessAudited<br />ExploitGuardChildProcessBlocked</td>
      <td>Exploit protection <strong>detected</strong> or <strong>blocked</strong> the creation of a child process</td>
      <td>Don’t allow child processes</td>
    </tr>
    <tr>
      <td>ExploitGuardEafViolationAudited<br />ExploitGuardEafViolationBlocked</td>
      <td>Export address filtering (EAF) blocked possible exploitation activity.</td>
      <td>Export address filtering (EAF)</td>
    </tr>
    <tr>
      <td>ExploitGuardIafViolationAudited<br />ExploitGuardIafViolationBlocked</td>
      <td>Import address filtering (IAF) <strong>detected</strong> or <strong>blocked</strong> possible exploitation activity.</td>
      <td>Import address filtering (IAF)</td>
    </tr>
    <tr>
      <td>ExploitGuardLowIntegrityImageAudited<br />ExploitGuardLowIntegrityImageBlocked</td>
      <td>Exploit protection <strong>detected</strong> or <strong>blocked</strong> the launch of a process from a low-integrity file.</td>
      <td>Block low integrity images</td>
    </tr>
    <tr>
      <td>ExploitGuardNonMicrosoftSignedAudited<br />ExploitGuardNonMicrosoftSignedBlocked</td>
      <td>Exploit protection <strong>detected</strong> or <strong>blocked</strong> the launch of a process from an image file that is not signed by Microsoft.</td>
      <td>Code integrity guard</td>
    </tr>
    <tr>
      <td>ExploitGuardRopExploitAudited<br />ExploitGuardRopExploitBlocked</td>
      <td>Exploit protection <strong>detected</strong> or <strong>blocked</strong> possible return-object programming (ROP) exploitation.</td>
      <td>Simulate execution (SimExec)<br />Validate API invocation (CallerCheck)<br />Validate stack integrity (StackPivot)</td>
    </tr>
    <tr>
      <td>ExploitGuardSharedBinaryAudited<br />ExploitGuardSharedBinaryBlocked</td>
      <td>Exploit protection <strong>detected</strong> or <strong>blocked</strong> the launch of a process from a file in a remote shared file.</td>
      <td>Block remote images</td>
    </tr>
    <tr>
      <td>ExploitGuardWin32SystemCallAudited<br />ExploitGuardWin32SystemCallBlocked</td>
      <td>Exploit protection <strong>detected</strong> or <strong>blocked</strong> a call to the Windows system AIP</td>
      <td>Disable Win32k system calls</td>
    </tr>
    <tr>
      <td>ControlFlowGuardViolation</td>
      <td>Control Flow Guard terminated an application after detecting an invalid function call</td>
      <td>Control flow guard (CFG)</td>
    </tr>
  </tbody>
</table>

<p>Unfortunately, it seems that for the following Exploit Protection measures, there are no Device Events created. If you know more details, please let me know:</p>

<ul>
  <li>Data Execution Prevention (DEP)</li>
  <li>Validate exception chains (SEHOP)</li>
  <li>Validate heap integrity</li>
  <li>Block untrusted fonts</li>
  <li>Disable extension points</li>
  <li>Validate handle usage</li>
  <li>Validate image dependency integrity</li>
  <li>Hardware-enforced stack protection</li>
</ul>

<p>The KQL query to search for Exploit Protection detections is the following:</p>

<pre><code class="language-kql">DeviceEvents
| where ActionType startswith "ExploitGuard" and ActionType !contains "NetworkProtection" or ActionType == "ControlFlowGuardViolation"
</code></pre>
<h3 id="network-protection-web-protection-and-smartscreen">Network Protection, Web Protection, and SmartScreen</h3>

<p>Network, Web Protection, and Smart Screen are in the same section because of their co-dependency.</p>

<p>As mentioned in the previous section, Network and Web Protection have only 2 <code>ActionType</code> possible values reported in <code>DeviceEvents</code>:</p>

<table>
  <thead>
    <tr>
      <th>ActionType</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ExploitGuardNetworkProtectionAudited</td>
      <td>Network protection detected an attempt to access a malicious or unwanted IP address domain or URL.</td>
    </tr>
    <tr>
      <td>ExploitGuardNetworkProtectionBlocked</td>
      <td>Network protection blocked a malicious or unwanted IP address domain or URL.</td>
    </tr>
  </tbody>
</table>

<p>Because Microsoft Defender SmartScreen is also involved in the detections, the following SmartScreen related <code>ActionType</code> values should also be searched for:</p>

<table>
  <thead>
    <tr>
      <th>ActionType</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>SmartScreenExploitWarning</td>
      <td>SmartScreen warned about opening a <strong>web page</strong> that contains an exploit.</td>
    </tr>
    <tr>
      <td>SmartScreenUrlWarning</td>
      <td>SmartScreen warned about opening a low-reputation <strong>URL</strong> that might be hosting malware or is a phishing site.</td>
    </tr>
    <tr>
      <td>SmartScreenAppWarning</td>
      <td>SmartScreen warns about running a downloaded <strong>application</strong> that is untrusted or malicious.</td>
    </tr>
    <tr>
      <td>SmartScreenUserOverride</td>
      <td>A user has overriden a SmartScreen warning and continued to open an untrusted <strong>app</strong> or a low-reputation <strong>URL</strong>.</td>
    </tr>
  </tbody>
</table>

<p>As shown above, some SmartScreen detections are related to <strong>URL</strong> detections, <strong>application</strong> detections, or both.</p>

<p>The KQL query to search for Network/Web Protection or SmartScreen detections is the following:</p>

<pre><code class="language-kql">DeviceEvents
|where ActionType in ("ExploitGuardNetworkProtectionAudited","ExploitGuardNetworkProtectionBlocked",
"SmartScreenExploitWarning","SmartScreenUrlWarning","SmartScreenAppWarning","SmartScreenUserOverride")
</code></pre>
<p>Below is an example of how you can parse the JSON data in the <code>AdditionalFields</code> column when it comes to Network/Web Protection detections (ref. Microsoft’s <a href="https://learn.microsoft.com/en-us/defender-endpoint/network-protection#advanced-hunting">Documentation</a>):</p>

<pre><code class="language-kql">DeviceEvents
|where ActionType in ("ExploitGuardNetworkProtectionAudited","ExploitGuardNetworkProtectionBlocked")
|extend ParsedFields=parse_json(AdditionalFields)
|project DeviceName, ActionType, Timestamp, RemoteUrl, InitiatingProcessFileName,IsAudit=tostring(ParsedFields.IsAudit),
ResponseCategory=tostring(ParsedFields.ResponseCategory), DisplayName=tostring(ParsedFields.DisplayName)
|sort by Timestamp desc
</code></pre>
<h3 id="tamper-protection">Tamper Protection</h3>

<p>Regarding Tamper Protection, the Defender <code>AlertInfo</code> table can be searched for any Alert raised related to Defender tampering.</p>

<p>The KQL query to search for Defender alerts is the following:</p>

<pre><code class="language-kql">AlertInfo
| where Title contains "tamper"
</code></pre>

<p>The <code>DeviceEvents</code> table can also be used to identify tampering attempts. Tampering attempts could be done either by the user of by any other process, including Group Policies enforcement, Intune, etc.</p>

<p>Keep in mind that when Tamper Protection is on, the following settings are blocked from being changed by <strong>any</strong> means, including GPO, Intune, Configuration Manager, etc.</p>

<ul>
  <li>Virus and threat protection remains enabled.</li>
  <li>Real-time protection remains turned on.</li>
  <li>Behavior monitoring remains turned on.</li>
  <li>Antivirus protection, including IOfficeAntivirus (IOAV) remains enabled.</li>
  <li>Cloud protection remains enabled.</li>
  <li>Security intelligence updates occur.</li>
  <li>Automatic actions are taken on detected threats.</li>
  <li>Notifications are visible in the Windows Security app on Windows devices.</li>
  <li>Archived files are scanned.</li>
  <li>Exclusions can’t be modified or added</li>
</ul>

<p>So, most likely, some Tamper Attempts initiated by processes like <code>svchost.exe</code> will be reported, with commands like <code>svchost.exe -k GPSvcGroup</code>. These are attempts of defender settings changes done by processes like the Service Host <code>svchost.exe</code>.</p>

<p>The KQL query to search for Tamper Protection related events is the following:</p>

<pre><code class="language-kql">DeviceEvents
| where ActionType contains "tamper"
</code></pre>

<h3 id="mdav-detections">MDAV Detections</h3>

<p>When it comes to detections from Microsoft Defender Antivirus, there are five different <code>ActionType</code> possible values:</p>

<table>
  <thead>
    <tr>
      <th>ActionType</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>AntivirusDetection</td>
      <td>Windows Defender Antivirus detected a threat.</td>
    </tr>
    <tr>
      <td>AntivirusError</td>
      <td>Windows Defender Antivirus encountered an error while taking action on malware or a potentially unwanted application.</td>
    </tr>
    <tr>
      <td>AntivirusMalwareActionFailed</td>
      <td>Windows Defender Antivirus attempted to take action on malware or a potentially unwanted application but the action failed.</td>
    </tr>
    <tr>
      <td>AntivirusMalwareBlocked</td>
      <td>Windows Defender Antivirus blocked files or activity involving malware or potentially unwanted applications or suspicious behavior.</td>
    </tr>
    <tr>
      <td>AntivirusReport</td>
      <td>This should be ignored. It creates too many events without any useful info. It reports events of when Microsoft Defender Antivirus reported a threat, which can be a memory, boot sector, or rootkit threat.</td>
    </tr>
  </tbody>
</table>

<p>The KQL query to search for MDAV detections is the following:</p>

<pre><code class="language-kql">DeviceEvents
| where ActionType in ("AntivirusDetection","AntivirusError",
"AntivirusMalwareActionFailed","AntivirusMalwareBlocked")
</code></pre>

<h3 id="potential-unwanted-apps-pua">Potential Unwanted Apps (PUA)</h3>

<p>PUA detections are under the <code>ActionType</code> of value <code>AntivirusDetection</code>, and therefore its detections can be found with the KQL queries of the previous section.</p>

<p>If there is a focus to investigate specifically for PUA detections/blocks, the following KQL query can be used:</p>

<pre><code class="language-kql">DeviceEvents
| where ActionType == "AntivirusDetection"
| where tostring(AdditionalFields) contains "PUA"
</code></pre>

<p>Generally, it was noticed that the KQL queries provided by Microsoft’s documentation sometimes have issues. For example, the KQL query provided <a href="https://learn.microsoft.com/en-us/defender-endpoint/detect-block-potentially-unwanted-apps-microsoft-defender-antivirus#view-pua-events-using-advanced-hunting">here</a> to view PUA events only checks for a subset of PUA events, because it checks for the “PUA:” substring instead of “PUA” (without the column), missing many events like “PUABundler”. Therefore, it is better to use the above KQL query provided in this post.</p>

<h3 id="windows-defender-application-control-wdac-and-applocker">Windows Defender Application Control (WDAC) and AppLocker</h3>

<p>WDAC and AppLocker have a lot of different possible <code>ActionType</code> values, which are depicted in the table below:</p>

<table>
  <thead>
    <tr>
      <th>ActionType</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>AppControlAppInstallationAudited<br />AppControlAppInstallationBlocked</td>
      <td>App control detected/blocked the installation of an untrusted app.</td>
    </tr>
    <tr>
      <td>AppControlCIScriptAudited<br />AppControlCIScriptBlocked</td>
      <td>A script MSI file generated by Windows LockDown Policy was audited/blocked.</td>
    </tr>
    <tr>
      <td>AppControlCodeIntegrityDriverRevoked</td>
      <td>Application control found a driver with a revoked certificate.</td>
    </tr>
    <tr>
      <td>AppControlCodeIntegrityImageAudited</td>
      <td>Application control detected an executable file that violated code integrity policies.</td>
    </tr>
    <tr>
      <td>AppControlCodeIntegrityImageRevoked</td>
      <td>Application control found an executable file with a revoked certificate.</td>
    </tr>
    <tr>
      <td>AppControlCodeIntegrityOriginAllowed</td>
      <td>App control allowed a file due to its good reputation (ISG) or installation source (managed installer).</td>
    </tr>
    <tr>
      <td>AppControlCodeIntegrityOriginAudited<br />AppControlCodeIntegrityOriginBlocked</td>
      <td>App control detected/blocked a file due to its bad reputation (ISG) or installation source (managed installer).</td>
    </tr>
    <tr>
      <td>AppControlCodeIntegrityAudited<br />AppControlCodeIntegrityBlocked</td>
      <td>App control detected/blocked a code integrity policy violation.</td>
    </tr>
    <tr>
      <td>AppControlCodeIntegrityLoaded</td>
      <td>An app control code integrity policy was loaded.</td>
    </tr>
    <tr>
      <td>AppControlCodeIntegritySigningInformation</td>
      <td>App control signed information was generated.</td>
    </tr>
    <tr>
      <td>AppControlExecutableAudited<br />AppControlExecutableBlocked</td>
      <td>App control detected/blocked the use of an untrusted executable.</td>
    </tr>
    <tr>
      <td>AppControlPackagedAppAudited<br />AppControlPackagedAppBlocked</td>
      <td>App control detected/blocked the use of an untrusted packages app.</td>
    </tr>
    <tr>
      <td>AppControlPolicyApplied</td>
      <td>An app control policy was applied to the device.</td>
    </tr>
    <tr>
      <td>AppControlScriptAudited<br />AppControlScriptBlocked</td>
      <td>App control detected/blocked the use of an untrusted script.</td>
    </tr>
    <tr>
      <td>AppLockerBlockExecutable</td>
      <td>AppLocker prevented an untrusted executable from running.</td>
    </tr>
    <tr>
      <td>AppLockerBlockPackagedApp</td>
      <td>AppLocker prevented an untrusted packaged app from running.</td>
    </tr>
    <tr>
      <td>AppLockerBlockPackagedAppInstallation</td>
      <td>AppLocker prevented the installation of an untrusted packaged app.</td>
    </tr>
    <tr>
      <td>AppLockerBlockScript</td>
      <td>AppLocker prevented an untrusted script from running.</td>
    </tr>
  </tbody>
</table>

<p>The KQL query to search for WDAC and AppLocker detections is the following:</p>

<pre><code class="language-kql">DeviceEvents
| where (ActionType startswith "AppControl" or ActionType startswith "AppLocker") and ActionType contains "block"
</code></pre>

<h3 id="mde-alerts">MDE Alerts</h3>

<p>MDE Alerts can be queried to investigate for detections which were not reported by the above queries, with the following KQL.</p>

<pre><code class="language-kql">AlertInfo
| where Timestamp &gt; ago(30d)
| where ServiceSource == "Microsoft Defender for Endpoint"
</code></pre>

<h3 id="general-kql-query-for-all-detections">General KQL query for all detections</h3>

<p>There are almost too many Action Types to look for when investigating the behavior of Defender on a machine. In order to have a headstart, the following KQL query is written to search for most, if not all, of them. The query creates a new column named <code>MDEMeasure </code>based on the MDE capability, with the following possible values:</p>

<table>
  <thead>
    <tr>
      <th>MDEMeasure Value</th>
      <th>MDE Capability</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ASRRules</td>
      <td>Attack Surface Reduction Rules</td>
    </tr>
    <tr>
      <td>CFA</td>
      <td>Controlled Folder Access</td>
    </tr>
    <tr>
      <td>DeviceControl</td>
      <td>Device Control</td>
    </tr>
    <tr>
      <td>ExploitProtection</td>
      <td>Exploit Protection</td>
    </tr>
    <tr>
      <td>TamperProtection</td>
      <td>Tamper Protection</td>
    </tr>
    <tr>
      <td>PUA</td>
      <td>Potentially Unwanted Apps</td>
    </tr>
    <tr>
      <td>MDAV</td>
      <td>Microsoft Defender Antivirus</td>
    </tr>
    <tr>
      <td>WDAC_AppLocker</td>
      <td>Windows Defender App Control and AppLocker</td>
    </tr>
  </tbody>
</table>

<pre><code class="language-kql">DeviceEvents
| where DeviceId == "&lt;Enter Device ID here&gt;" //If you are investigating a specific device, then enter its Device ID in this filter
| extend MDEMeasure = case(ActionType startswith "Asr" and (ActionType endswith "Blocked" or ActionType endswith "WarnBypassed"), "ASRRules",
ActionType == "ControlledFolderAccessViolationBlocked", "CFA",
ActionType in ("BluetoothPolicyTriggered", "PnPDeviceBlocked", "PrintJobBlocked", "RemovableStorageFileEvent", "RemovableStoragePolicyTriggered"), "DeviceControl", //Device Control, these need to be checked for Allow actions
(ActionType startswith "ExploitGuard" and (ActionType endswith "Enforced" or ActionType contains "Blocked")) and ActionType !contains "NetworkProtection" or ActionType =="ControlFlowGuardViolation" , "ExploitProtection",
ActionType in ("ExploitGuardNetworkProtectionBlocked","SmartScreenExploitWarning","SmartScreenUrlWarning","SmartScreenAppWarning","SmartScreenUserOverride"),"Network_Web_SmartScreen",
ActionType contains "tamper","TamperProtection",
ActionType in ("AntivirusDetection","AntivirusError","AntivirusMalwareActionFailed","AntivirusMalwareBlocked") and tostring(AdditionalFields) !contains "PUA","MDAV",
ActionType == "AntivirusDetection" and tostring(AdditionalFields) contains "PUA","PUA",
(ActionType startswith "AppControl" or ActionType startswith "AppLocker") and ActionType contains "block","WDAC_AppLocker",
"NonMDE")
| where MDEMeasure != "NonMDE"
// | summarize count() by MDEMeasure, ActionType
</code></pre>

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

<p>It is always a complex task to definitively reply to the question “Did Defender block something?”. With this post, hopefully is has become at least a little bit easier.</p>

<p>Stick around for the next posts, where we will dive deep into looking Windows Event Logs and how to Troubleshoot Performance Issues for MDE.</p>

<p>I hope to see you again.</p>]]></content><author><name>Kostas Koutroumpouchos</name></author><category term="Other" /><summary type="html"><![CDATA[Introduction]]></summary></entry><entry><title type="html">What is Microsoft Defender for Endpoint, for the Endpoint - Getting to know MDE Part 1</title><link href="https://kostaskoutrou.github.io/2025/12/17/what-is-mde.html" rel="alternate" type="text/html" title="What is Microsoft Defender for Endpoint, for the Endpoint - Getting to know MDE Part 1" /><published>2025-12-17T00:00:00+00:00</published><updated>2025-12-17T00:00:00+00:00</updated><id>https://kostaskoutrou.github.io/2025/12/17/what-is-mde</id><content type="html" xml:base="https://kostaskoutrou.github.io/2025/12/17/what-is-mde.html"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>What is MDE, for the Endpoint? I always wondered what is going on on the device after MDE is installed on it and it gets onboarded to MDE. There are so many capabilities that MDE and Defender Antivirus (MDAV) provides, that it is difficult to know what is happening in the background when something is blocked or the performance of the machine is degraded when running a specific process.</p>

<p>In this series about MDE named “Getting to know MDE”, I will try to gather and simplify all the concepts needed to know when running MDE on an endpoint. The purpose of the series is to have a better understanding of what exactly is being run on a machine onboarded to MDE, what is being checked/scanned and how, and how to approach a troubleshooting session effectively in case that it is needed.</p>

<blockquote>
  <p><strong>Many of us have heard “is it Defender?” when a machine is performing poorly or an application is not running as expected. This series is an attempt to have an assistive cheatsheet in answering quickly and effectively.</strong></p>
</blockquote>

<p>In this Blog post, the focus is on what the capabilities of MDE are, when it is running on an endpoint. The capabilities are explained on a high level, without going into many details, just to get an understanding of what each capability does. In the next posts, more details of each capability will be provided, and what to look for when troubleshooting them.</p>

<p>All the information is based on official documentation of Microsoft, which you can find <a href="https://learn.microsoft.com/en-us/defender-endpoint/microsoft-defender-endpoint">here</a>.</p>

<p>So let’s get started.</p>

<hr />

<h2 id="contents">Contents</h2>

<ul id="markdown-toc">
  <li><a href="#introduction" id="markdown-toc-introduction">Introduction</a></li>
  <li><a href="#contents" id="markdown-toc-contents">Contents</a></li>
  <li><a href="#mde-components-and-capabilities" id="markdown-toc-mde-components-and-capabilities">MDE Components and Capabilities</a>    <ul>
      <li><a href="#1-attack-surface-reduction-asr" id="markdown-toc-1-attack-surface-reduction-asr">1 Attack Surface Reduction (ASR)</a>        <ul>
          <li><a href="#11-attack-surface-reduction-rules" id="markdown-toc-11-attack-surface-reduction-rules">1.1 Attack Surface Reduction Rules</a></li>
          <li><a href="#12-controlled-folder-access-cfa" id="markdown-toc-12-controlled-folder-access-cfa">1.2 Controlled Folder Access (CFA)</a></li>
          <li><a href="#13-device-control" id="markdown-toc-13-device-control">1.3 Device Control</a></li>
          <li><a href="#14-exploit-protection" id="markdown-toc-14-exploit-protection">1.4 Exploit Protection</a></li>
          <li><a href="#15-web-and-network-protection" id="markdown-toc-15-web-and-network-protection">1.5 Web and Network Protection</a>            <ul>
              <li><a href="#151-web-protection" id="markdown-toc-151-web-protection">1.5.1 Web Protection</a></li>
              <li><a href="#152-network-protection" id="markdown-toc-152-network-protection">1.5.2 Network Protection</a></li>
            </ul>
          </li>
        </ul>
      </li>
      <li><a href="#2-next-generation-protection" id="markdown-toc-2-next-generation-protection">2 Next-Generation Protection</a>        <ul>
          <li><a href="#21-microsoft-defender-antivirus-mdav" id="markdown-toc-21-microsoft-defender-antivirus-mdav">2.1 Microsoft Defender Antivirus (MDAV)</a></li>
          <li><a href="#22-cloud-protection-and-mdav" id="markdown-toc-22-cloud-protection-and-mdav">2.2 Cloud Protection and MDAV</a></li>
          <li><a href="#23-tamper-protection" id="markdown-toc-23-tamper-protection">2.3 Tamper Protection</a></li>
          <li><a href="#24-block-at-first-sight-bafs" id="markdown-toc-24-block-at-first-sight-bafs">2.4 Block at first sight (BAFS)</a></li>
          <li><a href="#25-anti-malware-scan-interface-amsi-integration-with-mdav" id="markdown-toc-25-anti-malware-scan-interface-amsi-integration-with-mdav">2.5 Anti-Malware Scan Interface (AMSI) integration with MDAV</a></li>
          <li><a href="#26-mdav-components-and-technologies" id="markdown-toc-26-mdav-components-and-technologies">2.6 MDAV Components and Technologies</a></li>
          <li><a href="#27-potentially-unwanted-apps-pua" id="markdown-toc-27-potentially-unwanted-apps-pua">2.7 Potentially Unwanted Apps (PUA)</a></li>
          <li><a href="#28-behavioral-blocking-and-containment" id="markdown-toc-28-behavioral-blocking-and-containment">2.8 Behavioral blocking and containment</a></li>
          <li><a href="#29-uefi-scanning-in-mde" id="markdown-toc-29-uefi-scanning-in-mde">2.9 UEFI scanning in MDE</a></li>
          <li><a href="#210-early-launch-antimalware-elam-and-mdav" id="markdown-toc-210-early-launch-antimalware-elam-and-mdav">2.10 Early Launch Antimalware (ELAM) and MDAV</a></li>
        </ul>
      </li>
      <li><a href="#3-scheduled-scans" id="markdown-toc-3-scheduled-scans">3 Scheduled Scans</a></li>
      <li><a href="#4-mdav-modes" id="markdown-toc-4-mdav-modes">4 MDAV Modes</a></li>
    </ul>
  </li>
  <li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
</ul>

<hr />

<h2 id="mde-components-and-capabilities">MDE Components and Capabilities</h2>

<h3 id="1-attack-surface-reduction-asr">1 Attack Surface Reduction (ASR)</h3>

<p>ASR is the <strong>first line</strong> of defense in the world of MDE. ASR, as its name suggests, applies a series of security configurations and protections which aim at <strong>minimizing the potential entry points for attackers</strong>. This is done by blocking different series of actions which are known to be frequently used by attacks, but rarely used for legitimate purposes.</p>

<h4 id="11-attack-surface-reduction-rules">1.1 Attack Surface Reduction Rules</h4>

<p>ASR Rules is one of the protection measures included under ASR. These <strong>protect against common malware behaviors and risky application actions</strong>. The ASR Rules can be separeted in a few different categories based on what protection they provide, and they are the following:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center">Polymorphic threats</th>
      <th style="text-align: center">Lateral movement &amp; credential theft</th>
      <th style="text-align: center">Productivity apps rules</th>
      <th style="text-align: center">Email rules</th>
      <th style="text-align: center">Script rules</th>
      <th style="text-align: center">Misc rules</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center">Block executable files from running unless they meet a prevalence (1,000 machines), age, or trusted list criteria</td>
      <td style="text-align: center">Block process creations originating from PSExec and WMI commands</td>
      <td style="text-align: center">Block Office apps from creating executable content</td>
      <td style="text-align: center">Block executable content from email client and webmail</td>
      <td style="text-align: center">Block execution of potentially obfuscated scripts</td>
      <td style="text-align: center">Block abuse of exploited vulnerable signed drivers</td>
    </tr>
    <tr>
      <td style="text-align: center">Block untrusted and unsigned processes that run from USB</td>
      <td style="text-align: center">Block credential stealing from the Windows local security authority subsystem (lsass.exe)</td>
      <td style="text-align: center">Block Office apps from creating child processes</td>
      <td style="text-align: center">Block only Office communication applications from creating child processes</td>
      <td style="text-align: center">Block JS/VBS from launching downloaded executable content</td>
      <td style="text-align: center">Block webshell creation for servers</td>
    </tr>
    <tr>
      <td style="text-align: center">Use advanced protection against ransomware</td>
      <td style="text-align: center">Block persistence through WMI event subscription</td>
      <td style="text-align: center">Block Office apps from injecting code into other processes</td>
      <td style="text-align: center">Block Office communication apps from creating child processes</td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
    </tr>
    <tr>
      <td style="text-align: center">Block use of copied of impersonated system tools</td>
      <td style="text-align: center">Block rebooting machine in Safe Mode</td>
      <td style="text-align: center">Block Adobe Reader from creating child processes</td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
    </tr>
    <tr>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center">Block Win32 API calls from office macros</td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
      <td style="text-align: center"> </td>
    </tr>
  </tbody>
</table>

<h4 id="12-controlled-folder-access-cfa">1.2 Controlled Folder Access (CFA)</h4>

<p>CFA is a measure that mostly protects against <strong>attacks related to ransomware</strong>.</p>
<ol>
  <li>With CFA essentially you <strong>define a set of directories (folders) and a set of applications</strong>. Only that set of apps is allowed to process in any way the set of directories.</li>
  <li>There is already a <strong>default set of directories and processes</strong> in CFA which can be used immediately.</li>
  <li><strong>Apps are added to the list automatically</strong> when they are highly prevalent in the organization and haven’t displayed any behavior deemed malicious.</li>
  <li>In the <strong>default folder list</strong>, the folders Documents, Pictures, Videos, Videos, Music, and Favorites are included.</li>
</ol>

<h4 id="13-device-control">1.3 Device Control</h4>

<p>Device Control enables <strong>controls related to usage and installation of peripheral (USB/Bluetooth) or other devices</strong> with endpoints. Common scenarios include:</p>
<ul>
  <li><strong>Control access to USB devices</strong>:
    <ul>
      <li>Configure device installation restrictions</li>
      <li>Control access to removable media</li>
    </ul>
  </li>
  <li><strong>BitLocker control</strong>: Block usage of removable media if BitLocker is disabled</li>
  <li>Control access to <strong>printers</strong></li>
  <li><strong>Bluetooth services control</strong>: Allowing advertising, discovery, preparing prompting</li>
</ul>

<h4 id="14-exploit-protection">1.4 Exploit Protection</h4>

<p>Exploit Protection helps <strong>protect against malware that uses exploits</strong> to infect devices and spread. It consists of many <strong>mitigations that can be applied to either the whole OS or individual apps</strong>. It protects by default some behaviors which are correlated to exploits, like an app accessing parts of memory which it should not be able to. The protection measures vary from simple which can be enabled OS-wide and in audit mode to test, all the way to complex protection which protect only per-application and cannot be turned on audit mode but only be turned on immediately.
Exploit Protection provides the following protection measures:</p>
<ol>
  <li><strong>Control flow guard (CFG)</strong>: Mitigates the risk of attackers using memory corruption vulnerabilities by protecting indirect function calls.</li>
  <li><strong>Data Execution Prevention (DEP)</strong>: Prevents memory that wasn’t explicitly allocated as executable from being executed.</li>
  <li><strong>Force randomization for images (Mandatory ASLR)</strong>: With Address Space Layout Randomization (ASLR), every time a program runs, Windows loads its code and libraries at a random memory address, to block attackers predicting where these are located.</li>
  <li><strong>Randomize memory allocations (Bottom-Up ASLR)</strong>: This mitigation requires Mandatory ASLR. This adds entropy to relocations, so their location is randomized.</li>
  <li><strong>Validate exception chains (SEHOP)</strong>: Mitigation against the Structured Exception Handler (SEH) overwrite exploitation technique.</li>
  <li><strong>Validate heap integrity</strong>: This increases the protection level of heap mitigations in Windows, by causing the app to terminate if a heap corruption is detected.</li>
  <li><strong>Arbitrary code guard (ACG)</strong>: Protect an application from executing dynamically generated code. It prevents memory from being marked as executable.</li>
  <li><strong>Block low integrity images</strong>: Prevents the app from loading files that are untrusted, because they’ve been downloaded from the internet from a sandboxed browser.</li>
  <li><strong>Block remote images</strong>: Prevents the application from loading files that are hosted on a remote device, such as UNC share, which could be a device controlled by the attacker.</li>
  <li><strong>Block untrusted fonts</strong>: Mitigates the risk of a flow in font parsing leading to the attacker being able to run code on the device.</li>
  <li><strong>Code integrity guard</strong>: Ensure that all binaries loaded into a process are digitally signed by Microsoft. With the use of WHQL (Windows Hardware Quality Labs) signatures, drivers developed by third parties can be signed to also become trusted.</li>
  <li><strong>Disable extension points</strong>: Windows lets programs hook into or extend other programs in a few ways, for example, by loading DLLs automatically or watching certain system events. These are called extension points. This function disables various extension points for an app, which might be used to establish persistence or elevate privileges of malicious content, thus only loading the program’s own code.</li>
  <li><strong>Disable Win32k system calls</strong>: Win32k.sys is a Windows kernel module that connects apps to the display and input system. Because it is running in kernel mode, and apps can indirectly call its functions, sandboxes/low-privilege apps could exploit it to escape the sandbox and gain admin-level privileges. So, disabling Win32k system calls for an app means that this app doesn’t need to draw windows or interact with the display, so there is no need to communicate with the Windows GUI system at all.</li>
  <li><strong>Don’t allow child processes</strong>: Prevents an app from creating child processes/applications.</li>
  <li><strong>Export address filtering (EAF)</strong>: Mitigates the risk of malicious code looking at the export address table of all loaded modules to find modules that contain useful APIs for their attack.</li>
  <li><strong>Import address filtering (IAF)</strong>: This mitigation protects against modifying the import address table (IAT), potentially redirecting to arbitrary code.</li>
  <li><strong>Simulate execution (SimExec)</strong>: Only for 32-bit apps. It helps validate that calls to sensitive APIs return to legitimate caller functions.</li>
  <li><strong>Validate API invocation (CallerCheck)</strong>: It is a mitigation for return-oriented programming (ROP) techniques that validates that sensitive APIs were called from a valid caller.</li>
  <li><strong>Validate handle usage</strong>: A handle is a reference to a protected object. This measure is a mitigation that helps protect against an attacker using an existing handle to access a protected object.</li>
  <li><strong>Validate image dependency integrity</strong>: Helps protect against attacks that attempt to substitute code for DLLs that are statically linked by Windows binaries.</li>
  <li><strong>Validate stack integrity (StackPivot)</strong>: Helps protect against the Stack Pivot attack, a ROP attack where an attacker creates a fake stack in heap memory, and then tricks the application into returning into the fake stack that controls the flow of execution.</li>
  <li><strong>Hardware-enforced stack protection</strong>: This protects against ROP based attacks. Attackers modify the return address stored on the stack, which tells the CPU where to go back to after finishing a function.</li>
</ol>

<h4 id="15-web-and-network-protection">1.5 Web and Network Protection</h4>

<h5 id="151-web-protection">1.5.1 Web Protection</h5>

<p>Web Protection in MDE includes the following capabilities:</p>
<ol>
  <li><strong>Custom Indicators</strong>: When defined in MDE portal, with the Web Protection feature enabled, the endpoints are blocked when accessing the Blocked Indicators (URLs/IPs).</li>
  <li><strong>Web Threat Protection</strong>: It stops access to phishing, malicious, untrusted, or low-reputation sites.</li>
  <li><strong>Web Content Filtering (WCF)</strong>: This provides the ability to block access of websites based on categories (e.g., gambling, torrenting, cloud storage, etc.).</li>
</ol>

<h5 id="152-network-protection">1.5.2 Network Protection</h5>

<p>Network Protection in MDE:</p>
<ol>
  <li>Protects devices by <strong>preventing connections</strong> to malicious or suspicious sites.</li>
  <li><strong>Expands MS Defender SmartScreen</strong> to block all outbound HTTP(S) traffic to poor-reputation destinations.</li>
  <li><strong>Extends Web Protection</strong>, which works only on Microsoft Edge browser, to the OS level.</li>
  <li>Is a core component to <strong>Web Content Filtering</strong>.</li>
  <li>Provides <strong>visibility</strong> and blocking of IoCs when used with EDR. <strong>Indicators</strong> defined in MDE portal are blocked only if Network Protection is enabled.</li>
</ol>

<h3 id="2-next-generation-protection">2 Next-Generation Protection</h3>

<p>Under Next-Gen Protection is where most of the more “advanced” protection mechanisms reside to block emerging threats.</p>

<h4 id="21-microsoft-defender-antivirus-mdav">2.1 Microsoft Defender Antivirus (MDAV)</h4>

<p>Here is where the famous MDAV lives. With it, process creation events and file download events from the internet are monitored. It not only uses its signature-based engine, but also predictive technologies such as machine learning and cloud-delivered protection to find attacks. If working offline, the latest dynamic intelligence from the Intelligence Security Graph is provisioned regularly throughout the day.</p>

<h4 id="22-cloud-protection-and-mdav">2.2 Cloud Protection and MDAV</h4>

<p>To identify new threats dynamically, Next-Gen Protection technologies work with</p>
<ol>
  <li>AI systems which use machine learning models</li>
  <li>Large sets of interconnected data in the Microsoft Intelligent Security Graph.</li>
</ol>

<p>MDAV works with Microsoft Cloud services, also known as Microsoft Advanced Protection Service (MAPS). With these, next-gen technologies provide quick identification of new threats. This is done by MDAV uploading samples of metadata or the samples themselves to allow Cloud protection to identify if the samples are malicious.</p>

<h4 id="23-tamper-protection">2.3 Tamper Protection</h4>

<p>When Tamper Protection is <strong>enabled</strong> on a machine, the following MDAV setting cannot be changed by anyone, <strong>not even by applying a new GPO or a setting in Intune</strong>:</p>

<ul>
  <li>Virus and threat protection remains enabled.</li>
  <li>Real-time protection remains turned on.</li>
  <li>Behavior monitoring remains turned on.</li>
  <li>Antivirus protection, including IOfficeAntivirus (IOAV) remains enabled.</li>
  <li>Cloud protection remains enabled.</li>
  <li>Security intelligence updates occur.</li>
  <li>Automatic actions are taken on detected threats.</li>
  <li>Notifications are visible in the Windows Security app on Windows devices.</li>
  <li>Archived files are scanned.</li>
  <li>Exclusions can’t be modified or added</li>
</ul>

<h4 id="24-block-at-first-sight-bafs">2.4 Block at first sight (BAFS)</h4>

<p>BAFS is a simple feature in MDE. When enabled, if a file or executable is never seen before by MDE and it is identified as suspicious, <strong>the opening action or the execution of it is blocked</strong> until a verdict is received from the cloud-delivered protection.</p>

<h4 id="25-anti-malware-scan-interface-amsi-integration-with-mdav">2.5 Anti-Malware Scan Interface (AMSI) integration with MDAV</h4>

<p>AMSI provides the capability to <strong>inspect PowerShell and other scripts</strong>, even if there is obfuscation applied on them. MDE utilizes AMSI to protect against fileless malware, dynamic script-based attacks, and other threats of this nature.</p>

<h4 id="26-mdav-components-and-technologies">2.6 MDAV Components and Technologies</h4>

<p>MDAV utilizes <strong>multiples engines</strong> to be able to detect and prevent a wide range of threats and attacker techniques. The following diagram shows the different engines used:</p>

<p><img alt="image" src="https://github.com/user-attachments/assets/05ee9ded-66da-4284-b3e6-07a945c83178" />
<!--
<img width="975" height="401" alt="image" src="https://github.com/user-attachments/assets/05ee9ded-66da-4284-b3e6-07a945c83178" />
-->
As shown above, there is a balance between engines running locally on the client which provide real time protection, and engines being provided via cloud-delivered protection which handle the heavy load when needed. With this graph the necessity of the cloud-delivered protection is also highlighted, as a machine gets a plethora of additional capabilities when it is enabled.</p>

<p>In the following table, the engines are described briefly:</p>

<table>
  <thead>
    <tr>
      <th>On the Client</th>
      <th>In the Cloud</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>ML engine</strong>: Lightweight ML engine. It has specialized models for specific file types commonly abused, like PE files, PowerShell, macros, JS, PDF, etc.</td>
      <td><strong>Metadata-based ML engine</strong>: Specialized ML models analyze a featurized description of suspicious files sent by the client. Stacked ensemble classifiers combine results to make a verdict. You can read more on how it works in <a href="https://www.microsoft.com/security/blog/2019/07/25/new-machine-learning-model-sifts-through-the-good-to-unearth-the-bad-in-evasive-malware/">this Microsoft blogpost</a>.</td>
    </tr>
    <tr>
      <td><strong>Behavior monitoring engine</strong>: Monitors for potential attacks post-execution. It observes process behaviors, including behavior sequence at runtime, to identify and block activities based on predetermined rules.</td>
      <td><strong>Behavior-based ML engine</strong>: Suspicious behavior sequences are used to trigger to analyze the process tree behavior using ML models. Monitored attack techniques span the attack chain, like exploits, elevation, persistence, lateral movement, and exfiltration.</td>
    </tr>
    <tr>
      <td><strong>Memory scanning engine</strong>: Scans memory space used by a running process to expose malicious behavior that could be hiding with code obfuscation.</td>
      <td><strong>AMSI-paired ML engine</strong>: Client-side and cloud-side pairs analyze scripts behavior pre and post execution to detect threats like fileless and in-memory attacks.</td>
    </tr>
    <tr>
      <td><strong>AMSI integration engine</strong>: Enables detection of files and in-memory attacks, defeating code obfuscation. This blocks malicious behavior of scripts client-side.</td>
      <td><strong>File classification ML engine</strong>: Multi-class, deep neural network classifiers examine full file contents. Suspicious files are held from running and submitted to the cloud protection service for classification.</td>
    </tr>
    <tr>
      <td><strong>Heuristics engine</strong>: Rules identify file characteristics that have similarities with known malicious characteristics to catch new threats or modifications of known ones.</td>
      <td><strong>Detonation-based ML engine</strong>: Suspicious files are detonated in a sandbox. Deep learning classifiers analyze the observed behavior to block attacks.</td>
    </tr>
    <tr>
      <td><strong>Emulation engine</strong>: Dynamically unpacks malware and examine how they would behave at runtime. Checks both during runtime and the memory content after, finding malware packers and polymorphic malware.</td>
      <td><strong>Reputation ML engine</strong>: Reputation sources and models from all Microsoft are queried to block threats linked to malicious/suspicious URLs, domains, emails, and files. Sources include SmartScreen, MDO, and others through the Microsoft Intelligent Security Graph.</td>
    </tr>
    <tr>
      <td><strong>Network engine</strong>: Network activity is inspected.</td>
      <td><strong>Smart rules engine</strong>: Smart rules identify threats based on researcher expertise and collective knowledge of threats.</td>
    </tr>
    <tr>
      <td><strong>CommandLine Scanning engine</strong>: Scans command lines of all processes before they execute.</td>
      <td><strong>CommandLine ML engine</strong>: ML models scan suspicious command lines in the cloud.</td>
    </tr>
  </tbody>
</table>

<h4 id="27-potentially-unwanted-apps-pua">2.7 Potentially Unwanted Apps (PUA)</h4>

<p><strong>PUA is a category of software that can</strong>:</p>

<ul>
  <li>Cause the machine to run slowly</li>
  <li>Display unexpected apps</li>
  <li>Install other software that might be unwanted</li>
</ul>

<p><strong>PUA could be</strong>:</p>

<ul>
  <li>Advertising software</li>
  <li>Bundled software that offers to install additional software not related to the original one, and could be not signed by the same entity as the original one</li>
  <li>Software that evades detection</li>
</ul>

<p><strong>PUA could be a problem because</strong>:</p>

<ul>
  <li>Increase the risk of the endpoints being infected with actual malware</li>
  <li>Make malware harder to identify</li>
  <li>Cost time and effort to clean up</li>
</ul>

<p>Both MDAV and SmartScreen provide PUA protection.</p>

<h4 id="28-behavioral-blocking-and-containment">2.8 Behavioral blocking and containment</h4>

<p>This is a feature based on AI/ML to target fileless malware, polymorphic threats, and human-operated attacks. It detects attacks based on their behaviors and process trees. This feature utilizes the following:</p>

<ul>
  <li><strong>Next-Gen Protection</strong> detects threats by analyzing <strong>behaviors</strong>.</li>
  <li><strong>EDR</strong> received <strong>signals</strong> across your network, devices, and kernel behavior, creating alerts and incidents.</li>
  <li><strong>MDE</strong> has <strong>visibility</strong> in email, data, apps, network, endpoint, and kernel behavior <strong>signals</strong> received through EDR. <strong>MDE correlates these signals</strong>, and raises alerts and incidents.</li>
</ul>

<p><strong>Components of Behavioral blocking and containment:</strong></p>

<ul>
  <li><strong>ASR Rules</strong> prevent predefines <strong>common attack behaviors</strong>.</li>
  <li><strong>Client behavioral blocking</strong>: As suspicious behaviors are detected on devices by MDAV, artifacts such as files or apps are blocked, checked, and remediated automatically. When suspicious behaviors are detected, MDAV sends them and their process tress to the cloud protection service to determine their maliciousness. If detected, it is blocked on the device.</li>
  <li><strong>Feedback-loop blocking</strong>, AKA Rapid Protection: With it, when a suspicious behavior or file is detection, information about it is sent to multiple classifiers. The rapid protection loop engine inspects and correlated the info with other signals to identify if to block the file. This results in blocking malware on a device, other devices in the org, and devices in other orgs. So there is this community approach where you will get threat intel from other orgs based on detections.</li>
</ul>

<h4 id="29-uefi-scanning-in-mde">2.9 UEFI scanning in MDE</h4>

<p>MDE now has a UEFI scanner. This helps in attempts of attackers compromosing the boot flow to achieve low-level malware behavior that is hard to detect.</p>

<p>Windows Defender System Guard combats this with hardware-based security features including hypervisor-level attestation and Secure Launch (aka Dynamic Root of Trust (DRTM)).</p>

<p><strong>With the new UEFI scanner, firmware scanning becomes broadly available</strong>. It is built-in with MDAV. It gives MDE the ability to scan inside the firmware file system and perform security assessments. It performs dynamic analysis on the firmware it gets from the hardware flash storage. By obtaining the firmware, the scanner is able to parse the firmware, enabling MDE to inspect firmware content at runtime.</p>

<h4 id="210-early-launch-antimalware-elam-and-mdav">2.10 Early Launch Antimalware (ELAM) and MDAV</h4>

<p><strong>ELAM combats early boot threats</strong> (e.g., rootkits, or malicious drivers that can hide from detection) by using a <strong>driver named Wdboot.sys that starts before other boot-start drivers</strong>. ELAM enables the evaluation of other drivers, and helps the Windows kernel decide whether those drivers should be initialized.</p>

<h3 id="3-scheduled-scans">3 Scheduled Scans</h3>

<p>There are three types of scans in MDAV:</p>

<ul>
  <li><strong>Quick Scan</strong>: Scan the locations with high probability of having malware registered to start with the system, including registry keys and known Windows startup folders.</li>
  <li><strong>Full Scan</strong>: Start with a Quick Scan, and then scan all mounted fixed disks and removable and network drives. A Full Scan could take a few hours or days to complete.</li>
  <li><strong>Custom Scan</strong>: Scan what is provided in input.</li>
</ul>

<p>In most cases, the recommended approach is to execute only one Full Scan per endpoint, and then only run Quick Scans, because them together with Real-Time Protection being enabled covers all files in an endpoint.</p>

<h3 id="4-mdav-modes">4 MDAV Modes</h3>

<p>MDAV could be operating in different modes, depending on whether there is another AV solution installed on the endpoint, or based on the policies and security settings applied on the endpoint:</p>

<ul>
  <li><strong>Active Mode</strong>:
    <ol>
      <li>MDAV is the <strong>main</strong> AV solution on the machine.</li>
      <li>Files are scanned, and actions are taken, i.e., threats are actively remediated.</li>
    </ol>
  </li>
  <li><strong>Passive Mode</strong>:
    <ol>
      <li>MDAV is <strong><em>not</em></strong> the main AV solution on the machine.</li>
      <li>While files are still scanned, actions are <strong>not</strong> taken, i.e., threats are not remediated.</li>
      <li>Updates should still be applied, as they improve alerting and performance(!).</li>
    </ol>
  </li>
  <li><strong>Endpoint Detection and Response (EDR) in Block Mode</strong>:
    <ol>
      <li>This is an <strong>enhancement to Passive Mode</strong>, where actions are taken in malicious artifacts detected by EDR capabilites. Actions are taken on post-breach, behavioral EDR detections, which were not detected by the installed AV solution.</li>
      <li>Several features are still <strong>not</strong> available (Active Mode is required):</li>
    </ol>
    <ul>
      <li>Real-Time Protection</li>
      <li>Network Protection</li>
      <li>ASR</li>
      <li>Indicators</li>
    </ul>
  </li>
  <li><strong>Disabled or Uninstalled</strong>:
    <ol>
      <li>MDAV is <strong><em>not</em></strong> the main AV solution on the machine.</li>
      <li>Files are <strong><em>not</em></strong> scanned and actions are <strong><em>not</em></strong> taken.</li>
      <li>It is recommended to have MDAV in Passive Mode for additional alerting, and if the main AV gets removed for any reason (licensing, uninstall, etc.), then MDAV will switch to Active Mode automatically, to keep protecting the machine.</li>
    </ol>
  </li>
</ul>

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

<p>This post got longer that I hoped for, even after cutting quite a bit of details. Hopefully it will help have more spherical knowledge around MDE when it is running on an endpoint.</p>

<p>I hope to see you back in the next posts.</p>]]></content><author><name>Kostas Koutroumpouchos</name></author><category term="Other" /><summary type="html"><![CDATA[Introduction]]></summary></entry></feed>