<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>ZeroExfil Blog</title>
    <link>https://www.zeroexfil.com/blog/</link>
    <description>Research, findings, and practical guidance to help organizations and individuals stay secure, alongside updates from the ZeroExfil platform.</description>
    <language>en-us</language>
    <copyright>Copyright 2026 Olsson Security</copyright>
    <lastBuildDate>Sun, 03 May 2026 08:00:00 GMT</lastBuildDate>
    <atom:link href="https://www.zeroexfil.com/blog/rss.xml" rel="self" type="application/rss+xml" />
    <image>
      <url>https://www.zeroexfil.com/images/Logo_Original.png</url>
      <title>ZeroExfil Blog</title>
      <link>https://www.zeroexfil.com/blog/</link>
    </image>

    <item>
      <title>The SMB Security Gap: What Comes After Antivirus</title>
      <link>https://www.zeroexfil.com/blog/posts/smb-security-after-antivirus/</link>
      <guid isPermaLink="true">https://www.zeroexfil.com/blog/posts/smb-security-after-antivirus/</guid>
      <pubDate>Sun, 03 May 2026 08:00:00 GMT</pubDate>
      <dc:creator>Victor Olsson</dc:creator>
      <category>Industry Analysis</category>
      <description><![CDATA[CrowdStrike's State of SMB Cybersecurity Survey shows that small businesses know the risks but cannot afford or operate enterprise security. 94% are knowledgeable about threats, 83% have a plan, but only 42% provide regular training and breach rates are identical between companies with and without a plan. 66% cite cost as the top obstacle to upgrading. Antivirus is the floor, a SOC is out of reach, and the layer that belongs in between is what we built ZeroExfil to be.]]></description>
    </item>

    <item>
      <title>The SAP CAP npm Compromise: 1,969 Marker Repos, 41 Actual Victims</title>
      <link>https://www.zeroexfil.com/blog/posts/sap-npm-supply-chain-credential-sweep/</link>
      <guid isPermaLink="true">https://www.zeroexfil.com/blog/posts/sap-npm-supply-chain-credential-sweep/</guid>
      <pubDate>Fri, 01 May 2026 08:00:00 GMT</pubDate>
      <dc:creator>Victor Olsson</dc:creator>
      <category>Threat Research</category>
      <description><![CDATA[Four official SAP CAP packages were trojanised on npm to sweep developer credentials. The GitHub search for the marker description returns about 2,400 hits, which sounds like a mass-compromise event. Verifying every one and tracing them back to GitHub accounts gives a very different picture: 41 distinct victims, almost all SAP CAP developers and consultants. Includes the verification scripts, the file-system fingerprint of the malware, and a tool-neutral detection pattern.]]></description>
    </item>

    <item>
      <title>Can You Trust What Your AI Coding Agent Is Doing?</title>
      <link>https://www.zeroexfil.com/blog/posts/ai-tool-file-access-visibility/</link>
      <guid isPermaLink="true">https://www.zeroexfil.com/blog/posts/ai-tool-file-access-visibility/</guid>
      <pubDate>Tue, 21 Apr 2026 08:00:00 GMT</pubDate>
      <dc:creator>Victor Olsson</dc:creator>
      <category>AI Security</category>
      <description><![CDATA[AI coding agents run in your user context and can read files across your machine. A simple test shows how ZeroExfil's kernel-level monitoring catches every read, independent of what the AI's chat interface reports.]]></description>
      <content:encoded><![CDATA[<header class="post-header">
          <div class="post-meta">
            <span class="post-tag">AI Security</span>
            <span class="sep" aria-hidden="true"></span>
            <time datetime="2026-04-21">April 21, 2026</time>
            <span class="sep" aria-hidden="true"></span>
            <span>7 min read</span>
          </div>
          <h1>Can You Trust What Your AI Coding Agent Is Doing?</h1>
          <p class="post-lede">AI coding agents run in your user context and can read files anywhere you can. A quick test with ZeroExfil running in the background shows exactly what that looks like at the kernel level, and why the chat UI alone is not a sufficient audit trail.</p>
          <div class="post-author">
            <span class="author-by">Written by</span>
            <span class="author-name">Victor Olsson</span>
          </div>
        </header>

        <details class="toc-mobile">
          <summary>On this page</summary>
          <!-- TOC injected by blog.js -->
        </details>

        <div class="post-content">
          <p>AI coding agents have become a standard part of the development workflow. Tools like GitHub Copilot integrate directly into your editor and can read files, run terminal commands, and interact with your file system on your behalf. They run in your user context, which for most developers means they can read almost anything you can.</p>

          <p>That is useful when you ask the agent to fix a failing test or refactor a function. It gets more interesting when you stop to ask who else might be able to influence what it reads, and what it does with whatever it finds.</p>

          <blockquote>
            <p>Most software runs in your user context too, but its behavior is predictable. A calculator does arithmetic. An AI coding agent runs with the same privileges but with no fixed scope: the next action depends on the prompt.</p>
          </blockquote>

          <h2>The test</h2>

          <p>To see what that looks like in practice, I set up a simple test on my own machine: a file called <code>secret</code> sitting outside any project folder, and a normal Copilot chat session in VS Code. The question was straightforward. What, if anything, stops the agent from reading it, and what do I see when it does?</p>

          <p>Before doing anything, I asked Copilot directly what would stop it from reading a file outside the project. Its own answer is the most useful framing for this whole post:</p>

          <figure class="post-figure">
            <img src="https://www.zeroexfil.com/blog/posts/ai-tool-file-access-visibility/copilot-asking-for-read_file-permission.png" alt="GitHub Copilot in VS Code answering the question 'what would stop you from reading a file outside of the project context?' It explains that nothing in the tools enforces a project boundary, that read_file accepts absolute paths, that there is no sandbox, and that the only real protection is OS file permissions plus its own restraint." loading="lazy">
            <figcaption>Copilot's own answer. The tools accept absolute paths, there is no sandbox, and the only real protection is OS file permissions plus the agent's own restraint. A confirmation prompt does appear for files outside the workspace, visible at the bottom of the screenshot.</figcaption>
          </figure>

          <p>That confirmation prompt is worth pausing on. To VS Code's credit, when Copilot tried to access a file outside the workspace folder, the editor asked permission first. The dialog clearly named the file (<code>secret</code>) and the directory it was in (<code>Videos</code>), with an Allow Once button and a Skip option. This is a real guardrail, and most users will see it before any out-of-workspace read happens.</p>

          <p>I clicked Allow Once. The read proceeded immediately:</p>

          <figure class="post-figure">
            <img src="https://www.zeroexfil.com/blog/posts/ai-tool-file-access-visibility/copilot-read-secret-after-confirmation.png" alt="After clicking Allow Once, Copilot reads the file C:\Users\VictorOlsson\Videos\secret and displays its 20-byte contents, 'super secret content', inline in the chat panel." loading="lazy">
            <figcaption>After granting permission, the read happened and the file contents appeared inline in the chat.</figcaption>
          </figure>

          <p>So far, this is the system working as designed. The user asked, the editor confirmed, the user approved, the agent read the file. Where it gets interesting is in the cases where that confirmation step does not save you.</p>

          <p>The Allow prompt protects you from one thing: an AI that wanders off on its own. It does not protect you when the request looks reasonable. If you are debugging something and the agent asks to read a file you do not recognise, most people will click Allow. That moment is also when an injected instruction would take effect, and you would not know. The prompt only works if you read every dialog carefully, every time, for hours. Most people don't.</p>

          <p>It also assumes the prompt is shown at all. The same dialog has an option to approve the action for the rest of the session. Once you click that, future reads in the same location happen silently. Copilot also lets you change the approval mode at the top level:</p>

          <figure class="post-figure">
            <img src="https://www.zeroexfil.com/blog/posts/ai-tool-file-access-visibility/approvals.png" alt="GitHub Copilot's approval mode menu in VS Code, showing three options: Default Approvals (Copilot uses your configured settings), Bypass Approvals (all tool calls are auto-approved), and Autopilot Preview (autonomously iterates from start to finish)." loading="lazy">
            <figcaption>Copilot's approval modes. Bypass Approvals reduces the per-tool-call confirmation step. Autopilot iterates from start to finish without intervening prompts.</figcaption>
          </figure>

          <p>Bypass Approvals reduces the per-tool-call confirmation step. Whether it also turns off the out-of-workspace warning depends on configuration. Either way, the pattern is clear: every option above the default trades a guardrail for less friction. None of these are exotic settings. They exist because the prompts get in the way during long tasks, and people turn them off. The further you move from the default, the less the chat UI tells you what is actually happening.</p>

          <p>And Copilot is one of the better cases. Other AI coding tools offer weaker guardrails, or none at all. Whether your tool asks before reading a file depends on how that tool was built. If your monitoring strategy depends on an in-app prompt, you are assuming the prompt exists, that it is on, and that the user reads and rejects every dubious one. None of those are safe assumptions.</p>

          <h2>What the chat UI shows you</h2>

          <p>VS Code makes tool calls visible in the conversation. When the agent read the file, the invocation and its parameters were displayed inline. This is genuine transparency about what the tool layer did, and the Allow prompt adds an explicit consent step on top of that.</p>

          <p>The limitation is that the chat interface only shows what the tool layer reports about itself, and the consent prompt only protects against actions the user notices and rejects. In a prompt injection scenario, the chat log would show the read, but the user may have already clicked Allow because it looked relevant to the task. The chat UI is useful, but it is not an independent audit trail.</p>

          <h2>What ZeroExfil saw</h2>

          <p>For context, ZeroExfil is an endpoint agent that observes file activity at the kernel level. It captures reads, writes, renames, and deletes on the system, along with the process and account responsible. It runs independently of the applications it observes, so its view does not depend on what those applications choose to report.</p>

          <p>The same file read that appeared in the Copilot chat also produced an event in ZeroExfil's telemetry:</p>

          <div class="data-table-wrap" open>
            <div class="table-scroll">
              <table class="data-table">
                <thead>
                  <tr><th scope="col">Field</th><th scope="col">Value</th></tr>
                </thead>
                <tbody>
                  <tr><td><code>TimeGenerated</code></td><td>2026-04-21 21:26:53</td></tr>
                  <tr><td><code>accountName</code></td><td>AzureAD\VictorOlsson</td></tr>
                  <tr><td><code>eventType</code></td><td>READ</td></tr>
                  <tr><td><code>processName</code></td><td>Code.exe</td></tr>
                  <tr><td><code>filePath</code></td><td>C:\Users\VictorOlsson\Videos\secret</td></tr>
                  <tr><td><code>processPath</code></td><td>C:\Users\VictorOlsson\AppData\Local\Programs\Microsoft VS Code\Code.exe</td></tr>
                  <tr><td><code>pid</code></td><td>25472</td></tr>
                  <tr><td><code>bytesTransferred</code></td><td>20</td></tr>
                  <tr><td><code>isAggregated</code></td><td>false</td></tr>
                </tbody>
              </table>
            </div>
          </div>

          <p>The event captures the full picture: the exact file path, the process responsible (<code>Code.exe</code>), the account context, and the exact number of bytes transferred. The file contains 20 bytes, and ZeroExfil reported 20 bytes transferred. The read left a verifiable trace that is entirely independent of what the chat interface reported.</p>

          <div class="callout">
            <p class="callout-label">Key distinction</p>
            <p>The chat UI shows what the tool reported. ZeroExfil shows what the kernel recorded. Most of the time these agree. You need the second source for the times they don't.</p>
          </div>

          <h2>Why this is worth paying attention to</h2>

          <p>The scenario above was deliberate: I asked, I confirmed, I saw the result. The real risk is not an AI honestly reading a file you asked it to read. It is an AI being manipulated into reading files it should not.</p>

          <h3>Prompt injection, briefly</h3>

          <p><a href="https://owasp.org/www-community/attacks/PromptInjection" target="_blank" rel="noopener">Prompt injection</a> is a known attack class against AI systems. An attacker hides instructions inside content the AI is asked to process: a source file, a code comment, a document being summarised. Those instructions redirect the agent. It might be told to read credential files, private keys, or browser data, and put the contents in its response. From there, the data flows out through the chat interface or whatever API is consuming the output.</p>

          <p>The AI is not deceiving you. It is doing what its tools allow, based on instructions it found in the content. The gap is that you did not write those instructions.</p>

          <h3>Why it matters on a developer machine</h3>

          <p>VS Code Copilot, Cursor, and similar tools are used in environments where API keys, database credentials, and certificate stores sit on the same machine as the development workspace. The attack surface is real, not theoretical.</p>

          <h3>It has already happened in the wild</h3>

          <p>GitHub's own security team documented this in a <a href="https://github.blog/security/vulnerability-research/safeguarding-vs-code-against-prompt-injections/" target="_blank" rel="noopener">2025 write-up on safeguarding VS Code against prompt injections</a>. They describe several bypasses where an instruction hidden inside a public GitHub Issue caused Copilot to read a local credential file and exfiltrate the token to an attacker-controlled server. No confirmation prompt was shown. A separate bypass used the agent's file editing tool to overwrite <code>settings.json</code>, which led to arbitrary command execution.</p>

          <p>Those specific issues have since been fixed. The underlying point holds: the consent layer lives inside the same tool that is being attacked. When that layer is bypassed, you only know about it if something independent was also watching.</p>

          <h2>What to look for</h2>

          <p>If you are thinking about this from a monitoring perspective, the process of interest is <code>Code.exe</code> for VS Code-based agents, with equivalents for other editors. AI agents do not just read. They also write generated code, rename during refactors, and delete temporary files. The full lifecycle matters. Useful questions to ask of your telemetry:</p>

          <ul>
            <li>Is <code>Code.exe</code> reading or writing files outside the active workspace folder?</li>
            <li>Is it accessing credential stores, private key directories, or <code>.env</code> files?</li>
            <li>Are there reads, renames, or deletes against paths under <code>%APPDATA%</code>, <code>%USERPROFILE%</code>, or network shares that have no obvious relation to the current project?</li>
            <li>Is there a pattern of reads followed by outbound network activity from the same process?</li>
          </ul>

          <p>None of those patterns are individually definitive. They are starting points for investigation. What matters is having the events in the first place, across the full lifecycle, with enough detail to make the investigation possible.</p>

          <h2>What ZeroExfil gives you here</h2>

          <p>The event above came from ZeroExfil's kernel-level monitoring. It surfaced the read, attributed it to the correct process, and recorded the bytes transferred, regardless of whether the AI's chat interface confirmed it or not. The read happened to be the interesting event in this test, but the same telemetry covers writes, renames, and deletes. That is the same property that made it useful in the <a href="https://www.zeroexfil.com/blog/posts/monitoring-gaps-defender-file-events/">previous post on Defender telemetry gaps</a>: the full lifecycle of a file surfaces at the kernel, not just at the tool layer.</p>

          <p>For AI tools specifically, this gives you a way to answer the question independently: what did <code>Code.exe</code> actually do to your files during this session, and against which paths? That answer comes from the OS, not from the AI.</p>

          <h2>Final thought</h2>

          <p>This is not an argument against using AI coding agents. They are genuinely useful, and broad file access is part of what makes them capable. It is an argument for having visibility into what that access looks like in practice.</p>

          <p>If your AI agent read a file outside your project today, would you know? Would you have a record of which file, how many bytes, and which process was responsible?</p>

          <p>With ZeroExfil, the answer is yes.</p>
        </div>

        <div class="article-cta">
          <h3>See it on your own endpoints</h3>
          <p>If this raised questions about your own setup, run the same kind of test with us. Try ZeroExfil free for two weeks on up to 10 endpoints, with full visibility into reads, writes, renames, and deletes from AI tools and any other process on the machine. No card, no commitment.</p>
          <a href="https://www.zeroexfil.com/#contact" class="btn-primary">Start a free pilot</a>
        </div>

        <div class="share-block share-block-bottom">
          <p class="share-label">Share this post</p>
          <div class="share-buttons">
            <button type="button" class="share-btn" data-share="copy">Copy link</button>
            <button type="button" class="share-btn" data-share="native">Share</button>
            <a class="share-btn" href="https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.zeroexfil.com%2Fblog%2Fposts%2Fai-tool-file-access-visibility%2F" target="_blank" rel="noopener">LinkedIn</a>
            <a class="share-btn" href="https://x.com/intent/tweet?url=https%3A%2F%2Fwww.zeroexfil.com%2Fblog%2Fposts%2Fai-tool-file-access-visibility%2F&text=Can%20You%20Trust%20What%20Your%20AI%20Coding%20Agent%20Is%20Doing%3F" target="_blank" rel="noopener">X</a>
          </div>
        </div>]]></content:encoded>
    </item>

    <item>
      <title>Are You Aware of Your Monitoring Gaps?</title>
      <link>https://www.zeroexfil.com/blog/posts/monitoring-gaps-defender-file-events%2F</link>
      <guid isPermaLink="true">https://www.zeroexfil.com/blog/posts/monitoring-gaps-defender-file-events%2F</guid>
      <pubDate>Mon, 20 Apr 2026 08:00:00 GMT</pubDate>
      <dc:creator>Victor Olsson</dc:creator>
      <category>Security Monitoring</category>
      <description><![CDATA[A practical look at what Microsoft Defender's DeviceFileEvents table reliably surfaces, what it does not, and why that matters for detecting data exfiltration.]]></description>
      <content:encoded><![CDATA[<header class="post-header">
          <div class="post-meta">
            <span class="post-tag">Security Monitoring</span>
            <span class="sep" aria-hidden="true"></span>
            <time datetime="2026-04-20">April 20, 2026</time>
            <span class="sep" aria-hidden="true"></span>
            <span>6 min read</span>
          </div>
          <h1>Are You Aware of Your Monitoring Gaps?</h1>
          <p class="post-lede">Microsoft Defender is a powerful platform. Like any system operating at scale, it makes trade-offs about which telemetry it captures. I ran a small experiment to see what file activity actually surfaces in DeviceFileEvents, and what that means for detecting exfiltration.</p>
          <div class="post-author">
            <span class="author-by">Written by</span>
            <span class="author-name">Victor Olsson</span>
          </div>
        </header>

        <details class="toc-mobile">
          <summary>On this page</summary>
        </details>

        <div class="post-content">
          <p>Microsoft Defender is a solution I have spent countless hours working with. It is powerful and highly capable, but like any system operating at fleet scale, it is built around trade-offs. Not all events can be captured or retained equally.</p>

          <p>That is not a criticism. Every large-scale security platform makes similar decisions. Understanding where those trade-offs live is what matters.</p>

          <blockquote>
            <p>If you do not understand the limits of your visibility, you do not understand the limits of your security.</p>
          </blockquote>

          <h2>The DeviceFileEvents table</h2>

          <p>Within the Microsoft security stack, the <code>DeviceFileEvents</code> table is commonly used during investigations. The <a href="https://learn.microsoft.com/en-us/defender-xdr/advanced-hunting-devicefileevents-table" target="_blank" rel="noopener">official documentation</a> describes it as containing:</p>

          <blockquote>
            <p>"information about file creation, modification, and other file system events."</p>
          </blockquote>

          <p>At a high level, file system activity can be broken down into:</p>

          <ul>
            <li>File access (open)</li>
            <li>File reads (data being viewed or copied)</li>
            <li>File changes (writes and modifications)</li>
            <li>File renames or moves</li>
            <li>File deletions</li>
            <li>Folder browsing (discovery)</li>
          </ul>

          <h2>Where the documentation gets quiet</h2>

          <p>The documentation explicitly references file creation and modification. It is less explicit about two things that matter for exfiltration detection:</p>

          <ul>
            <li>Which action types are captured (reads, deletes, renames) and which are not</li>
            <li>Which file extensions produce events and which are silently dropped</li>
          </ul>

          <p>That raises a useful question.</p>

          <blockquote>
            <p>To what extent does the available telemetry represent the full lifecycle of file activity?</p>
          </blockquote>

          <h2>A small experiment</h2>

          <p>To explore this, I ran a simple test on a managed endpoint enrolled in Microsoft Defender for Endpoint.</p>

          <h3>Setup</h3>

          <ul>
            <li>100 files created on the system in a single directory.</li>
            <li>20 files using common extensions (documents, images, scripts).</li>
            <li>80 files using higher-value extensions (databases, backups, archives, configuration files, key material).</li>
            <li>Each file held 50 bytes of data.</li>
            <li>After a 60 second pause, every file was read sequentially, then moved into a <code>staging</code> subfolder, then deleted.</li>
            <li>Every file system operation was timestamped and logged locally so the test output could be compared against the telemetry surfaced in <code>DeviceFileEvents</code>.</li>
          </ul>

          <p>The intent was to mirror the kind of access pattern that a collection or staging step might produce, while exercising the four operation types most relevant to exfiltration: create, read, move, and delete. The full PowerShell used to generate the workload:</p>

<pre data-lang="powershell"><code class="language-powershell"># -------------------------------
# File Operation Logging Experiment
# -------------------------------
# Creates 100 files (20 common + 80 high-value extensions),
# then runs four phases: CREATE, READ, MOVE, DELETE.
# Every operation is timestamped to stdout so the local log
# can be diffed against EDR / DeviceFileEvents telemetry.

$commonExtensions = @(
    "txt","log","csv","json","xml",
    "html","js","css","pdf","docx",
    "xlsx","pptx","png","jpg","gif",
    "mp3","mp4","zip","ps1","bat"
)  # 20 entries

$importantExtensions = @(
    "db","sqlite","sqlite3","mdb","accdb",
    "bak","backup","dump","dmp","sql",
    "ldf","mdf","ndf","dbf","frm",
    "ibd","myd","myi","ora","rdb",
    "pst","ost","eml","msg","mbox",
    "kdbx","pfx","pem","key","crt",
    "cer","der","csr","pub","ppk",
    "env","config","conf","ini","yaml",
    "yml","toml","properties","reg","dat",
    "bin","blob","cache","tmp","temp",
    "tar","gz","bz2","7z","rar",
    "iso","img","vhd","vhdx","qcow2",
    "parquet","avro","orc","feather","h5",
    "sav","dta","rdata","mat","npy",
    "pickle","joblib","pt","pth","ckpt",
    "vmdk","ova","p12","jks","ovpn"
)  # 80 entries

$totalFiles = 100
$dir = "$PWD\file-ops-experiment"
$stagingDir = Join-Path $dir "staging"

if (($commonExtensions.Count + $importantExtensions.Count) -lt $totalFiles) {
    throw "Not enough extensions defined to reach $totalFiles files."
}

New-Item -ItemType Directory -Path $dir -Force | Out-Null

$content = [byte[]](1..50)

function Log($msg) {
    $ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
    Write-Host "[$ts] $msg"
}

Log "Creating $totalFiles files"

# Create common files (20)
for ($i = 0; $i -lt $commonExtensions.Count; $i++) {
    $filename = "$($i + 1).$($commonExtensions[$i])"
    $filepath = Join-Path $dir $filename
    [System.IO.File]::WriteAllBytes($filepath, $content)
    Log "CREATE $filename"
}

# Create remaining files (to reach 100 total)
$remaining = $totalFiles - $commonExtensions.Count

for ($i = 0; $i -lt $remaining; $i++) {
    $index = $i + $commonExtensions.Count + 1
    $filename = "$index.$($importantExtensions[$i])"
    $filepath = Join-Path $dir $filename
    [System.IO.File]::WriteAllBytes($filepath, $content)
    Log "CREATE $filename"
}

Log "Sleeping 60 seconds before read phase"
Start-Sleep -Seconds 60

# Read phase
Log "Starting READ phase"
foreach ($file in Get-ChildItem -Path $dir -File) {
    try {
        [void][System.IO.File]::ReadAllBytes($file.FullName)
        Log "READ $($file.Name)"
    }
    catch {
        Log "READ_FAIL $($file.Name)"
    }
}

# Move phase
Log "Creating staging folder"
New-Item -ItemType Directory -Path $stagingDir -Force | Out-Null

Log "Starting MOVE phase"
Get-ChildItem -Path $dir -File | ForEach-Object {
    Move-Item $_.FullName -Destination $stagingDir -Force
    Log "MOVE $($_.Name)"
}

# Delete phase
Log "Starting DELETE phase"
Get-ChildItem -Path $stagingDir -File | ForEach-Object {
    Remove-Item $_.FullName -Force
    Log "DELETE $($_.Name)"
}

Log "Experiment complete"
</code></pre>

          <h3>What I observed</h3>

          <p>Pulling the events for the experiment folder out of <code>DeviceFileEvents</code>:</p>

<pre data-lang="kql"><code class="language-kql">DeviceFileEvents
| where FolderPath contains @"C:\Users\VictorOlsson\Documents\file-ops-experiment"
</code></pre>

          <p>And the distinct list of extensions that surfaced at all, across any action type:</p>

<pre data-lang="kql"><code class="language-kql">DeviceFileEvents
| where FolderPath contains @"C:\Users\VictorOlsson\Documents\file-ops-experiment"
| extend FileExtension = extract("\\.([0-9A-Za-z]+)(?:[\\?#]|$)", 1, FileName)
| distinct FileExtension
</code></pre>

          <div class="stats-row" role="group" aria-label="Test results summary">
            <div class="stat-item">
              <span class="stat-value">100</span>
              <span class="stat-label">Files written</span>
            </div>
            <div class="stat-item">
              <span class="stat-value">31</span>
              <span class="stat-label">Extensions with any telemetry</span>
            </div>
            <div class="stat-item">
              <span class="stat-value">0</span>
              <span class="stat-label">Read events surfaced</span>
            </div>
          </div>

          <p>Two observations stand out. First, only two action types surfaced: file creation from the create phase, and file rename from the move phase. A same-volume move is implemented as a rename at the kernel level, so both Microsoft Defender and ZeroExfil report the staging step as a rename rather than a separate move event. The read phase and the delete phase produced nothing in <code>DeviceFileEvents</code>, even though every <code>ReadAllBytes</code> and every <code>Remove-Item</code> ran. Second, the create and rename events that did surface only covered a subset of the file types. The same 31 extensions showed up across both phases, and the remaining 69 were silent.</p>

          <details class="data-table-wrap">
            <summary>Per-extension result for all 100 files</summary>
            <div class="table-scroll">
              <table class="data-table">
                <thead>
                  <tr><th scope="col" class="num">#</th><th scope="col">Extension</th><th scope="col">Logged</th></tr>
                </thead>
                <tbody>
                  <tr><td class="num">1</td><td><code>txt</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">2</td><td><code>log</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">3</td><td><code>csv</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">4</td><td><code>json</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">5</td><td><code>xml</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">6</td><td><code>html</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">7</td><td><code>js</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">8</td><td><code>css</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">9</td><td><code>pdf</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">10</td><td><code>docx</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">11</td><td><code>xlsx</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">12</td><td><code>pptx</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">13</td><td><code>png</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">14</td><td><code>jpg</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">15</td><td><code>gif</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">16</td><td><code>mp3</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">17</td><td><code>mp4</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">18</td><td><code>zip</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">19</td><td><code>ps1</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">20</td><td><code>bat</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">21</td><td><code>db</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">22</td><td><code>sqlite</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">23</td><td><code>sqlite3</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">24</td><td><code>mdb</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">25</td><td><code>accdb</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">26</td><td><code>bak</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">27</td><td><code>backup</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">28</td><td><code>dump</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">29</td><td><code>dmp</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">30</td><td><code>sql</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">31</td><td><code>ldf</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">32</td><td><code>mdf</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">33</td><td><code>ndf</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">34</td><td><code>dbf</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">35</td><td><code>frm</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">36</td><td><code>ibd</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">37</td><td><code>myd</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">38</td><td><code>myi</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">39</td><td><code>ora</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">40</td><td><code>rdb</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">41</td><td><code>pst</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">42</td><td><code>ost</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">43</td><td><code>eml</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">44</td><td><code>msg</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">45</td><td><code>mbox</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">46</td><td><code>kdbx</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">47</td><td><code>pfx</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">48</td><td><code>pem</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">49</td><td><code>key</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">50</td><td><code>crt</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">51</td><td><code>cer</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">52</td><td><code>der</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">53</td><td><code>csr</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">54</td><td><code>pub</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">55</td><td><code>ppk</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">56</td><td><code>env</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">57</td><td><code>config</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">58</td><td><code>conf</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">59</td><td><code>ini</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">60</td><td><code>yaml</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">61</td><td><code>yml</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">62</td><td><code>toml</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">63</td><td><code>properties</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">64</td><td><code>reg</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">65</td><td><code>dat</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">66</td><td><code>bin</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">67</td><td><code>blob</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">68</td><td><code>cache</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">69</td><td><code>tmp</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">70</td><td><code>temp</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">71</td><td><code>tar</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">72</td><td><code>gz</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">73</td><td><code>bz2</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">74</td><td><code>7z</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">75</td><td><code>rar</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">76</td><td><code>iso</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">77</td><td><code>img</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">78</td><td><code>vhd</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">79</td><td><code>vhdx</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">80</td><td><code>qcow2</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">81</td><td><code>parquet</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">82</td><td><code>avro</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">83</td><td><code>orc</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">84</td><td><code>feather</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">85</td><td><code>h5</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">86</td><td><code>sav</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">87</td><td><code>dta</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">88</td><td><code>rdata</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">89</td><td><code>mat</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">90</td><td><code>npy</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">91</td><td><code>pickle</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">92</td><td><code>joblib</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">93</td><td><code>pt</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">94</td><td><code>pth</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">95</td><td><code>ckpt</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">96</td><td><code>vmdk</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">97</td><td><code>ova</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">98</td><td><code>p12</code></td><td><span class="pill yes">Yes</span></td></tr>
                  <tr><td class="num">99</td><td><code>jks</code></td><td><span class="pill no">No</span></td></tr>
                  <tr><td class="num">100</td><td><code>ovpn</code></td><td><span class="pill no">No</span></td></tr>
                </tbody>
              </table>
            </div>
          </details>

          <div class="callout">
            <p class="callout-label">Important context</p>
            <p>The absence of telemetry does not imply absence of activity. Microsoft Defender for Endpoint does not aim to provide complete, lossless event logging. Telemetry is subject to prioritization, filtering, and potential sampling.</p>
          </div>

          <h2>The same run, seen by ZeroExfil</h2>

          <p>The machine was also running ZeroExfil, so I queried the same folder in our hunting interface:</p>

<pre data-lang="kql"><code class="language-kql">where filePath contains "file-ops-experiment"</code></pre>

          <p>ZeroExfil surfaced all four action types from the script, from the same powershell_ise.exe process tree:</p>

          <div class="stats-row" role="group" aria-label="ZeroExfil results summary">
            <div class="stat-item">
              <span class="stat-value">4 / 4</span>
              <span class="stat-label">Phases observed</span>
            </div>
            <div class="stat-item">
              <span class="stat-value">97</span>
              <span class="stat-label">Files per phase (of 100)</span>
            </div>
            <div class="stat-item">
              <span class="stat-value">4850 B</span>
              <span class="stat-label">Bytes accounted for in writes</span>
            </div>
          </div>

          <p>Each file is exactly 50 bytes, so the full write phase should account for 5000 bytes across 100 files. ZeroExfil reported 4850 bytes across 97 files. The read, rename, and delete phases also surfaced 97 of 100 files. Diffing the captured paths against the script's file list, the same three files dropped out of every phase: <code>68.cache</code>, <code>69.tmp</code>, and <code>70.temp</code>. These extensions are deliberately excluded in the collection pipeline to reduce noise from temporary and cache files. The shape of the result is still markedly different from MDE: every phase produced events, every event carried the full file path, and the totals add up to within a few percent of the work the script actually did.</p>

          <p>Side by side, on the same machine and the same run:</p>

          <div class="data-table-wrap" open>
            <div class="table-scroll">
              <table class="data-table">
                <thead>
                  <tr><th scope="col">Phase</th><th scope="col">Action</th><th scope="col">MDE</th><th scope="col">ZeroExfil</th></tr>
                </thead>
                <tbody>
                  <tr><td>Create</td><td><code>WRITE</code></td><td><span class="pill no">31 of 100 extensions</span></td><td><span class="pill yes">97 of 100 files</span></td></tr>
                  <tr><td>Read</td><td><code>READ</code></td><td><span class="pill no">Not surfaced</span></td><td><span class="pill yes">97 of 100 files</span></td></tr>
                  <tr><td>Move</td><td><code>RENAME</code></td><td><span class="pill no">31 of 100 extensions</span></td><td><span class="pill yes">97 of 100 files</span></td></tr>
                  <tr><td>Delete</td><td><code>DELETE</code></td><td><span class="pill no">Not surfaced</span></td><td><span class="pill yes">97 of 100 files</span></td></tr>
                </tbody>
              </table>
            </div>
          </div>

          <p>The difference matters for exfiltration specifically, because exfiltration rarely requires modifying a file. If reads and moves are silent, the sequence that matters most is the one you never see.</p>

          <h2>What to do with this</h2>

          <p>The takeaway is not that one tool is better than another. It is that the shape of your telemetry decides which questions you can answer. If your investigations rely on <code>DeviceFileEvents</code>, it is worth running a similar test in your own environment, mapping which extensions and action types actually surface, and being explicit about the gap between what you can see and what you assume you can see.</p>

          <h2>Final thought</h2>

          <p>I hope this gives a useful starting point. If you run a similar experiment in your own environment and get different numbers, I would genuinely like to hear about it. The more data points we have on what surfaces and what does not, the better the community can reason about detection coverage.</p>

          <p>If nothing else, check whether your telemetry covers the file operations that matter most to you. Knowing where the gaps are is half the work.</p>
        </div>

        <div class="article-cta">
          <h3>See it on your own endpoints</h3>
          <p>If this experiment raised questions about your own environment, run the same one with us. Try ZeroExfil free for two weeks on up to 10 endpoints, no card and no commitment.</p>
          <a href="https://www.zeroexfil.com/#contact" class="btn-primary">Start a free pilot</a>
        </div>

        <div class="share-block share-block-bottom">
          <p class="share-label">Share this post</p>
          <div class="share-buttons">
            <button type="button" class="share-btn" data-share="copy">Copy link</button>
            <button type="button" class="share-btn" data-share="native">Share</button>
            <a class="share-btn" href="https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.zeroexfil.com%2Fblog%2Fposts%2Fmonitoring-gaps-defender-file-events%2F" target="_blank" rel="noopener">LinkedIn</a>
            <a class="share-btn" href="https://x.com/intent/tweet?url=https%3A%2F%2Fwww.zeroexfil.com%2Fblog%2Fposts%2Fmonitoring-gaps-defender-file-events/&text=Are%20You%20Aware%20of%20Your%20Monitoring%20Gaps%3F" target="_blank" rel="noopener">X</a>
          </div>
        </div>]]></content:encoded>
    </item>
  </channel>
</rss>
