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
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
| Check | CIS | ISO 27001 | NIST 800-53 |
|---|---|---|---|
| SSH root login | 5.2.8 | A.8.2 | AC-6(5) |
| Unused filesystems | 1.1.1 | A.8.9 | CM-7 |
Rudy Prasetiya
IT GRC, cybersecurity & audit practitioner. Writes about controls that actually hold.

