CIS_Benchmarks/main.py
2026-06-18 20:47:15 +03:00

210 lines
7.9 KiB
Python

#!/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/<name>/
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())