File Integrity Monitoring for Compliance with CBFS Filter
File integrity monitoring (FIM) answers a narrower question than general auditing: not just who touched a file, but whether its content actually changed, and whether that change was expected. It is a control named explicitly by several compliance frameworks, including the logging and monitoring expectations discussed in Supporting an ISO/IEC 27001 ISMS with Callback Technologies and the audit-trail material in Building GDPR-Aligned Applications with Callback Technologies. This article covers a concrete implementation using CBFilter: detecting that a monitored file was written to, hashing its content once the write is complete, and comparing that hash against a known-good baseline.
Tracking Which Files Were Actually Written
Add a filter rule for the directory you need to monitor, requesting both the write and close events:
filter.AddFilterRule(@"C:\ProtectedData\*.*", Constants.ACCESS_NONE,
Constants.FS_CE_AFTER_WRITE | Constants.FS_CE_AFTER_CLOSE, Constants.FS_NE_NONE);
AfterCloseFile does not indicate whether the file that is being closed was modified during that open session, so you cannot rely on it alone to decide whether a re-check is warranted. Instead, set a flag in HandleContext from the AfterWriteFile handler, and read that flag back when the corresponding AfterCloseFile event fires for the same handle. This is the same context-passing pattern described in Creating a Replacement for FileSystemWatcher, applied here to avoid hashing files that were only opened for reading.
``` void AfterWriteFile(object sender, CBFilterAfterWriteFileEventArgs e) { // Mark this handle as dirty so the AfterCloseFile handler knows // to recompute the hash once the file is fully closed. e.HandleContext = MarkDirty(e.HandleContext); }
void AfterCloseFile(object sender, CBFilterAfterCloseFileEventArgs e) { if (!WasMarkedDirty(e.HandleContext)) return;
string newHash = ComputeSha256(e.FileName); string baselineHash = LookUpBaseline(e.FileName);
if (baselineHash != null && newHash != baselineHash) { RaiseIntegrityAlert(e.FileName, baselineHash, newHash); }
UpdateBaseline(e.FileName, newHash); } ```
ComputeSha256 above is ordinary application code, not a CBFilter method: by the time AfterCloseFile fires, the OS-level close has already completed, so the file can be opened and hashed with standard file I/O and a standard SHA256 implementation, without needing a direct handle from CBFilter. This keeps the hashing logic simple, at the cost of doing a full read of the file on every close that followed a write. For very large files that are written to frequently, that full re-read may be worth optimizing away by hashing incrementally as data is written, using the buffer available in AfterWriteFile's parameters; that adds real complexity, however, since writes are not guaranteed to be sequential or non-overlapping, and an incremental hash has to account for a write that lands in the middle of previously hashed content. Start with the straightforward re-hash-at-close approach and only move to incremental hashing if profiling shows it is actually needed.
Deciding What Counts as a Baseline Violation
Whether a hash mismatch should raise an alert or simply update the baseline silently depends on your policy, not on anything CBFilter can determine for you. A file that is supposed to change only through a specific, known application (a configuration file your own service writes, for example) can be handled by comparing the identity of the writing process, using GetOriginatorProcessName as shown in Blocking Data Exfiltration to USB Drives with CBFS Filter, against an allow list before deciding whether the new hash is an expected update or a violation to flag. A file that should never change at all after creation should treat every mismatch as a violation.
Tracking Renames
A file that is renamed keeps its content and its hash, but a baseline keyed purely by path will lose track of it. If your monitored set includes files that may be renamed or moved, add FS_CE_AFTER_RENAME to the rule and update the baseline's key rather than treating the new name as an unbaselined file, following the same rename-handling logic described in Creating a Replacement for FileSystemWatcher.
Where the Baseline Lives
This article does not prescribe where to store the baseline itself; a local file, a database, or a remote service are all reasonable choices depending on the rest of your application's architecture. Whatever you choose, keep in mind that the baseline store is now part of your trust boundary: if it can be modified by anything other than the code that maintains it, an attacker who can also modify the protected files can defeat the comparison by updating both.
What This Does and Does Not Cover
This technique detects content changes made through the filesystem on the machine where the filter is running. It does not see changes made to the same data by a different machine over a protocol this filter instance is not attached to (a shared backend modified from another server, for instance), and it does not protect against a change made before the filter was installed or while it was stopped. For the broader audit trail this kind of monitoring is often paired with, see the Notify* event guidance in Tracking and Controlling Access to Network Resources with CBFS Filter and the identity-focused examples in Restricting File Access by User or Group with CBFS Filter.
Getting Started with CBFilter
You can find an evaluation version of the SDK for your platform and programming language in the Download Center. After downloading and installing the SDK, you will receive a library, sample code, and comprehensive documentation on your system. The documentation includes a "Getting Started" section with programming instructions. Additionally, free technical support is available during the evaluation phase.
We appreciate your feedback. If you have any questions, comments, or suggestions about this article please contact our support team at support@callback.com.