Build a File Integrity Monitor

Build a Python tool that detects file tampering using SHA-256 hashing.

Introduction

30 Second Summary

Every time someone breaks into a computer system, one of the first things they do is modify files. They change settings to maintain access, edit logs to cover their tracks, or plant harmful code that runs later.

In this project, you will build a File Integrity Monitor using Python. It takes a cryptographic fingerprint of every file in a folder, then alerts you the moment anything is added, modified, or deleted.

What You'll Build

You run a single command and watch your monitor flag every file an attacker touched, color-coded by severity and mapped to the compliance requirement it satisfies.

By the end of this project, you'll have:

  • A SHA-256 baseline that cryptographically fingerprints every file in a folder, proving at a glance whether anything has been tampered with.
  • A one-command integrity check that reports exactly which files were modified, added, or deleted, showing you what an attacker's activity looks like from a defender's perspective.
  • A timestamped compliance report mapping each finding to PCI DSS 11.5 and NIST 800-53 SI-7, ready to reference in a GRC interview.
  • Secret Mission: Add continuous monitoring that watches your folder in real-time and alerts you the instant a file changes.

Do I need any coding experience?

No. This project walks you through every step, including installing Python and VS Code. You just need a Windows PC.

Before We Start

Before We Start...

Before we dive in, let's lock in what you're about to build and why it matters.

Install Python and VS Code

Before you can build a security tool, you need two things on your machine: a code editor to write your scripts in, and a programming language to run them.

Python is the most common scripting language in cybersecurity. VS Code is a lightweight code editor with a built-in terminal for running commands.

In this step, get ready to:
  • Install VS Code as your code editor.
  • Install Python using the Windows Package Manager.
  • Create your project folder and open it in VS Code.
Install VS Code

VS Code is where you will write all of your Python scripts and run terminal commands throughout this project.

  • Open your web browser and go to https://code.visualstudio.com/download.
  • Click the large Windows download button to download the User Setup installer.
  • Once the download finishes, open the installer file.
  • Follow the prompts, accepting the default options.
  • On the final screen, make sure Launch Visual Studio Code is checked, then click Finish.

VS Code opens in a new window. You should see a Welcome tab.

VS Code did not open?

If you missed the launch checkbox, press the Windows key on your keyboard to open search.

Type Visual Studio Code and press Enter to open it.

Help me troubleshoot VS Code not opening on Windows

Install Python

Python will power your File Integrity Monitor. You will install it using Windows' built-in package manager called winget.

  • In VS Code, click View in the top menu bar.
  • Click Terminal to open the integrated terminal panel at the bottom of the window.
  • Install the Python Install Manager by running this command:
winget install 9NQ7512CXL7T

What does this command do?

This installs the Python Install Manager from the Microsoft Store. The Install Manager handles downloading and setting up Python for you automatically.

First time using winget?

If this is your first time running a winget command, it may ask you to accept source agreements. Type Y and press Enter to accept and continue.

Wait for the installation to complete. You will see a success message when it finishes.

  • Close the terminal by clicking the trash can icon on the right side of the terminal panel.
  • Open a fresh terminal by clicking View then Terminal again.

Why close and reopen the terminal?

The terminal needs to reload so it can find the newly installed Python command. Without reopening, your terminal still uses the old settings from before the install.

What do you think will happen when you run python --version for the first time?

  • Confirm Python is installed by running:
python --version

The Install Manager detects that no Python runtime exists yet and automatically downloads one. After a progress bar, it prints a version number.

✔️ I see Python 3.14

You should see something like:

Python 3.14.6

Python is installed and ready. Any 3.14.x version works for this project.

ⓧ I see an older version

If you see a version lower than 3.14 (for example 3.12 or 3.9), an older Python installation on your machine is taking priority.

  • Open Settings on your PC.
  • Click Apps, then Installed apps.
  • Search for Python and uninstall any older Python versions you find.
  • Close VS Code entirely and reopen it.
  • Open a new terminal and run python --version again.

You should now see Python 3.14.x printed.

Help me fix my Python version

ⓧ Command not found

If you see an error mentioning that python is not recognized, the terminal cannot find Python yet.

  • Close VS Code entirely (not just the terminal, but the whole application).
  • Press the Windows key, type Visual Studio Code, and press Enter to reopen it.
  • Click View then Terminal to open a new terminal.
  • Run python --version again.

If it still does not work, try running the install command again:

winget install 9NQ7512CXL7T

Then close and reopen VS Code one more time before checking the version.

Help me fix python not recognized on Windows

Create your project folder

Every project needs its own folder to keep files organized. You will create a folder on your Desktop so it is easy to find.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir file-integrity-monitor
cd file-integrity-monitor
  • Open this folder in a fresh VS Code window by running:
code .

What does code . do?

