69 lines
1.8 KiB
Python
Executable File
69 lines
1.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
SysDeck - Integrity Bridge Helper
|
|
Author: Jeremy Anderson (https://dcos.net)
|
|
|
|
Runs lynis audit system and parses the hardening index from the report.
|
|
|
|
Usage:
|
|
python3 -m sysdeck.bridge.integrity score
|
|
python3 -m sysdeck.bridge.integrity scan
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from typing import Any
|
|
|
|
HARDENING_RE = re.compile(r"Hardening index\s*:\s*(\d+)")
|
|
|
|
|
|
def score() -> int | None:
|
|
"""Return the latest hardening index, or None if lynis hasn't run."""
|
|
try:
|
|
with open("/var/log/lynis.log", encoding="utf-8") as f:
|
|
log = f.read()
|
|
except FileNotFoundError:
|
|
return None
|
|
m = HARDENING_RE.search(log)
|
|
return int(m.group(1)) if m else None
|
|
|
|
|
|
def scan() -> dict[str, Any]:
|
|
"""Run a fresh lynis audit and return the parsed result."""
|
|
# A lynis audit runs for minutes by design; 15 minutes is the hard
|
|
# ceiling so a wedged audit cannot hang the bridge forever.
|
|
try:
|
|
subprocess.run(
|
|
["lynis", "audit", "system"], capture_output=True, text=True, check=True,
|
|
timeout=900,
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
return {"error": "lynis audit timed out after 900s", "score": None}
|
|
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
|
|
return {"error": str(exc), "score": None}
|
|
return {"score": score()}
|
|
|
|
|
|
COMMANDS = {
|
|
"score": lambda _args: score(),
|
|
"scan": lambda _args: scan(),
|
|
}
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
if not argv or argv[0] in ("-h", "--help"):
|
|
print(__doc__)
|
|
return 0
|
|
cmd = COMMANDS.get(argv[0])
|
|
if not cmd:
|
|
print(f"Unknown subcommand: {argv[0]}", file=sys.stderr)
|
|
return 2
|
|
print(json.dumps(cmd(argv[1:])))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv[1:]))
|