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))
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']}")
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)