Python Security Scripting — Bash to Python
30 min In ProgressLearning Objectives
- Translate common bash audit patterns to Python using subprocess and pathlib
- Build a reusable OOP Auditor base class with ok/warn/fail counters
- Structure a multi-check security audit that prints a pass/warn/fail summary
Why Convert Bash to Python
A 392-line bash security audit script (like arc-sec-audit) works, but bash has real limitations: no native data structures, fragile string parsing, hard to test, and no OOP. Converting to Python gives you the same logic in a structure that's modular, testable, and extendable — each check becomes its own function or class, and shared state (counters, output helpers) lives in one base class. The three main translation patterns are: 1. Shell commands → subprocess.run() 2. stat -c '%a' file → pathlib.Path.stat().st_mode 3. grep 'pattern' file → re.finditer() on file content
# Pattern 1: Shell command → subprocess.run()
# bash: val=$(sysctl -n kernel.randomize_va_space)
# python:
import subprocess
def run_cmd(cmd_list, timeout=10):
try:
result = subprocess.run(
cmd_list, capture_output=True, text=True, timeout=timeout
)
return result.stdout.strip()
except (subprocess.TimeoutExpired, FileNotFoundError, PermissionError):
return ''
# Pattern 2: File permissions → pathlib
# bash: actual=$(stat -c '%a' /etc/shadow)
# python:
from pathlib import Path
def get_perms(path_str):
p = Path(path_str)
if not p.exists():
return None
return oct(p.stat().st_mode)[-3:]
# Pattern 3: grep on file → re
# bash: grep -rih '^PermitRootLogin' /etc/ssh/sshd_config | tail -1
# python:
import re
def grep_file(filepath, pattern):
p = Path(filepath)
if not p.exists():
return None
matches = re.findall(
pattern, p.read_text(errors='replace'), re.IGNORECASE
)
return matches[-1] if matches else None
# Show the patterns in action (these work on this machine)
perms = get_perms('/etc/hostname')
print(f'/etc/hostname permissions: {perms}')
hostname = run_cmd(['hostname'])
print(f'Hostname: {hostname}')
The OOP Auditor Base Class
arc-sec-audit structures its nine check modules around a shared Auditor base class. The base class holds: three counters (passed/warnings/failed), output helpers (ok/warn/fail that both print and increment the right counter), a subprocess wrapper (run()), and helper methods for the two most common check patterns (sysctl_chk and perm_chk). Each check module is then just a function check(auditor) that calls those helpers — clean, flat, testable.
class auditor:
def __init__(self):
self.passed = 0
self.warnings = 0
self.failed = 0
def ok(self, msg):
print(f' PASS {msg}')
self.passed += 1
def warn(self, msg):
print(f' WARN {msg}')
self.warnings += 1
def fail(self, msg):
print(f' FAIL {msg}')
self.failed += 1
def hdr(self, msg):
print(f'\n--- {msg} ---')
def run(self, cmd, timeout=10):
import subprocess
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=timeout
)
return result.stdout.strip()
except (subprocess.TimeoutExpired, FileNotFoundError, PermissionError):
return ''
def perm_chk(self, path, want):
from pathlib import Path
p = Path(path)
if not p.exists():
self.warn(f'{path} not found')
return
actual = oct(p.stat().st_mode)[-3:]
if actual == want:
self.ok(f'{path} permissions {actual}')
else:
self.warn(f'{path} permissions {actual} (want {want})')
def summary(self):
total = self.passed + self.warnings + self.failed
print(f'\nResults: {self.passed} passed, {self.warnings} warnings, {self.failed} failed / {total} total')
# Use it
a = auditor()
a.hdr('Permission Checks')
a.perm_chk('/etc/hostname', '644')
a.perm_chk('/etc/hosts', '644')
a.perm_chk('/etc/shadow', '640')
a.summary()
The Nine Check Categories — arc-sec-audit Structure
arc-sec-audit organizes its 9 checks as separate modules, each exporting a single function check(auditor). The entry point calls them in sequence and prints the final summary. The nine categories: ssh — PermitRootLogin, PasswordAuthentication, X11Forwarding kernel — ASLR, dmesg_restrict, kptr_restrict, syncookies perms — passwd, shadow, sudoers, /root, SSH host keys suid — SUID binaries vs known-good baseline world-write — world-writable files in /etc /home /usr /var sudoers — NOPASSWD:ALL entries outside whitelisted files services — TCP listeners localhost-bound vs network-exposed packages — pending security updates + debsecan CVE check system — root account lock, AppArmor, core dumps, unattended-upgrades
class auditor:
def __init__(self):
self.passed = 0
self.warnings = 0
self.failed = 0
def ok(self, msg): print(f' PASS {msg}'); self.passed += 1
def warn(self, msg): print(f' WARN {msg}'); self.warnings += 1
def fail(self, msg): print(f' FAIL {msg}'); self.failed += 1
def hdr(self, msg): print(f'\n--- {msg} ---')
def summary(self):
total = self.passed + self.warnings + self.failed
print(f'\nResults: {self.passed}P / {self.warnings}W / {self.failed}F / {total} total')
# Simulated check modules — mirrors the arc-sec-audit structure
def check_ssh(a, sshd_config_lines):
a.hdr('SSH Configuration')
settings = {}
for line in sshd_config_lines:
if line.strip() and not line.startswith('#'):
parts = line.split()
if len(parts) >= 2:
settings[parts[0].lower()] = parts[1].lower()
val = settings.get('permitrootlogin', 'yes')
if val in ('no', 'prohibit-password'):
a.ok(f'PermitRootLogin {val}')
else:
a.fail(f'PermitRootLogin {val} (want no)')
val = settings.get('passwordauthentication', 'yes')
if val == 'no':
a.ok('PasswordAuthentication no')
else:
a.warn('PasswordAuthentication yes — consider key-only auth')
def check_system(a, root_locked, apparmor_active, core_dumps_disabled):
a.hdr('System Integrity')
if root_locked:
a.ok('root account locked')
else:
a.fail('root account has a valid login shell')
if apparmor_active:
a.ok('AppArmor active')
else:
a.warn('AppArmor not enforcing')
if core_dumps_disabled:
a.ok('core dumps disabled')
else:
a.warn('core dumps enabled — may leak sensitive memory')
a = auditor()
sshd_config = [
'PermitRootLogin no',
'PasswordAuthentication yes',
'X11Forwarding no',
]
check_ssh(a, sshd_config)
check_system(a, root_locked=True, apparmor_active=True, core_dumps_disabled=False)
a.summary()
📝 Exercise: Build a Mini Hardening Auditor
Using the auditor base class, write two check functions: check_kernel(a, sysctl_values) which verifies that kernel.randomize_va_space is 2 and kernel.dmesg_restrict is 1, and check_ssh(a, sshd_lines) which verifies PermitRootLogin is no and X11Forwarding is no. Run both and print the summary.