Python AutomationTutorialIntermediate

Python for auditors: automating CIS Benchmark checks on Linux

A small, auditable Python script that checks a Linux host against a subset of CIS Benchmark v3 and emits an audit-ready CSV.

Rudy Prasetiya

Rudy Prasetiya

Apr 24, 2026 · 8 min

CISISO 27001NIST 800-53
Python for auditors: automating CIS Benchmark checks on Linux

If you audit ten Linux servers you can do it by hand. If you audit a thousand, you write a script, and the script itself becomes part of your evidence. Here is a minimal, defensible CIS check runner in Python.

Design goals

  • Read-only — never mutates the target.
  • One function per CIS recommendation, named after the recommendation ID.
  • Output a CSV with: rec_id, title, status, evidence, scanned_at.
  • No third-party deps — runs on a frozen Python 3 install.

The runner

cis_check.pypython
import csv, datetime, pathlib, subprocess, sys
from dataclasses import dataclass, asdict

@dataclass
class Result:
    rec_id: str
    title: str
    status: str  # PASS | FAIL | MANUAL
    evidence: str

def _read(path: str) -> str:
    try:
        return pathlib.Path(path).read_text()
    except FileNotFoundError:
        return ""

def cis_5_2_8_disable_root_ssh() -> Result:
    cfg = _read("/etc/ssh/sshd_config")
    ok = any(line.strip().lower() == "permitrootlogin no" for line in cfg.splitlines())
    return Result("5.2.8", "Disable SSH root login",
                  "PASS" if ok else "FAIL",
                  "PermitRootLogin no" if ok else "directive missing or != no")

def cis_1_1_1_disable_unused_filesystems() -> Result:
    out = subprocess.run(["lsmod"], capture_output=True, text=True).stdout
    flagged = [m for m in ("cramfs", "freevxfs", "hfs", "hfsplus", "squashfs", "udf")
               if m in out]
    return Result("1.1.1", "Disable unused filesystems",
                  "FAIL" if flagged else "PASS",
                  f"loaded: {','.join(flagged)}" if flagged else "none loaded")

CHECKS = [cis_5_2_8_disable_root_ssh, cis_1_1_1_disable_unused_filesystems]

def main(out_path: str) -> int:
    rows = [asdict(c()) for c in CHECKS]
    ts = datetime.datetime.utcnow().isoformat()
    with open(out_path, "w", newline="") as f:
        w = csv.DictWriter(f, fieldnames=[*rows[0].keys(), "scanned_at"])
        w.writeheader()
        for r in rows:
            w.writerow({**r, "scanned_at": ts})
    failed = sum(1 for r in rows if r["status"] == "FAIL")
    return 1 if failed else 0

if __name__ == "__main__":
    sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "cis_results.csv"))

Audit insight

The script's git commit hash, the CSV, and a screenshot of the run command form a complete evidence triplet. Auditors love reproducible evidence.

What you do NOT do

Risk

Do not let the script auto-remediate. The moment it writes to the host, your read-only audit posture is gone and you need change-management evidence for every run.

Mapping to frameworks

CheckCISISO 27001NIST 800-53
SSH root login5.2.8A.8.2AC-6(5)
Unused filesystems1.1.1A.8.9CM-7
#python#cis-benchmark#linux#automation
Rudy Prasetiya

Rudy Prasetiya

IT GRC, cybersecurity & audit practitioner. Writes about controls that actually hold.