210 lines
7.9 KiB
Python
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())
|