Are You Aware of Your Monitoring Gaps?

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.

On this page

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.

That is not a criticism. Every large-scale security platform makes similar decisions. Understanding where those trade-offs live is what matters.

If you do not understand the limits of your visibility, you do not understand the limits of your security.

The DeviceFileEvents table

Within the Microsoft security stack, the DeviceFileEvents table is commonly used during investigations. The official documentation describes it as containing:

"information about file creation, modification, and other file system events."

At a high level, file system activity can be broken down into:

  • File access (open)
  • File reads (data being viewed or copied)
  • File changes (writes and modifications)
  • File renames or moves
  • File deletions
  • Folder browsing (discovery)

Where the documentation gets quiet

The documentation explicitly references file creation and modification. It is less explicit about two things that matter for exfiltration detection:

  • Which action types are captured (reads, deletes, renames) and which are not
  • Which file extensions produce events and which are silently dropped

That raises a useful question.

To what extent does the available telemetry represent the full lifecycle of file activity?

A small experiment

To explore this, I ran a simple test on a managed endpoint enrolled in Microsoft Defender for Endpoint.

Setup

  • 100 files created on the system in a single directory.
  • 20 files using common extensions (documents, images, scripts).
  • 80 files using higher-value extensions (databases, backups, archives, configuration files, key material).
  • Each file held 50 bytes of data.
  • After a 60 second pause, every file was read sequentially, then moved into a staging subfolder, then deleted.
  • Every file system operation was timestamped and logged locally so the test output could be compared against the telemetry surfaced in DeviceFileEvents.

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:

# -------------------------------
# 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"

What I observed

Pulling the events for the experiment folder out of DeviceFileEvents:

DeviceFileEvents
| where FolderPath contains @"C:\Users\VictorOlsson\Documents\file-ops-experiment"

And the distinct list of extensions that surfaced at all, across any action type:

DeviceFileEvents
| where FolderPath contains @"C:\Users\VictorOlsson\Documents\file-ops-experiment"
| extend FileExtension = extract("\\.([0-9A-Za-z]+)(?:[\\?#]|$)", 1, FileName)
| distinct FileExtension
100 Files written
31 Extensions with any telemetry
0 Read events surfaced

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 DeviceFileEvents, even though every ReadAllBytes and every Remove-Item 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.

Per-extension result for all 100 files
#ExtensionLogged
1txtNo
2logNo
3csvYes
4jsonNo
5xmlNo
6htmlYes
7jsNo
8cssYes
9pdfYes
10docxYes
11xlsxNo
12pptxYes
13pngNo
14jpgNo
15gifYes
16mp3No
17mp4No
18zipYes
19ps1Yes
20batYes
21dbNo
22sqliteYes
23sqlite3No
24mdbNo
25accdbNo
26bakNo
27backupNo
28dumpNo
29dmpYes
30sqlNo
31ldfNo
32mdfYes
33ndfNo
34dbfNo
35frmNo
36ibdNo
37mydNo
38myiNo
39oraNo
40rdbNo
41pstNo
42ostNo
43emlYes
44msgNo
45mboxNo
46kdbxNo
47pfxYes
48pemYes
49keyYes
50crtYes
51cerYes
52derYes
53csrNo
54pubNo
55ppkNo
56envNo
57configYes
58confNo
59iniNo
60yamlNo
61ymlNo
62tomlNo
63propertiesNo
64regYes
65datNo
66binNo
67blobNo
68cacheNo
69tmpNo
70tempNo
71tarYes
72gzYes
73bz2Yes
747zYes
75rarYes
76isoYes
77imgYes
78vhdNo
79vhdxNo
80qcow2Yes
81parquetNo
82avroNo
83orcNo
84featherNo
85h5No
86savNo
87dtaNo
88rdataNo
89matNo
90npyNo
91pickleNo
92joblibNo
93ptNo
94pthNo
95ckptNo
96vmdkNo
97ovaNo
98p12Yes
99jksNo
100ovpnNo

Important context

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.

The same run, seen by ZeroExfil

The machine was also running ZeroExfil, so I queried the same folder in our hunting interface:

where filePath contains "file-ops-experiment"

ZeroExfil surfaced all four action types from the script, from the same powershell_ise.exe process tree:

4 / 4 Phases observed
97 Files per phase (of 100)
4850 B Bytes accounted for in writes

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: 68.cache, 69.tmp, and 70.temp. 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.

Side by side, on the same machine and the same run:

PhaseActionMDEZeroExfil
CreateWRITE31 of 100 extensions97 of 100 files
ReadREADNot surfaced97 of 100 files
MoveRENAME31 of 100 extensions97 of 100 files
DeleteDELETENot surfaced97 of 100 files

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.

What to do with this

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 DeviceFileEvents, 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.

Final thought

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.

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.

Curious to see more?

If this experiment raised questions about your own environment, I am happy to walk you through how ZeroExfil approaches the same problem. Book a 30 minute chat.

Book a demo