Capstone — Porting arc-sec-audit to Python
30 min In ProgressLearning Objectives
- Understand how a real bash security tool maps to Python structure
- Build an OOP Auditor base class with counters and output helpers
- Translate sysctl, stat, grep, and find patterns to Python equivalents
- Write runnable exercises on the pure-Python parts of the tool
What arc-sec-audit Does
arc-sec-audit is a 392-line bash workstation hardening scanner. It runs nine check categories — SSH config, kernel params, file permissions, SUID binaries, world-writable files, sudoers, listening services, packages, and system integrity — then prints a colour-coded pass / warn / fail summary. Porting it to Python means: one class per concept, subprocess replaces shell commands, pathlib replaces stat, re replaces grep.
# bash → python pattern map
#
# _pass=0 self.passed = 0
# _ok() { (( _pass++ )) } def ok(self, msg): ...
# _check_ssh() def check(a): in checks/ssh.py
# sysctl -n key subprocess.run(['sysctl','-n',key])
# stat -c '%a' file oct(Path(file).stat().st_mode)[-3:]
# grep -rihE pat | tail -1 re.finditer() — take last match
# find / -perm -4000 subprocess.run(['find',...])
The Auditor Base Class
Every bash script has global counters and output helpers. In Python those become an Auditor class. One instance is created at startup and passed into every check function. This is the OOP pattern from lesson 1 applied to a real tool.
class Auditor:
def __init__(self):
self.passed = 0
self.warnings = 0
self.failed = 0
def ok(self, msg):
print(f' ✓ {msg}')
self.passed += 1
def warn(self, msg):
print(f' ! {msg}')
self.warnings += 1
def fail(self, msg):
print(f' ✗ {msg}')
self.failed += 1
def summary(self):
print(f' {self.passed} passed · '
f'{self.warnings} warnings · '
f'{self.failed} failed')
a = Auditor()
a.ok('PermitRootLogin no')
a.warn('PasswordAuthentication yes — should be no')
a.summary()
File Permissions — pathlib Instead of stat
The bash script shells out to stat -c "%a" to get octal permissions. In Python, pathlib.Path.stat() gives you the mode directly. oct(mode)[-3:] slices out the three permission digits. No subprocess needed — pure Python, runs anywhere.
from pathlib import Path
def perm_chk(auditor, path, want):
p = Path(path)
if not p.exists():
auditor.info(f'{path} not found — skipping')
return
actual = oct(p.stat().st_mode)[-3:]
if actual == want:
auditor.ok(f'{path} {actual}')
else:
auditor.warn(f'{path} {actual} — want {want}')
# bash: actual=$(stat -c '%a' /etc/shadow)
SSH Config — re Instead of grep
The bash version greps sshd_config for directives and pipes through awk. Python reads the file as a string and uses re.finditer(). sshd processes config top-to-bottom and last entry wins, so we take the last regex match, not the first.
import re
fake_cfg = '''
PermitRootLogin yes
PasswordAuthentication yes
PermitRootLogin no
'''
pattern = re.compile(
r'^PermitRootLogin\s+(\S+)',
re.IGNORECASE | re.MULTILINE
)
matches = list(pattern.finditer(fake_cfg))
last = matches[-1].group(1).lower() if matches else None
print(last) # no — last entry wins
subprocess — The Bridge to OS Commands
Checks that need sysctl, ss, find, or sudo shell out via subprocess.run(). The Auditor wraps this in a run() helper that always returns a string — empty string on failure — so every check handles missing tools gracefully. subprocess calls need a real Linux environment. Use them locally: sudo python arc_sec_audit.py
import subprocess
# inside Auditor class:
def run(self, cmd, sudo=False, timeout=10):
if sudo:
cmd = ['sudo'] + cmd
try:
result = subprocess.run(
cmd, capture_output=True,
text=True, timeout=timeout
)
return result.stdout.strip()
except (subprocess.TimeoutExpired,
FileNotFoundError, PermissionError):
return ''
def sysctl_chk(self, key, want, label, mode='exact'):
val_str = self.run(['sysctl', '-n', key])
if not val_str:
self.info(f'{label} (not available)')
return
val = int(val_str)
if mode == 'min' and val >= want:
self.ok(f'{label} {val}')
elif mode == 'exact' and val == want:
self.ok(f'{label} {val}')
else:
self.warn(f'{label} got {val}, want {want}')
📝 Exercise: Build a Mini Auditor
Build a miniature arc-sec-audit. Write check_shadow_perm() — takes an auditor and a simulated permission string, calls ok() if it matches '640', warn() otherwise. Then write check_root_login() — takes a config string, uses re to find the last PermitRootLogin value, calls ok() if 'no', warn() otherwise. Run both and print the summary.