File Integrity Check Script (size+mtime)

I recently showed an example of a file integrity check script that calculates SHA-256 hashes. However, if the files are large and 100 gigabytes or more, this will naturally put a significant load on the disk system. Therefore, if this is critical and slows down other processes, I will give an example of a simple script that works much faster. It simply checks the file size and mtime (modification time). However, it is worth considering that, unlike the SHA-256 hash, an attacker can specify the mtime of a new, substituted, infected file similar to the old one.

Installing python3 on Ubuntu Server:

python3 --version
apt install python3 python3-pip

Script contents:

#!/usr/bin/env python3
import os
import json
import time
import subprocess

WATCH_DIR = "/var/www"
DB_FILE = "/root/db_size_mtime.json"
LOG_FILE = "/root/file_monitor.log"

EMAIL_TO = "root"
EMAIL_SUBJECT = "FILE MODIFIED (size+mtime)"

EXCLUDE_EXT = [".log", ".tmp"]
EXCLUDE_NAMES = ["hgrc"]
EXCLUDE_DIRS = [
    "/var/www/logs",
    "/var/www/tmp",
]

# ============================================


def is_excluded(path):
    name = os.path.basename(path)
    if name in EXCLUDE_NAMES:
        return True
    for d in EXCLUDE_DIRS:
        if path.startswith(d):
            return True
    ext = os.path.splitext(name)[1]
    return ext in EXCLUDE_EXT


def fingerprint(path):
    st = os.stat(path)
    return {
        "size": st.st_size,
        "mtime": int(st.st_mtime)
    }


def load_db():
    if os.path.exists(DB_FILE):
        with open(DB_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    return {}


def save_db(db):
    os.makedirs(os.path.dirname(DB_FILE), exist_ok=True)
    with open(DB_FILE, "w", encoding="utf-8") as f:
        json.dump(db, f, indent=2)


def scan():
    data = {}
    for root, dirs, files in os.walk(WATCH_DIR):
        if any(root.startswith(d) for d in EXCLUDE_DIRS):
            continue

        for name in files:
            path = os.path.join(root, name)
            if is_excluded(path):
                continue

            try:
                data[path] = fingerprint(path)
            except Exception:
                continue
    return data


def log_changes(changes):
    if not changes:
        return
    os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
    with open(LOG_FILE, "a", encoding="utf-8", errors="replace") as f:
        f.write(f"\n[{time.strftime('%Y-%m-%d %H:%M:%S')}]\n")
        for c in changes:
            f.write(c + "\n")


def send_email(changes):
    if not changes:
        return
    body = "\n".join(changes)
    body = body.encode("utf-8", errors="replace").decode("utf-8")

    msg = f"Subject: {EMAIL_SUBJECT}\n\n{body}"
    try:
        p = subprocess.Popen(
            ["/usr/sbin/sendmail", EMAIL_TO],
            stdin=subprocess.PIPE
        )
        p.communicate(msg.encode("utf-8"))
    except Exception as e:
        print("[EMAIL ERROR]", e)


def main():
    old = load_db()
    new = scan()
    changes = []

    # Изменённые
    for path, meta in new.items():
        if path in old and old[path] != meta:
            changes.append(f"MODIFIED: {path}")

    # Удалённые
    for path in old:
        if path not in new:
            changes.append(f"DELETED: {path}")

    # Новые
    for path in new:
        if path not in old:
            changes.append(f"NEW: {path}")

    if changes:
        log_changes(changes)
        send_email(changes)

    save_db(new)


if __name__ == "__main__":
    main()

Let’s make the file executable:

chmod +x ixnfo.com.py
./ixnfo.com.py

Let’s add it to cron:

02 7 * * * root /dir/ixnfo.com.py > /dev/null 2>&1

See also my articles:
Using and configuring CRON
File integrity check script (SHA-256 hashes)

Leave a comment

Leave a Reply