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
stagingsubfolder, 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
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
| # | Extension | Logged |
|---|---|---|
| 1 | txt | No |
| 2 | log | No |
| 3 | csv | Yes |
| 4 | json | No |
| 5 | xml | No |
| 6 | html | Yes |
| 7 | js | No |
| 8 | css | Yes |
| 9 | pdf | Yes |
| 10 | docx | Yes |
| 11 | xlsx | No |
| 12 | pptx | Yes |
| 13 | png | No |
| 14 | jpg | No |
| 15 | gif | Yes |
| 16 | mp3 | No |
| 17 | mp4 | No |
| 18 | zip | Yes |
| 19 | ps1 | Yes |
| 20 | bat | Yes |
| 21 | db | No |
| 22 | sqlite | Yes |
| 23 | sqlite3 | No |
| 24 | mdb | No |
| 25 | accdb | No |
| 26 | bak | No |
| 27 | backup | No |
| 28 | dump | No |
| 29 | dmp | Yes |
| 30 | sql | No |
| 31 | ldf | No |
| 32 | mdf | Yes |
| 33 | ndf | No |
| 34 | dbf | No |
| 35 | frm | No |
| 36 | ibd | No |
| 37 | myd | No |
| 38 | myi | No |
| 39 | ora | No |
| 40 | rdb | No |
| 41 | pst | No |
| 42 | ost | No |
| 43 | eml | Yes |
| 44 | msg | No |
| 45 | mbox | No |
| 46 | kdbx | No |
| 47 | pfx | Yes |
| 48 | pem | Yes |
| 49 | key | Yes |
| 50 | crt | Yes |
| 51 | cer | Yes |
| 52 | der | Yes |
| 53 | csr | No |
| 54 | pub | No |
| 55 | ppk | No |
| 56 | env | No |
| 57 | config | Yes |
| 58 | conf | No |
| 59 | ini | No |
| 60 | yaml | No |
| 61 | yml | No |
| 62 | toml | No |
| 63 | properties | No |
| 64 | reg | Yes |
| 65 | dat | No |
| 66 | bin | No |
| 67 | blob | No |
| 68 | cache | No |
| 69 | tmp | No |
| 70 | temp | No |
| 71 | tar | Yes |
| 72 | gz | Yes |
| 73 | bz2 | Yes |
| 74 | 7z | Yes |
| 75 | rar | Yes |
| 76 | iso | Yes |
| 77 | img | Yes |
| 78 | vhd | No |
| 79 | vhdx | No |
| 80 | qcow2 | Yes |
| 81 | parquet | No |
| 82 | avro | No |
| 83 | orc | No |
| 84 | feather | No |
| 85 | h5 | No |
| 86 | sav | No |
| 87 | dta | No |
| 88 | rdata | No |
| 89 | mat | No |
| 90 | npy | No |
| 91 | pickle | No |
| 92 | joblib | No |
| 93 | pt | No |
| 94 | pth | No |
| 95 | ckpt | No |
| 96 | vmdk | No |
| 97 | ova | No |
| 98 | p12 | Yes |
| 99 | jks | No |
| 100 | ovpn | No |
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:
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:
| Phase | Action | MDE | ZeroExfil |
|---|---|---|---|
| Create | WRITE | 31 of 100 extensions | 97 of 100 files |
| Read | READ | Not surfaced | 97 of 100 files |
| Move | RENAME | 31 of 100 extensions | 97 of 100 files |
| Delete | DELETE | Not surfaced | 97 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.