Files

310 lines
12 KiB
Python

from __future__ import annotations
import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from cis_benchmarks.checker import parse_benchmark, parse_expectations
REPO_DIR = Path(__file__).resolve().parents[1]
BENCHMARK = REPO_DIR / "text" / "CIS_AlmaLinux_OS_9_Benchmark_v2.0.0.txt"
class ParseBenchmarkTests(unittest.TestCase):
def test_parses_real_controls(self) -> None:
metadata, controls = parse_benchmark(BENCHMARK.read_text(encoding="utf-8"))
self.assertIn("CIS AlmaLinux OS 9 Benchmark", metadata.title)
self.assertEqual(metadata.version, "v2.0.0")
self.assertGreater(len(controls), 200)
control_map = {control.identifier: control for control in controls}
self.assertIn("1.3.1.5", control_map)
self.assertEqual(control_map["1.3.1.5"].assessment, "Automated")
self.assertIn("getenforce", control_map["1.3.1.5"].audit_text)
self.assertTrue(control_map["1.3.1.5"].remediation)
self.assertNotIn("References:", control_map["1.3.1.5"].remediation)
self.assertEqual(control_map["1.1.1.9"].assessment, "Manual")
def test_extracts_script_and_simple_commands(self) -> None:
_, controls = parse_benchmark(BENCHMARK.read_text(encoding="utf-8"))
control_map = {control.identifier: control for control in controls}
script, expectations = parse_expectations(control_map["1.1.1.1"].audit_text)
self.assertIsNotNone(script)
self.assertFalse(expectations)
script, expectations = parse_expectations(control_map["5.2.2"].audit_text)
self.assertIsNone(script)
self.assertEqual(len(expectations), 2)
self.assertEqual(expectations[1].expectation_type, "empty")
def test_cli_lists_controls_as_json(self) -> None:
env = {"PYTHONPATH": str(REPO_DIR / "src")}
result = subprocess.run(
[
sys.executable,
"-m",
"cis_benchmarks.checker",
str(BENCHMARK),
"--control",
"1.3.1.5",
"--list-controls",
"--format",
"json",
],
check=False,
capture_output=True,
text=True,
cwd=REPO_DIR,
env=env,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
payload = json.loads(result.stdout)
self.assertEqual(payload[0]["id"], "1.3.1.5")
def test_report_dir_writes_json_and_markdown(self) -> None:
env = {"PYTHONPATH": str(REPO_DIR / "src")}
with tempfile.TemporaryDirectory() as tmpdir:
report_dir = Path(tmpdir)
result = subprocess.run(
[
sys.executable,
"-m",
"cis_benchmarks.checker",
str(BENCHMARK),
"--control",
"1.3.1.5",
"--manual",
"--report-dir",
str(report_dir),
"--format",
"markdown",
],
check=False,
capture_output=True,
text=True,
cwd=REPO_DIR,
env=env,
)
self.assertTrue((report_dir / "audit-report.json").exists(), msg=result.stderr)
self.assertTrue((report_dir / "audit-report.md").exists(), msg=result.stderr)
self.assertTrue((report_dir / "remediation-guide.md").exists(), msg=result.stderr)
markdown = (report_dir / "audit-report.md").read_text(encoding="utf-8")
remediation = (report_dir / "remediation-guide.md").read_text(encoding="utf-8")
payload = json.loads((report_dir / "audit-report.json").read_text(encoding="utf-8"))
self.assertIn("LLM Handoff", markdown)
self.assertIn("Remediation Plan By Subsystem", remediation)
self.assertIn("Likely Accepted Exceptions", remediation)
self.assertIn("summary", payload)
def test_report_json_can_render_remediation_guide(self) -> None:
env = {"PYTHONPATH": str(REPO_DIR / "src")}
with tempfile.TemporaryDirectory() as tmpdir:
report_dir = Path(tmpdir)
first = subprocess.run(
[
sys.executable,
"-m",
"cis_benchmarks.checker",
str(BENCHMARK),
"--control",
"5.2.2",
"--report-dir",
str(report_dir),
"--format",
"json",
],
check=False,
capture_output=True,
text=True,
cwd=REPO_DIR,
env=env,
)
self.assertTrue((report_dir / "audit-report.json").exists(), msg=first.stderr)
guide = report_dir / "standalone-remediation.md"
second = subprocess.run(
[
sys.executable,
"-m",
"cis_benchmarks.checker",
"--report-json",
str(report_dir / "audit-report.json"),
"--guide-output",
str(guide),
],
check=False,
capture_output=True,
text=True,
cwd=REPO_DIR,
env=env,
)
self.assertEqual(second.returncode, 0, msg=second.stderr)
self.assertTrue(guide.exists(), msg=second.stderr)
self.assertIn("LLM Usage Notes", guide.read_text(encoding="utf-8"))
def test_can_record_and_list_approved_exception(self) -> None:
env = {"PYTHONPATH": str(REPO_DIR / "src")}
with tempfile.TemporaryDirectory() as tmpdir:
exceptions_file = Path(tmpdir) / "exceptions.json"
approve = subprocess.run(
[
sys.executable,
"-m",
"cis_benchmarks.checker",
"--exceptions-file",
str(exceptions_file),
"--host",
"lynx",
"--approve-exception",
"3.3.1",
"--exception-reason",
"Docker networking requires forwarding",
"--exception-notes",
"Reviewed with operator during CIS audit",
],
check=False,
capture_output=True,
text=True,
cwd=REPO_DIR,
env=env,
)
self.assertEqual(approve.returncode, 0, msg=approve.stderr)
payload = json.loads(approve.stdout)
self.assertEqual(payload["control_id"], "3.3.1")
listed = subprocess.run(
[
sys.executable,
"-m",
"cis_benchmarks.checker",
"--exceptions-file",
str(exceptions_file),
"--host",
"lynx",
"--list-exceptions",
],
check=False,
capture_output=True,
text=True,
cwd=REPO_DIR,
env=env,
)
self.assertEqual(listed.returncode, 0, msg=listed.stderr)
listed_payload = json.loads(listed.stdout)
self.assertIn("3.3.1", listed_payload["hosts"]["lynx"]["controls"])
def test_report_uses_approved_exception_annotations(self) -> None:
env = {"PYTHONPATH": str(REPO_DIR / "src")}
with tempfile.TemporaryDirectory() as tmpdir:
report_dir = Path(tmpdir) / "report"
exceptions_file = Path(tmpdir) / "exceptions.json"
subprocess.run(
[
sys.executable,
"-m",
"cis_benchmarks.checker",
"--exceptions-file",
str(exceptions_file),
"--host",
"lynx",
"--approve-exception",
"1.1.2.1.1",
"--exception-reason",
"Single-root homelab layout accepted",
],
check=True,
capture_output=True,
text=True,
cwd=REPO_DIR,
env=env,
)
result = subprocess.run(
[
sys.executable,
"-m",
"cis_benchmarks.checker",
str(BENCHMARK),
"--control",
"1.1.2.1.1",
"--host",
"lynx",
"--exceptions-file",
str(exceptions_file),
"--report-dir",
str(report_dir),
"--format",
"remediation",
],
check=False,
capture_output=True,
text=True,
cwd=REPO_DIR,
env=env,
)
guide = (report_dir / "remediation-guide.md").read_text(encoding="utf-8")
report_json = json.loads((report_dir / "audit-report.json").read_text(encoding="utf-8"))
self.assertIn("Approved Exceptions", guide)
self.assertIn("Single-root homelab layout accepted", guide)
self.assertEqual(report_json["results"][0]["approved_exception_host"], "lynx")
def test_exception_candidates_are_flagged_in_guide(self) -> None:
env = {"PYTHONPATH": str(REPO_DIR / "src")}
with tempfile.TemporaryDirectory() as tmpdir:
report_dir = Path(tmpdir)
result = subprocess.run(
[
sys.executable,
"-m",
"cis_benchmarks.checker",
str(BENCHMARK),
"--control",
"1.1.2.1.1",
"--report-dir",
str(report_dir),
"--format",
"remediation",
],
check=False,
capture_output=True,
text=True,
cwd=REPO_DIR,
env=env,
)
guide = (report_dir / "remediation-guide.md").read_text(encoding="utf-8")
self.assertIn("Likely exception candidates", guide)
self.assertIn("single-root or homelab systems", guide)
def test_ip_forwarding_exception_candidate_is_flagged(self) -> None:
env = {"PYTHONPATH": str(REPO_DIR / "src")}
with tempfile.TemporaryDirectory() as tmpdir:
report_dir = Path(tmpdir)
result = subprocess.run(
[
sys.executable,
"-m",
"cis_benchmarks.checker",
str(BENCHMARK),
"--control",
"3.3.1",
"--report-dir",
str(report_dir),
"--format",
"remediation",
],
check=False,
capture_output=True,
text=True,
cwd=REPO_DIR,
env=env,
)
guide = (report_dir / "remediation-guide.md").read_text(encoding="utf-8")
self.assertIn("Likely exception candidates", guide)
self.assertIn("IP forwarding is often intentionally enabled", guide)
if __name__ == "__main__":
unittest.main()