File Integrity Monitoring is an important control that can be implemented to enhance your enterprise’s/asset’s security posture in terms of detective capability. The end goal is for analysts/admins to receive alerts when critical/sensitive files are changed on systems. These can range from application configuration files to operating system configuration files to even source code if necessary. FIMs are also a key control in certain compliance standards and best practices frameworks.
There are a variety of companies that will provide FIM as a product or as a service, however, in this article I will be walking through writing a simple solution using Python to understand what’s actually happening behind the scenes.
The first steps in implementing an FIM solution is to figure out your security policy on what files to monitor on which machines, how often alerts should be sent out and to form a process around remediation in case of a security incident.
The Technical Idea:
The concept of FIM is quite simple, however provides powerful detective capabilities: Establish a baseline of your files across systems, periodically check for changes to those files, alert if necessary. Ideally, any new files added in monitored directories should have baselines created on the fly and alerts should be sent out on the same. The same concept can be applied to monitoring critical API Endpoints for changes by periodically recording and checking for change in the responses of those endpoints.
My implementation is focused on a client agent which would send updates periodically to a central server running MySQL.
- Baselining new files on the fly in monitored directories
- Detection for file deletion
- Detection for file content changes through hash variance
- Detection of file permission changes
- Indexed by hostname and file-path
- File size thresholding
- Chunk size parameter in case of file size threshold
- Email alerting — Elastics=Search Storage and alerting is pretty straightforward to implement and is a recommended future change.
The config file I have implemented (using the library: configparser to parse) requires:
- Path to the monitor-list — a file containing the paths to the files/directories you want to monitor.
- SQL Connection String Details — the password is hardcoded here (avoid doing this unless you are sure you can lock down permissions for the file and can ensure that this process runs under specialised permissions, not that easy to do within large environments) — you could further implement environment variables on each host to store passwords OR use a password vault solution.
- File size threshold — what happens when you try reading massive files into memory? This would heavily impact system performance and might cause application/OS crashes — more so since this whole pipeline runs periodically. Here we mention the maximum file size that we can read at once into RAM — let’s say 2MB.
- Chunk size: If one of the files in the monitor list has a size greater than the file size threshold (2MB in the example above), we will instead read the file in chunks of “chunk_size”, let’s say 1MB. The file would be read in chunks and continually hashed.
- SMTP Email alerting configuration details — host, port, address, password, enforce_tls. Once again, avoid hard-coding passwords into your config files, use a vault solution, environment variables or service email accounts with restrictions/rules on incoming and outgoing communication.
Entry-point and Scheduling:
Pretty simple stuff, we read the config file, read the ‘monitorlist’ parameter which points to the file containing the paths to monitor and read the content into mon_list split by newlines. We then iterate over this list, glob recursively if the entry is a directory and end up with a list of absolute file paths.
We then call computeBaselines() — which takes the config object and the list of paths.
Computing Baselines, + hashing after the initial baseline
We iterate over the list of file-paths and for each of them, we get the size using stat. If this size is greater than the file_size_threshold in our config file, we read the file in chunks of chunk_size-also mentioned in the config file, and update the hash. If the size was lesser than file_size_threshold, we read the entire file and hash it in a single iteration. This function returns a list of dictionaries containing the file-path and the SHA256 hash of its content.
Computing Metadata and Additional Details
We drop back into main and call getAdditionalInfo() — passing the list of dicts returned above. We iterate over each dict and compute the permissions, access time, modified time, change time and i-node number — using stat, and the hostname of the machine using sockets.
Storing Baselines and Detecting Changes
This function forms the meat of the project —
To check for file deletions, we look at what has already been baselined in the DB. The filepaths present for this host in the DB are matched up to the filepaths read in from the monitor list. If there exists a filepath that has a baseline in the DB but is not found on the host, this could indicate a change in the monitoring list or deletion of that file. This is added to the ‘comments’ in the baseline and an alert is sent out for the same.
To detect new file additions — is sort of the inverse of the above. If there exists a file in the monitor list expansion that does not have a baseline in the DB, this is probably a newly added file in a monitored directory. The baseline is computed and stored. An alert is sent out for the same.
If the baseline does exist, we compare the hash value and permission notations of the file on the host with those in the baseline. Any changes in these indicate file content changes and/or permission changes. The hash and comments are updated in the baseline and alerts are sent out on the same.
DB Storage — Insertion and Updates to the baselines
Boilerplate INSERT/UPDATE queries.
Once again, pretty straightforward stuff — we use the parameters in the config file to establish a connection with an SMTP server, authenticate and send our alerts through. You can change to/from lists through the config as well.
The Database Table Schema
Results and Working
At this point, we create a file within testfiles/testdir/ called b.txt, and let it baseline, similar to the stuff seen above.
Through all of the above, we have formed the basis of a change management system for remote host filesystems. Conceptually and programatically, the implementation is quite simple but when dealing with larger environments, the deployment and maintenance of these agents becomes an issue. Many providers choose to instead push out maintenance patches/new code and schedule the execution of these agents on remote hosts through SNMP — which is more fitting for larger environments.
Thank you for your time.