mirror of
https://git.valherun.com/valherun/cis-benchmarks.git
synced 2026-10-14 18:22:03 -04:00
310 lines
12 KiB
Python
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()
|