DEV Community

Naveen Karasu
Naveen Karasu

Posted on

Day 21/90: Building a Python System Audit Script

Day 21/90: Building a Python System Audit Script

90 Day Python Security Scripting Challenge

Today is a build day. We are creating a system audit script that answers three questions: What is running on this host? What ports are exposed? Are critical file permissions correct?

Why Automate System Audits

Manual audits drift. An engineer checks a server on Monday, marks it compliant, and by Thursday a new service is listening on an unexpected port. Automated audit scripts run the same checks in the same order every time. They do not forget steps, they do not get distracted, and they produce machine-readable output you can feed into dashboards.

Auditing Listening Ports Against a Baseline

The highest-value check in any system audit is port validation. Here is a function that enumerates listeners and flags anything outside your approved set:

import psutil
import json
from datetime import datetime

def audit_ports(approved_ports):
    results = []
    for conn in psutil.net_connections(kind="inet"):
        if conn.status != "LISTEN":
            continue
        port = conn.laddr.port
        proc = "unknown"
        if conn.pid:
            try:
                proc = psutil.Process(conn.pid).name()
            except (psutil.NoSuchProcess, psutil.AccessDenied):
                proc = "inaccessible"
        results.append({
            "port": port,
            "process": proc,
            "approved": port in approved_ports,
            "severity": "HIGH" if port not in approved_ports else "OK",
        })
    return results

approved = {22, 80, 443}
findings = audit_ports(approved)
report = {
    "timestamp": datetime.utcnow().isoformat(),
    "port_findings": findings,
    "unapproved_count": sum(
        1 for f in findings if not f["approved"]
    ),
}
print(json.dumps(report, indent=2))
Enter fullscreen mode Exit fullscreen mode

Store your approved ports in a config file that lives in version control. When infrastructure changes, update the baseline first, then deploy.

Checking File Permissions on Critical Paths

Files like /etc/shadow and /etc/sudoers have strict permission requirements. World-readable password hashes or world-writable sudo configs are findings you want to catch before an auditor does:

import os
import stat

CRITICAL_FILES = {
    "/etc/shadow": {"max_mode": 0o640, "owner": "root"},
    "/etc/ssh/sshd_config": {"max_mode": 0o644, "owner": "root"},
    "/etc/sudoers": {"max_mode": 0o440, "owner": "root"},
}

def check_permissions(file_rules):
    issues = []
    for path, rules in file_rules.items():
        if not os.path.exists(path):
            issues.append({"path": path, "issue": "file missing"})
            continue
        file_stat = os.stat(path)
        mode = stat.S_IMODE(file_stat.st_mode)
        if mode > rules["max_mode"]:
            issues.append({
                "path": path,
                "current": oct(mode),
                "expected_max": oct(rules["max_mode"]),
                "issue": "permissions too broad",
            })
    return issues

for issue in check_permissions(CRITICAL_FILES):
    print(f"FINDING: {issue['path']} - {issue['issue']}")
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

Automate the checks you would run manually during an audit. Output structured JSON so results are parseable by other tools. Version-control your baselines alongside your infrastructure code. Run audits on a schedule and diff consecutive reports to catch drift.


Day 21/90 of the 90 Day Python Security Scripting Challenge

Keep all security scripts in a dedicated venv with pinned dependencies. Run bandit against your own code regularly -- security tools should be held to the same standard as the systems they protect.

Top comments (0)