A Windows-native background service that monitors the NTFS USN Change Journal
in real time, filters filesystem activity against a user-defined whitelist, and
records forensic metadata to a dedicated Windows Event Log (FileSystem). It is
designed for endpoint auditing, change tracking, and lightweight host forensics.
The service is headless, survives reboots, runs under LocalSystem, and
self-manages its own rolling log archives so it can run unattended for long
periods without filling the disk.
See full FEATURES LIST
USN Journal -> read record -> resolve parent directory by file-reference
-> O(1) whitelist match -> classify by reason -> Windows Event
- Opens the volume and queries (or creates) the USN journal.
- Negotiates the richest read format the volume supports (V2 on NTFS, V3 on ReFS).
- For each change, resolves the containing directory's path (cached) and checks it against the whitelist in constant time.
- Classifies the change by its USN reason flags and writes a structured event with a category-specific Event ID.
- Windows (NTFS volume; ReFS supported via V3 records)
- Python 3.12+ (64-bit)
pywin32:pip install pywin32- Administrator /
LocalSystemprivileges (USN reads andClear-EventLogrequire elevation) - .NET Framework 4.x present (its
EventLogMessages.dllis used so events render a readable description; this ships with virtually all modern Windows installs)
:: 1. Install dependencies
pip install pywin32
:: 2. Configure which directories to monitor (launches the GUI)
python usn_monitor.py
:: 3. Test in the foreground (Ctrl+C to stop)
python usn_monitor.py debug
:: 4. Install and start as a Windows service
python usn_monitor.py --startup delayed install
python usn_monitor.py startEach change is classified by its dominant USN reason flag (reasons accumulate into a single close-record, so the most significant one wins). The full reason string is always preserved in the event body.
| Event ID | Category | Triggered by |
|---|---|---|
| 100 | Create | FileCreate |
| 101 | Modify | DataOverwrite, DataExtend, DataTruncation, NamedDataOverwrite, StreamChange |
| 102 | Delete | FileDelete |
| 103 | Rename | RenameOld, RenameNew |
| 104 | SecurityChange | ACL / ownership change (SecurityChange) |
| 105 | Other | IndexableChange, BasicInfoChange, HardLinkChange |
| 106 | RangeChange | USN_RECORD_V4 range-tracking (ReFS only) |
Classification priority: Delete > Create > Rename > Security > Modify > Other.
A file created and deleted before its close-record is reported as Delete (the
net on-disk effect).
Events are written to the FileSystem custom log, found in Event Viewer under
Applications and Services Logs → FileSystem.
# All deletions
Get-WinEvent -FilterHashtable @{LogName='FileSystem'; Id=102}
# Creates and deletes together
Get-WinEvent -FilterHashtable @{LogName='FileSystem'; Id=100,102}
# Everything in the last hour, newest first
Get-WinEvent -FilterHashtable @{LogName='FileSystem'; StartTime=(Get-Date).AddHours(-1)}Note: USN close-records accumulate every reason since the file was opened, so a routine "save" usually arrives as one Create/Modify event rather than one per write. This is the intended forensic granularity.
Each event carries a full human-readable field block as {PARAM[1]}, plus six
high-value fields as separate insertion strings for column extraction in Event
Log Explorer (or any tool using positional EventData). Extract a field as a
custom column with {PARAM[n]}, using the index below.
{PARAM[1]}is the completeKey: valueblock (all 16 fields), so the description tab stays fully readable. The six broken-out fields begin at{PARAM[2]}. These indices are part of the event field contract (SchemaVersion) — a MAJOR schema bump is required if they ever change.
| Index | Field | Notes |
|---|---|---|
{PARAM[1]} |
Full block | All 16 fields as Key: value lines (readable description + EvtxECmd regex source) |
{PARAM[2]} |
Hostname | Source host name |
{PARAM[3]} |
TargetFilename | Full path (mirrors Sysmon — correlation key) |
{PARAM[4]} |
UtcTime | YYYY-MM-DD HH:MM:SS.fff (mirrors Sysmon — correlation key) |
{PARAM[5]} |
MachineGuid | Stable machine identifier |
{PARAM[6]} |
SourceIP | IP address(es) at service start |
{PARAM[7]} |
VolumeSerial | NTFS volume serial (XXXX-XXXX) |
The remaining fields (SchemaVersion, Category, Reason, Usn, JournalId, FQDN,
Domain, MachineSID, MAC, OSBuild) are not broken out as separate {PARAM[n]}
strings — they live inside the {PARAM[1]} block. Extract them there (Event Log
Explorer supports regex against the description), or use the EvtxECmd maps, which
pull every field from the block by regex into Timeline Explorer columns.
Two tools, two access methods (by design):
- Event Log Explorer reads the raw insertion-strings array → use
{PARAM[n]}above for the six broken-out fields. - EvtxECmd → Timeline Explorer renders to a single
Datablob → the bundled maps (maps/) regex-extract all fields intoPayloadData1–6+Computer.
Note: these EventData elements are positional and unnamed (a limitation of
classic event reporting), so {EventData\Usn}-style named lookups do not
work. Named-field access requires the instrumentation-manifest build (planned),
which would also let every field become its own named column.
Run python usn_monitor.py with no arguments to open the configuration GUI:
- The tree lazily loads directories (no hang on large drives).
- Double-click a folder to recursively add (or remove) it and all of its sub-directories to the whitelist. Recursive walks run off the UI thread, so the GUI never freezes.
[X]= monitored,[ ]= not monitored.- Use the search box to jump to a folder by name.
- Click Save Config (JSON) to write
monitor_config.json.
By default, C:\Windows and all sub-directories are monitored; everything else is
excluded.
The config file lives next to usn_monitor.py. The service reads it once at
startup — restart the service after any change.
{
"paths": [
"C:\\Windows",
"C:\\Windows\\System32",
"C:\\Windows\\Temp"
],
"rotation_size_gb": 3.5,
"max_storage_gb": 60.0
}| Key | Meaning |
|---|---|
paths |
Monitored directories (recursive selections include every subdir) |
rotation_size_gb |
Rotate the live log when it reaches this size (GB) |
max_storage_gb |
Total archive directory cap; oldest archives deleted FIFO (GB) |
Changing the JSON does not affect a running service. Apply with:
python usn_monitor.py restart
- Live log:
FileSystem.evtx(the customFileSystemWindows Event Log). - Rotation triggers (whichever comes first):
- The live log reaches
rotation_size_gb(default 3.5 GB), or - The calendar reaches the 1st of the month at 01:00.
- The live log reaches
- Archive naming:
FileSystem_<Month>_<Year>_<counter>.evtx(e.g.FileSystem_June_2026_1.evtx); the counter resets each month. - Archive location:
C:\FileSystem_Archives - Storage cap: when the archive directory exceeds
max_storage_gb(default 60 GB), the oldest.evtxfiles are deleted first (FIFO) until the directory is back under the cap.
Rotation is performed with Clear-EventLog -Backup, which atomically archives and
clears the live log.
python usn_monitor.py --startup delayed install :: install (delayed auto-start)
python usn_monitor.py start :: start
python usn_monitor.py stop :: stop
python usn_monitor.py restart :: stop + start (reloads config)
python usn_monitor.py update :: re-register after editing the .py
python usn_monitor.py remove :: uninstall
sc query USNMonitorService :: check status- Use
--startup autoinstead ofdelayedto start as early as possible at boot. - Run
update(not reinstall) after editing the source; the SCM caches the script path.
The service writes its own operational log (errors, startup info, rotation activity) to:
C:\FileSystem_Archives\usn_monitor.log
For deep troubleshooting, enable a periodic counter summary (records seen /
resolve failures / whitelist matches / events emitted) by setting the
USN_VERBOSE environment variable to 1, true, yes, or on.
:: Console (inherits your shell environment)
set USN_VERBOSE=1
python usn_monitor.py debug
:: Service (must be machine-wide, then restart so the SCM picks it up)
setx USN_VERBOSE 1 /m
python usn_monitor.py restartA verbose stats line looks like:
stats: seen=48 (v2=48 v3=0 v4=0) resolve_fail=0 no_match=3 emitted=45 last_resolve_err=None
no_match counts changes outside the whitelist (expected to be large — it is the
whole-volume churn you are not capturing).
See DEPLOYMENT.md (if provided) for Group Policy / Active Directory and packaged-executable options. In brief:
- With Python on targets: copy
usn_monitor.py+monitor_config.jsonto each host, then run the install/start commands (e.g. via a GPO startup script or remote management tool). - Without Python on targets (recommended at scale): freeze to a standalone executable with PyInstaller and deploy that — no Python runtime required on endpoints.
- Push a standard
monitor_config.jsonto all hosts so they share one whitelist and retention policy.
- Requires elevation; under the default
LocalSystemaccount this is automatic. - The
FileSystemlog and event source are auto-registered on first run. - On very high-I/O volumes with a broad whitelist, the live log grows quickly — the 3.5 GB / 60 GB rolling policy is doing real work; tune it for your retention needs.
- Path resolution is cached by directory; rename/delete events bypass the cache to avoid stale paths.
Found at LICENSE