#!/usr/bin/env python3 """ CIS Benchmark Auditor — multi-benchmark CLI ============================================ Supports: nginx (CIS NGINX Benchmark v3.0.0) debian (CIS Debian Linux 13 Benchmark v1.0.0) Usage examples -------------- # List available profiles for a benchmark python main_v2.py nginx --list-profiles python main_v2.py debian --list-profiles --benchmark-file CIS_Debian_Linux_13_Benchmark_v1.0.0.xlsx # Audit local machine python main_v2.py nginx --profile "Level 1 - Webserver" python main_v2.py debian --profile "Level 1 - Server" \ --benchmark-file CIS_Debian_Linux_13_Benchmark_v1.0.0.xlsx # Audit remote host via SSH python main_v2.py debian \ --mode ssh --host 10.0.0.5 --user admin --key ~/.ssh/id_rsa \ --profile "Level 1 - Server" \ --benchmark-file CIS_Debian_Linux_13_Benchmark_v1.0.0.xlsx # Run only specific checks python main_v2.py debian --profile "Level 1 - Server" \ --benchmark-file CIS_Debian_Linux_13_Benchmark_v1.0.0.xlsx \ --checks 1.1.2.1.1,2.1.1,5.1.1 Adding a new benchmark ---------------------- 1. Create cis_auditor/benchmarks// 2. Implement loader.py (data-driven) or checks.py (hand-coded) 3. Expose a load(xlsx_path) -> BenchmarkInfo in __init__.py 4. Add the name to BENCHMARK_NAMES below """ import argparse import sys from datetime import datetime from pathlib import Path BENCHMARK_NAMES = ["nginx", "debian"] def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser( description="CIS Benchmark Auditor", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__, ) p.add_argument( "benchmark", choices=BENCHMARK_NAMES, help="Which benchmark to run", ) # Benchmark file (required for data-driven benchmarks like debian) p.add_argument( "--benchmark-file", metavar="XLSX", help="Path to the benchmark XLSX file (required for: debian)", ) # Execution mode p.add_argument("--mode", choices=["local", "ssh"], default="local") # SSH options ssh = p.add_argument_group("SSH options") ssh.add_argument("--host") ssh.add_argument("--port", type=int, default=22) ssh.add_argument("--user", default="root") ssh.add_argument("--key", help="Path to SSH private key") ssh.add_argument("--password") # Scope p.add_argument("--profile", metavar="PROFILE", help="Profile to audit (use --list-profiles to see options)") p.add_argument("--checks", help="Comma-separated check IDs to run, e.g. 1.1.2.1.1,2.1.1") p.add_argument("--list-profiles", action="store_true", help="Print available profiles and exit") # Output p.add_argument("--output", default=None, help="Output XLSX path (default: auto-named)") p.add_argument("--verbose", "-v", action="store_true") return p def load_benchmark(name: str, xlsx_path: str | None): """Load and return a BenchmarkInfo for *name*.""" if name == "nginx": from cis_auditor.benchmarks.nginx import benchmark return benchmark elif name == "debian": if not xlsx_path: print("[!] --benchmark-file is required for the debian benchmark.", file=sys.stderr) sys.exit(1) from cis_auditor.benchmarks import debian return debian.load(xlsx_path) else: print(f"[!] Unknown benchmark '{name}'.", file=sys.stderr) sys.exit(1) def main() -> int: parser = build_parser() args = parser.parse_args() # ── load benchmark ──────────────────────────────────────────────────────── bm = load_benchmark(args.benchmark, args.benchmark_file) # ── --list-profiles ─────────────────────────────────────────────────────── if args.list_profiles: print(f"\n{bm.name} {bm.version} — available profiles:\n") for prof in bm.profiles: n = len(bm.checks_by_profile.get(prof, [])) print(f" {prof!r:<45} ({n} checks)") print() return 0 if not args.profile: parser.error("--profile is required (use --list-profiles to see options)") if args.mode == "ssh" and not args.host: parser.error("--host is required when --mode ssh") # ── build executor ──────────────────────────────────────────────────────── from cis_auditor.core.executor import LocalExecutor, SSHExecutor, ExecutorError try: if args.mode == "local": executor = LocalExecutor() print(f"[*] Benchmark : {bm.name} {bm.version}") print(f"[*] Mode : LOCAL") else: print(f"[*] Benchmark : {bm.name} {bm.version}") print(f"[*] Mode : SSH → {args.user}@{args.host}:{args.port}") executor = SSHExecutor( host=args.host, port=args.port, username=args.user, password=args.password, key_file=args.key, ) except ExecutorError as exc: print(f"[!] {exc}", file=sys.stderr) return 1 except Exception as exc: print(f"[!] Connection error: {exc}", file=sys.stderr) return 1 # ── check subset ────────────────────────────────────────────────────────── check_ids = None if args.checks: check_ids = [c.strip() for c in args.checks.split(",") if c.strip()] print(f"[*] Profile : {args.profile}") total_checks = len(check_ids or bm.checks_by_profile.get(args.profile, [])) print(f"[*] Checks : {total_checks}") print(f"[*] Running audit …\n") # ── run audit ───────────────────────────────────────────────────────────── try: report = bm.run_profile( executor=executor, profile=args.profile, check_ids=check_ids, verbose=args.verbose, ) finally: executor.close() # ── print summary ───────────────────────────────────────────────────────── print(f"\n{'─' * 65}") print(f" {report.summary()}") print(f"{'─' * 65}") if report.failed: print(f"\n Failed checks ({len(report.failed)}):") for r in report.failed: print(f" ✗ {r.check_id:<18} {r.title[:55]}") if report.manual: print(f"\n Manual review required ({len(report.manual)}):") for r in report.manual: print(f" ⚠ {r.check_id:<18} {r.title[:55]}") if report.errors: print(f"\n Errors ({len(report.errors)}):") for r in report.errors: print(f" ✗ {r.check_id:<18} {str(r.actual_value)[:60]}") # ── generate report ─────────────────────────────────────────────────────── if args.output: output_path = Path(args.output) else: safe_bm = args.benchmark safe_prof = args.profile.replace(" ", "_").replace("-", "").replace("__", "_") ts = datetime.utcnow().strftime("%Y%m%d_%H%M%S") output_path = Path(f"cis_{safe_bm}_{safe_prof}_{ts}.xlsx") from cis_auditor.core.report import generate_report generate_report(report, output_path) print(f"\n[✓] Report saved to: {output_path.resolve()}\n") return 0 if __name__ == "__main__": sys.exit(main())