Python Security Scripting — Bash to Python

30 min In Progress

Learning 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.

/etc/shadow is typically owned by root:shadow with mode 640. If this runs as a non-root user, the stat() call may raise PermissionError — the perm_chk helper handles it gracefully.
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.

code.py
Output

            
← Back to Chapter