The code command launches VS Code from the terminal. The . tells it to open the current folder you are inside.

A new VS Code window opens. You will see a dialog asking if you trust the authors of the files in this folder.

  • Click Yes, I trust the authors.
  • Close the old VS Code window (the one where you ran the commands). Work in the new window from now on.
  • Click View in the top menu bar.
  • Click Terminal to open a terminal in your new project window.

You should see file-integrity-monitor in the VS Code title bar and in the Explorer sidebar on the left. Your terminal prompt also shows the folder name, confirming you are in the right place.

Don't see file-integrity-monitor in the title bar?

Make sure you closed the old window and are working in the new one that opened after code ..

If no new window appeared, click File in the top menu bar, then click Open Folder. Navigate to Desktop and select the file-integrity-monitor folder. Click Select Folder.

Help me open my project folder in VS Code

Your editor and programming language are ready. Next up, you will write your first Python script that computes a SHA-256 hash to fingerprint a file.

Hash Your First File

Your project folder is set up. Before you can monitor a whole folder for unauthorized changes, you need to understand the building block: computing a SHA-256 hash for a single file.

This hash is the cryptographic fingerprint that proves a file has not been tampered with. In this step, you will hash your first file and watch what happens when even a tiny change slips in.

In this step, get ready to:
  • Create a test file to represent sensitive data on a server.
  • Write a Python script that computes a SHA-256 hash for any file.
  • Prove that even a tiny change produces a completely different hash.
Create a test file

Your hashing script needs a file to work with. You will create a simple text file that simulates sensitive data on a server.

  • In VS Code's Explorer sidebar on the left, hover over the file-integrity-monitor folder name.
  • Click the New File... button that appears (a small page icon with a plus sign).
  • Type secret.txt and press Enter.
  • In the editor tab that opens, type This file contains sensitive data.
  • Save the file with Ctrl+S.

You should now see secret.txt listed in the Explorer sidebar.

Write the hashing script

Your hashing script needs to open a file in binary mode, feed its contents to the SHA-256 algorithm in chunks, and return the resulting 64-character fingerprint.

  • Create another new file in the Explorer sidebar by clicking New File... again.
  • Name it hash_file.py and press Enter.
  • Add the following code to hash_file.py:
import hashlib


def hash_file(filepath):
    # Create a new SHA-256 hash object
    sha256 = hashlib.sha256()
    # Open the file in binary mode so we hash raw bytes
    with open(filepath, "rb") as f:
        # Read in 8192-byte chunks to handle large files
        for chunk in iter(lambda: f.read(8192), b""):
            sha256.update(chunk)
    # Return the 64-character hex fingerprint
    return sha256.hexdigest()


if __name__ == "__main__":
    result = hash_file("secret.txt")
    print(f"SHA-256: {result}")

What does this code do?

  • hashlib.sha256() creates a fresh SHA-256 hash object that will accumulate data as you feed it.
  • open(filepath, "rb") opens the file in binary mode. Hashing works on raw bytes, not text characters.
  • iter(lambda: f.read(8192), b"") reads the file in 8192-byte chunks. This lets you hash files of any size without loading the entire thing into memory.
  • sha256.hexdigest() returns the final hash as a 64-character string of hex digits. This is the file's unique fingerprint.
  • Save hash_file.py with Ctrl+S.
  • Run the script in your terminal by running this command:
python hash_file.py

You should see a line starting with SHA-256: followed by a 64-character string of letters and numbers.

What is a SHA-256 hash?

SHA-256 is a one-way cryptographic function. You can turn any file into a fixed-length fingerprint, but you cannot reverse the fingerprint back into the original file.

Enterprise security tools like OSSEC and Tripwire use the same algorithm to detect unauthorized changes on servers. You are building the same core mechanism from scratch.

Seeing an error instead of a hash?

  • If you see FileNotFoundError, make sure secret.txt is saved in the same file-integrity-monitor folder (not inside a subfolder).
  • If you see SyntaxError, check that the f"SHA-256: {result}" line uses an f before the opening quote.
  • help me debug my hash_file.py script

✔️ Awesome, I've got everything!

Great. Double check you have saved hash_file.py and move on to the next substep.

ⓧ I'd like to double check the full code

Here is the complete hash_file.py file. Make sure yours matches exactly:

import hashlib


def hash_file(filepath):
    sha256 = hashlib.sha256()
    with open(filepath, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            sha256.update(chunk)
    return sha256.hexdigest()


if __name__ == "__main__":
    result = hash_file("secret.txt")
    print(f"SHA-256: {result}")
Prove that hashing detects changes

A hash is only useful for security if it reacts to changes. You are about to see the avalanche effect: even the smallest modification to a file produces a completely different fingerprint.

  • Click on secret.txt in the Explorer sidebar to open it.
  • Place your cursor at the very end of the text (after the word "data") and press the spacebar once to add a single space.
  • Save the file with Ctrl+S.

Before you run the script again, predict: will the new hash be slightly different from the first one, or completely different?

  • Run the script again by running this command:
python hash_file.py

The hash is completely different. Not a single character matches the previous output. You added just one space, and the fingerprint changed entirely.

Why does one tiny change produce a totally different hash?

This is called the avalanche effect. Cryptographic hash functions are designed so that any change to the input, no matter how small, flips roughly half the bits in the output.

This property is what makes hashing useful for security. An attacker cannot make a "small" undetectable change. Any tampering produces a completely different fingerprint that is immediately obvious when compared to the baseline.

Both hashes look the same?

  • Make sure you saved secret.txt after adding the space. Look for the dot on the file tab in VS Code, which means unsaved changes.
  • Confirm you actually changed secret.txt (not hash_file.py).
  • help me figure out why my hash is not changing

You now have the core building block of file integrity monitoring: a function that turns any file into a unique fingerprint. Next up, you will scale this to scan an entire folder and save the results as a security baseline.

Build the Baseline Scanner

In the previous step, you proved that SHA-256 hashing works on a single file. You saw how even a tiny change produces a completely different fingerprint.

A single-file hasher is a great proof of concept, but it is not practical for real security. Servers have dozens or hundreds of sensitive files. You need to scan an entire folder, hash every file in it, and save those hashes as a baseline snapshot for later comparison.

In this step, get ready to:
  • Create a folder of sample files that simulate sensitive server files.
  • Build a Python script that scans the folder and records every file's SHA-256 hash.
  • Run the baseline scan and verify the saved fingerprints.
Create test files to monitor

Before your scanner has anything to scan, you need a folder of files that simulate sensitive data on a server. These represent the critical files an attacker might tamper with.

  • Create the monitored folder and sample files by running these commands in your VS Code terminal:
mkdir monitored_files
Set-Content monitored_files\config.txt "Server host = 192.168.1.1"
Set-Content monitored_files\passwords.txt "admin:password123"
Set-Content monitored_files\database.txt "db_connection = localhost:5432"

What do these files represent?

These three files simulate what you would find on a real server. A config file holds server settings. A passwords file stores credentials. A database file contains connection strings. In a real environment, an attacker modifying any of these could gain unauthorized access.

  • Confirm the files were created by running:
dir monitored_files

You should see three files listed: config.txt, passwords.txt, and database.txt.

Not seeing three files?

Make sure your terminal is in the file-integrity-monitor folder. Run cd file-integrity-monitor if you are not, then re-run the commands above.

Help me figure out why my monitored_files folder is empty or missing.

Write the baseline scanner

Now you will build the main script that scans your entire folder, hashes every file, and saves the results. This script is called fim.py (short for File Integrity Monitor).

  • Create a new file called fim.py in VS Code. Add the imports and configuration constants by pasting this code:
import hashlib
import json
import os
import sys
import time


MONITORED_FOLDER = "monitored_files"
BASELINE_FILE = "baseline.json"
REPORT_FILE = "report.txt"

RED = "\033[91m"
YELLOW = "\033[93m"
GREEN = "\033[92m"
CYAN = "\033[96m"
RESET = "\033[0m"

COMPLIANCE_MAPPING = """
Compliance Mapping
==================================================
Control: PCI DSS Requirement 11.5
Purpose: Deploy a change-detection mechanism (e.g., file integrity
monitoring tools) to alert personnel to unauthorized modification
of critical system files, configuration files, or content files.

Control: NIST SP 800-53 SI-7 (Software, Firmware, and Information Integrity)
Purpose: Employ integrity verification tools to detect unauthorized
changes to software, firmware, and information.
==================================================
"""

What does this code do?

The imports bring in Python's standard library modules for hashing, JSON file handling, file system navigation, command-line arguments, and timestamps. No third-party packages needed.

The constants define which folder to monitor, where to save the baseline, and ANSI color codes for formatting terminal output later. The COMPLIANCE_MAPPING string maps your findings to PCI DSS Requirement 11.5 and NIST SP 800-53 SI-7. You will use this in a later step.

  • Below the COMPLIANCE_MAPPING string, add the hashing and folder-scanning functions:
def hash_file(filepath):
    # Compute SHA-256 hash of a single file
    sha256 = hashlib.sha256()
    with open(filepath, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            sha256.update(chunk)
    return sha256.hexdigest()


def scan_folder(folder):
    # Walk through every file in the folder and hash it
    file_hashes = {}
    for dirpath, dirnames, filenames in os.walk(folder):
        for filename in filenames:
            filepath = os.path.join(dirpath, filename)
            file_hashes[filepath] = hash_file(filepath)
    return file_hashes

What does this code do?

The hash_file() function works the same way as the one in hash_file.py. It reads a file in 8192-byte chunks and returns the SHA-256 fingerprint.

The scan_folder() function uses os.walk() to traverse every file inside a folder (including subfolders). It builds a dictionary mapping each file path to its hash. This is your snapshot of the folder's state at a single point in time.

  • Save fim.py and confirm the code loads without errors by running:
python -c "import fim; print(fim.scan_folder('monitored_files'))"

You should see a dictionary printed with three file paths as keys and 64-character hash strings as values.

Seeing a SyntaxError or ImportError?

Check that you saved fim.py before running the command. Also confirm there are no typos in the function definitions. Python is sensitive to indentation, so make sure each line inside a function is indented with 4 spaces.

Help me debug my fim.py import error.

  • Now add the baseline creation function and the command-line entry point below your scan_folder() function:
def create_baseline():
    # Scan the folder and save all hashes to a JSON file
    file_hashes = scan_folder(MONITORED_FOLDER)
    with open(BASELINE_FILE, "w") as f:
        json.dump(file_hashes, f, indent=2)
    print(f"Baseline created: {len(file_hashes)} files recorded in {BASELINE_FILE}")


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python fim.py --baseline | --check | --watch")
        sys.exit(1)

    command = sys.argv[1]

    if command == "--baseline":
        create_baseline()
    else:
        print(f"Unknown command: {command}")
        print("Usage: python fim.py --baseline | --check | --watch")
        sys.exit(1)

What does this code do?

The create_baseline() function calls scan_folder() to get all file hashes, then saves the dictionary to baseline.json using json.dump(). This JSON file becomes your trusted record of what every file looked like at this moment.

The if __name__ == "__main__" block reads the command-line argument (--baseline) so you can run the script from the terminal. Right now only --baseline works. You will add --check and --watch in later steps.

  • Save fim.py.

✔️ Awesome, I've got everything!

Great. Confirm you have saved fim.py with all three sections: the imports and constants at the top, the two functions in the middle, and the if __name__ block at the bottom.

ⓧ I'd like to double check the full code

Here is the complete fim.py file. Scroll through and confirm yours matches:

Section 1 (lines 1-30): Imports, path constants, color constants, and the COMPLIANCE_MAPPING string. Confirm your five imports (hashlib, json, os, sys, time) are at the top, followed by MONITORED_FOLDER, BASELINE_FILE, REPORT_FILE, the five color codes, and the full compliance mapping text.

Section 2 (lines 32-48): The hash_file() and scan_folder() functions. Confirm hash_file reads in 8192-byte chunks and scan_folder uses os.walk() and calls hash_file() on each file.

Section 3 (lines 50-end): The create_baseline() function and the if __name__ == "__main__" block. Confirm create_baseline calls scan_folder(MONITORED_FOLDER) and writes to BASELINE_FILE with json.dump().

Run the baseline scan

Your scanner is complete. Before you run it, think about what you expect to happen. The script should scan your three files, compute their hashes, and save everything to a JSON file.

  • Run the baseline scan by executing:
python fim.py --baseline

You should see this output:

Baseline created: 3 files recorded in baseline.json
  • Open baseline.json in VS Code to inspect the saved fingerprints. Click on the file in the Explorer sidebar.

You should see a JSON object mapping each file path to its 64-character SHA-256 hash. Something like this:

{
  "monitored_files\\config.txt": "a1b2c3...64 characters",
  "monitored_files\\passwords.txt": "d4e5f6...64 characters",
  "monitored_files\\database.txt": "g7h8i9...64 characters"
}

Why save hashes to a file?

The baseline file is your trusted record. It captures what every file looked like at the moment you ran the scan. Later, when you run a comparison check, your tool will re-hash all the files and compare against these saved values. Any difference means something changed.

This is exactly how enterprise file integrity monitoring tools like OSSEC and Tripwire work. They store a known-good baseline and alert when reality drifts from it.

Seeing 'No module named fim' or 0 files recorded?

If you see a module error, make sure your terminal is in the file-integrity-monitor folder (where fim.py lives). Run dir to confirm fim.py and monitored_files are both visible.

If you see 0 files recorded, check that your monitored_files folder is not empty. Run dir monitored_files to verify the three sample files exist inside it.

Help me figure out why my baseline scan isn't working.

Your baseline is locked in. You now have a cryptographic snapshot of every file in the folder. Next up, you will simulate an attacker tampering with those files and build the comparison engine that catches them red-handed.

Detect Tampering

Your baseline is saved. You have a snapshot of every file's SHA-256 fingerprint locked away in baseline.json. But a snapshot that just sits on disk does not protect anything.

The real power comes from comparison. Your tool needs to scan the folder again, compare every current hash against the saved baseline, and tell you exactly what changed. This is the step where your script becomes a real security tool.

In this step, get ready to:
  • Write a check function that compares current file hashes against the saved baseline.
  • Simulate three common attacker actions on your monitored files.
  • Run the check and see your tool catch every change.
Write the comparison logic

The check function loads the saved baseline, scans the folder for its current state, and compares the two dictionaries. Any difference between them is a potential security event.

  • In fim.py, find the create_baseline() function and add the following new function directly below it:
def check_integrity():
    # Check that a baseline exists before comparing
    if not os.path.exists(BASELINE_FILE):
        print("No baseline found. Run with --baseline first.")
        return

    # Load the saved baseline hashes
    with open(BASELINE_FILE, "r") as f:
        baseline = json.load(f)

    # Scan the folder to get current file hashes
    current = scan_folder(MONITORED_FOLDER)
    timestamp = time.strftime("%Y-%m-%d %H:%M:%S")

    # Compare current state against baseline
    modified = []
    added = []
    deleted = []

    for filepath, current_hash in current.items():
        if filepath not in baseline:
            added.append(filepath)
        elif baseline[filepath] != current_hash:
            modified.append(filepath)

    for filepath in baseline:
        if filepath not in current:
            deleted.append(filepath)

What does this code do?

This function loads your saved baseline and scans the monitored folder for its current state. Then it compares the two dictionaries to sort every file into one of three categories.

  • modified catches files whose hash has changed since the baseline was taken.
  • added catches files that exist now but were not in the original baseline.
  • deleted catches files that were in the baseline but no longer exist on disk.
Add the output formatting

Detection is useless if you cannot read the results quickly. Color-coded labels let you spot the severity of each finding at a glance.

  • Still inside the check_integrity() function, add the following code directly below the deleted.append(filepath) line (keep the indentation level the same):
    # Print a formatted header with the scan timestamp
    print(f"\n{'='*50}")
    print(f"  FILE INTEGRITY CHECK - {timestamp}")
    print(f"{'='*50}\n")

    # If no changes found, report all clear
    if not modified and not added and not deleted:
        print(f"{GREEN}No changes detected. All files intact.{RESET}")
        return

    # Print color-coded alerts for each change type
    for filepath in modified:
        print(f"{RED}[MODIFIED] {filepath}{RESET}")

    for filepath in added:
        print(f"{YELLOW}[ADDED]    {filepath}{RESET}")

    for filepath in deleted:
        print(f"{RED}[DELETED]  {filepath}{RESET}")

    # Print a summary count
    total = len(modified) + len(added) + len(deleted)
    print(f"\nSummary: {total} changes detected: {len(modified)} modified, {len(added)} added, {len(deleted)} deleted")

What does this code do?

  • The header prints a timestamp so you know exactly when the check ran.
  • If all hashes match, it prints a green "all clear" message and exits early.
  • ANSI escape codes (the RED, YELLOW, GREEN constants you defined earlier) color the output. Red means something was modified or deleted. Yellow means something new appeared.
  • The summary line gives a quick count so you can see the total scope of changes at a glance.

Now wire the new function into the command-line interface.

  • Scroll down to the if __name__ == "__main__" block at the bottom of fim.py.
  • Find this section:
    if command == "--baseline":
        create_baseline()
    else:
        print(f"Unknown command: {command}")
        print("Usage: python fim.py --baseline | --check | --watch")
        sys.exit(1)
  • Add an elif clause between the --baseline check and the else, so the block now looks like this:
    if command == "--baseline":
        create_baseline()
    elif command == "--check":
        check_integrity()
    else:
        print(f"Unknown command: {command}")
        print("Usage: python fim.py --baseline | --check | --watch")
        sys.exit(1)
  • Save fim.py.
  • Test the check command against an unmodified folder by running:
python fim.py --check

You should see a green message that reads: No changes detected. All files intact.

Seeing an error instead of the all-clear message?

  • If you see "No baseline found", re-run python fim.py --baseline first to regenerate your baseline.
  • If you see a SyntaxError, check that the new check_integrity() function starts at the same indentation level as create_baseline().
  • Make sure the elif command == "--check": line is indented to match the if command == "--baseline": line above it.

Why is my check command not working?

Simulate an attack

Time to think like an attacker. You will perform three actions that mirror what a real intruder does after gaining access to a system.

  • In VS Code's Explorer sidebar, open monitored_files/config.txt.
  • Replace its contents with the following and save the file:
# Attacker was here - modified server config
allow_remote_access = true
disable_logging = true

Why modify a config file?

Attackers frequently modify configuration files to weaken security settings, disable logging (to hide their tracks), or open remote access. This is one of the most common post-compromise actions.

  • Create a new file inside monitored_files called backdoor.txt. Type any content inside it and save.

Why plant a new file?

Attackers drop malicious scripts, web shells, or persistence mechanisms into system directories. A file that was never in the baseline should raise an immediate alert.

  • Delete monitored_files/passwords.txt by right-clicking it in VS Code's Explorer sidebar and selecting Delete.
  • Click Move to Trash when prompted to confirm.

Why delete a file?

Attackers delete log files, credential stores, or evidence of their presence. A file that vanishes from a monitored folder is a strong indicator of compromise.

Your monitored_files folder now contains config.txt (modified), backdoor.txt (new), and database.txt (unchanged). passwords.txt is gone.

Run the integrity check

Before you run the next command, take a moment to predict: how many changes do you think your tool will report, and what labels will they get?

  • Run the check against your tampered folder:
python fim.py --check

You should see a report with three findings: one [MODIFIED] alert for config.txt, one [ADDED] alert for backdoor.txt, and one [DELETED] alert for passwords.txt. The summary line confirms 3 changes detected.

What just happened?

Your tool re-scanned the folder, computed fresh SHA-256 hashes, and compared them against the saved baseline. The modified file produced a completely different hash (the avalanche effect you saw in Step 2). The new file had no entry in the baseline. The deleted file existed in the baseline but was missing from disk.

This is exactly how enterprise file integrity monitoring tools like OSSEC and Tripwire work. They compare a known-good state against the current state and flag any deviation.

Not seeing all three alerts?

  • If you see "No changes detected", your baseline may have been regenerated after you made the changes. Delete baseline.json, undo your file edits, run python fim.py --baseline to recreate it, then re-apply the simulated attack.
  • If config.txt does not show as modified, make sure you saved the file after editing it.
  • If passwords.txt does not show as deleted, check the Explorer sidebar to confirm it is actually gone from the monitored_files folder.

Help me figure out why my integrity check is not detecting all three changes.

✔️ Awesome, I've got everything!

Great. Double check you have saved fim.py and confirm all three alert types printed in your terminal.

ⓧ I'd like to double check the full code

Your fim.py should contain these sections in order. Scroll up and confirm each one is present.

  • Imports: hashlib, json, os, sys, time.
  • Constants: MONITORED_FOLDER, BASELINE_FILE, REPORT_FILE, color codes, and COMPLIANCE_MAPPING.
  • Function hash_file(filepath) that reads a file in chunks and returns its SHA-256 hex digest.
  • Function scan_folder(folder) that walks a directory and returns a dict of file paths to hashes.
  • Function create_baseline() that scans the folder and saves the result to baseline.json.
  • Function check_integrity() that loads the baseline, re-scans, compares, and prints color-coded alerts with a summary.
  • The if __name__ == "__main__" block handling --baseline and --check.

Your tool now catches every type of file tampering an attacker might attempt. Next up, you will turn these raw alerts into a proper compliance-mapped security report that auditors can use as evidence.

Generate a Compliance-Mapped Security Report

Your tool catches every change an attacker makes. In a real organization, that is only half the job.

Auditors and compliance teams need evidence that security controls are actually working. A timestamped report that maps each finding to a specific regulation (like PCI DSS or NIST 800-53) transforms your tool from a script into audit evidence. In this step, you will format your output for readability, link findings to compliance controls, and save everything to a report file.

In this step, get ready to:
  • Restructure terminal output to capture findings for a saved report.
  • Print a compliance mapping section referencing PCI DSS 11.5 and NIST SP 800-53 SI-7.
  • Save the full report to a timestamped report.txt file.
Format the terminal output

Right now your check_integrity() function prints colored alerts directly to the terminal. To save those same findings to a file, you need to capture each alert in a list as you print it.

  • In fim.py, find the check_integrity() function.
  • Locate the line that says if not modified and not added and not deleted: and look below it past the early return.
  • Replace everything from the for filepath in modified: loop down to the end of the function with the following code:
    report_lines = []
    report_lines.append(f"File Integrity Report - {timestamp}")
    report_lines.append("=" * 50)

    for filepath in modified:
        alert = f"[MODIFIED] {filepath}"
        print(f"{RED}{alert}{RESET}")
        report_lines.append(alert)

    for filepath in added:
        alert = f"[ADDED]    {filepath}"
        print(f"{YELLOW}{alert}{RESET}")
        report_lines.append(alert)

    for filepath in deleted:
        alert = f"[DELETED]  {filepath}"
        print(f"{RED}{alert}{RESET}")
        report_lines.append(alert)

    total = len(modified) + len(added) + len(deleted)
    summary = (
        f"\nSummary: {total} changes detected: "
        f"{len(modified)} modified, {len(added)} added, {len(deleted)} deleted"
    )
    print(summary)
    report_lines.append(summary)

What does this code do?

  • report_lines is a list that collects every finding as plain text (no color codes) so it can be written cleanly to a file later.
  • Each loop stores the alert string in a variable called alert, prints it with color for the terminal, and appends the uncolored version to report_lines.
  • The summary line counts all changes and breaks them into categories so you can see the scope of tampering at a glance.
  • Save fim.py.
  • Run the check to verify your colored output still works:
python fim.py --check

You should see red [MODIFIED] and [DELETED] alerts, a yellow [ADDED] alert, and a summary line counting 3 changes.

Not seeing colored output?

  • Make sure you saved fim.py after pasting the new code.
  • Check that the indentation matches exactly. Every line should be indented with 4 spaces inside the check_integrity() function.
  • If you see a SyntaxError, make sure you removed the old for filepath in modified: block completely before pasting the new one.
  • help me fix my check_integrity output
Add compliance mapping

In GRC, every security finding needs to reference the specific regulation it satisfies. Your tool already has the COMPLIANCE_MAPPING constant defined at the top of the file. Now you will print it after the findings and capture it for the report.

  • In fim.py, add the following code immediately after the report_lines.append(summary) line you just added:
    print(f"{CYAN}{COMPLIANCE_MAPPING}{RESET}")
    report_lines.append(COMPLIANCE_MAPPING)

    with open(REPORT_FILE, "w") as f:
        f.write("\n".join(report_lines))
    print(f"Report saved to {REPORT_FILE}")

What does this code do?

The first two lines print the compliance mapping in cyan on the terminal and append it to the report. The COMPLIANCE_MAPPING constant references PCI DSS Requirement 11.5 (change-detection for critical files) and NIST SP 800-53 SI-7 (software and information integrity verification).

The with open(REPORT_FILE, "w") block joins all captured lines and writes them to report.txt. This file becomes your audit evidence.

An auditor reading this report sees exactly which files changed and which compliance control the monitoring satisfies. That is what makes this a GRC-ready tool instead of just a script.

  • Save fim.py and run the check again:
python fim.py --check

You should see the colored alerts, the summary, a cyan compliance mapping section, and a final line saying Report saved to report.txt.

  • Confirm the report file was created by running:
type report.txt

You should see the full report printed to your terminal: file paths with [MODIFIED], [ADDED], and [DELETED] labels, a summary count, and the compliance mapping section referencing PCI DSS 11.5 and NIST SP 800-53 SI-7.

Don't see report.txt?

  • Make sure you added the with open(REPORT_FILE, "w") block after the compliance mapping lines, not before them.
  • Check that REPORT_FILE = "report.txt" is still defined at the top of your file near the other constants.
  • If you see the report in terminal but no file appears, confirm you are running the command from inside your file-integrity-monitor folder.
  • help me debug my report file saving
Save to a report file

Your reporting is now complete. The last piece is adding the --watch command handler so your tool's CLI is ready for the Secret Mission's continuous monitoring feature.

  • In fim.py, add the following function between check_integrity() and the if __name__ == "__main__": block:
def watch_folder():
    if not os.path.exists(BASELINE_FILE):
        print("No baseline found. Run with --baseline first.")
        return

    print(f"Watching {MONITORED_FOLDER} for changes (Ctrl+C to stop)...\n")

    with open(BASELINE_FILE, "r") as f:
        baseline = json.load(f)

    try:
        while True:
            current = scan_folder(MONITORED_FOLDER)
            timestamp = time.strftime("%Y-%m-%d %H:%M:%S")

            for filepath, current_hash in current.items():
                if filepath not in baseline:
                    print(f"{YELLOW}[{timestamp}] ADDED: {filepath}{RESET}")
                elif baseline[filepath] != current_hash:
                    print(f"{RED}[{timestamp}] MODIFIED: {filepath}{RESET}")

            for filepath in baseline:
                if filepath not in current:
                    print(f"{RED}[{timestamp}] DELETED: {filepath}{RESET}")

            time.sleep(5)
    except KeyboardInterrupt:
        print(f"\n{GREEN}Monitoring stopped. Goodbye!{RESET}")

What does this code do?

This function polls the monitored folder every 5 seconds, comparing current file hashes against the baseline. If anything changed, it prints a timestamped alert instantly.

The try/except KeyboardInterrupt pattern lets the loop run until you press Ctrl+C, then exits gracefully with a goodbye message. You will test this in the Secret Mission.

  • Now find the if __name__ == "__main__": block at the bottom of the file.
  • Find this line:
    else:
        print(f"Unknown command: {command}")
        print("Usage: python fim.py --baseline | --check | --watch")
        sys.exit(1)
  • Add the --watch handler above the else block, so the bottom of your file looks like this:
    if command == "--baseline":
        create_baseline()
    elif command == "--check":
        check_integrity()
    elif command == "--watch":
        watch_folder()
    else:
        print(f"Unknown command: {command}")
        print("Usage: python fim.py --baseline | --check | --watch")
        sys.exit(1)
  • Save fim.py.
  • Verify the watch command is recognized by running:
python fim.py --watch

You should see Watching monitored_files for changes (Ctrl+C to stop)... followed by alerts for the tampered files. Press Ctrl+C to stop it.

Seeing an error when running --watch?

  • Make sure the watch_folder() function is defined before the if __name__ == "__main__": block, not inside it.
  • Check that the elif command == "--watch": line is at the same indentation level as the other elif branches.
  • help me fix my --watch command

Before you run the final verification, predict what you will see. Will the report include color codes, or plain text?

  • Run the full check one more time to generate a fresh report:
python fim.py --check
  • Open report.txt in VS Code (click it in the Explorer sidebar).

The report contains plain-text findings with [MODIFIED], [ADDED], and [DELETED] labels, a summary count, and the full compliance mapping referencing PCI DSS Requirement 11.5 and NIST SP 800-53 SI-7. No ANSI color codes appear in the file because you captured the uncolored alert strings.

Why plain text in the report?

Color codes like \033[91m only render in terminals. A report file shared with auditors, attached to a ticket, or stored as evidence needs to be readable in any text editor. Separating the colored terminal output from the clean report output means your tool serves both audiences.

✔️ Awesome, I've got everything!

Great. Double check you have saved fim.py and that report.txt exists in your file-integrity-monitor folder.

ⓧ I'd like to double check the full code

Scroll up and confirm these sections are present in fim.py, in order:

  • Imports (hashlib, json, os, sys, time) and constants (MONITORED_FOLDER, BASELINE_FILE, REPORT_FILE, color codes, COMPLIANCE_MAPPING).
  • hash_file() function.
  • scan_folder() function.
  • create_baseline() function.
  • check_integrity() function with report_lines, compliance mapping print, and file save.
  • watch_folder() function with the polling loop and KeyboardInterrupt handler.
  • if __name__ == "__main__": block handling --baseline, --check, and --watch.

You now have a fully functional security tool that produces audit-ready evidence. Next up, the Secret Mission challenges you to make this tool watch for changes in real time.

Secret mission

Add Continuous Monitoring

Your FIM only catches changes when you manually run a check. Real attackers strike at 2 AM between your scheduled scans. Can you make your monitor watch continuously and alert you the instant something changes?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • file-integrity-monitor project folder (contains all scripts, test files, baseline, and report).
  • Python 3.14 (installed via Python Install Manager).
  • VS Code (installed as your code editor).

Keep everything running

No action needed. Choose this if you plan to keep building on your File Integrity Monitor or want to reference the code later.

  • Your file-integrity-monitor folder stays on your Desktop with all scripts and test files intact.
  • Python and VS Code remain installed and ready for your next project.

Pause - I'll come back to this later

Close VS Code to free up memory. Your project files remain on disk exactly as you left them.

  • Close VS Code.
  • When you return, open VS Code and use File > Open Folder to navigate back to your file-integrity-monitor folder on your Desktop.

Delete - I don't want to use this again

Remove all project files and optionally uninstall the tools you installed during this project.

Delete the project folder
  • In VS Code's terminal, move out of the project folder by running:
cd ~/Desktop
  • Delete the entire project folder by running:
Remove-Item -Recurse -Force file-integrity-monitor
  • Confirm the folder is gone by running:
ls file-integrity-monitor

You should see an error stating the path does not exist. That confirms the folder is deleted.

Uninstall Python (optional)
  • Press the Windows key and type Add or remove programs. Press Enter.
  • Search for Python in the app list.
  • Click the three dots next to Python Install Manager and select Uninstall.
  • Repeat for any Python 3.14 entries that appear in the list.
Uninstall VS Code (optional)
  • In the same Installed apps page, search for Visual Studio Code.
  • Click the three dots and select Uninstall.
  • Follow the uninstall prompts to complete removal.

Nice Work!

Nice Work!

You just built a working security tool from scratch. Your File Integrity Monitor uses the same cryptographic technique that enterprise tools like OSSEC and Tripwire rely on to protect critical systems.

Here's what you accomplished:

  • Built a File Integrity Monitor using Python and SHA-256 hashing that detects when files are modified, added, or deleted from a monitored folder.
  • Simulated real attacker behavior (modifying configs, planting backdoors, deleting evidence) and caught every change with color-coded timestamped alerts.
  • Generated compliance-mapped security reports that satisfy PCI DSS Requirement 11.5 and NIST SP 800-53 SI-7 audit requirements.
  • Secret Mission: Added real-time continuous monitoring that watches a folder and alerts you the instant a file is tampered with.

Ready to quiz yourself?