chore(git-walk): Git walk update for cis-benchmarks

This commit is contained in:
andersen committed 2026-03-24 21:17:47 -04:00
1 parent 093a846ac7
commit d796d39326
9 files changed
+1613

No files matched your search

+72
View File
@@ -25,5 +25,77 @@ Local repository for CIS benchmark source PDFs and plain-text extracts.
This keeps the source PDFs in `pdf/` and rewrites matching `.txt` files in `text/`.
## Benchmark Checker
The repo now includes a generic checker that parses a CIS text export and runs the
documented `Audit:` procedures for automated controls.
List parsed controls from a benchmark:
```bash
./bin/cis-check text/CIS_AlmaLinux_OS_9_Benchmark_v2.0.0.txt --list-controls
```
Run a single control:
```bash
./bin/cis-check text/CIS_AlmaLinux_OS_9_Benchmark_v2.0.0.txt --control 1.3.1.5
```
Run all automated Level 1 server checks and emit JSON:
```bash
./bin/cis-check text/CIS_AlmaLinux_OS_9_Benchmark_v2.0.0.txt \
--profile "Level 1 - Server" \
--format json
```
Generate both machine-readable and human-readable reports in one pass:
```bash
./bin/cis-check text/CIS_AlmaLinux_OS_9_Benchmark_v2.0.0.txt \
--profile "Level 1 - Server" \
--manual \
--report-dir reports/almalinux9
```
Generate only the remediation guide from an existing audit JSON:
```bash
./bin/cis-check \
--report-json reports/almalinux9/audit-report.json \
--guide-output reports/almalinux9/remediation-guide.md
```
Record an approved exception live while reviewing:
```bash
./bin/cis-check \
--exceptions-file reports/almalinux9/exceptions.json \
--host lynx \
--approve-exception 3.3.1 \
--exception-reason "Docker networking requires IP forwarding" \
--exception-notes "Reviewed during CIS audit"
```
Then rerun the report with the same exceptions file:
```bash
./bin/cis-check text/CIS_AlmaLinux_OS_9_Benchmark_v2.0.0.txt \
--host lynx \
--exceptions-file reports/almalinux9/exceptions.json \
--report-dir reports/almalinux9
```
Notes:
- Automated controls with embedded shell audit scripts are executed directly with `bash`.
- Automated controls with simple command examples are executed command-by-command and
evaluated against expected output in the benchmark text.
- Manual controls are excluded by default and reported as `MANUAL` when `--manual` is used.
- `--report-dir` writes `audit-report.json`, `audit-report.md`, and `remediation-guide.md`.
- `--format remediation` prints the remediation guide directly.
- The markdown report includes benchmark-provided remediation text for failed, error, review,
and manual controls.
- The remediation guide groups items by subsystem and separates execution blockers from
direct compliance gaps.
- The remediation guide also flags likely accepted exceptions such as structural partition
requirements or container-driven forwarding settings for human approval.
- Approved host-specific exceptions can be stored in a local JSON file and are then shown in
the audit and remediation outputs on future runs.
- Run the checker on the target host, ideally as `root`, because many CIS audits depend on
host-specific binaries, privileged files, and live system state.
## Host Assessments
- `docs/lynx-almalinux9-remediation-checklist.md`
Executable
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_PATH="$(readlink -f "$0")"
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
export PYTHONPATH="$REPO_DIR/src${PYTHONPATH:+:$PYTHONPATH}"
exec python3 -m cis_benchmarks.checker "$@"
+15
View File
@@ -0,0 +1,15 @@
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"
[project]
name = "cis-benchmarks"
version = "0.1.0"
description = "Parse CIS benchmark exports and run automated audit checks"
requires-python = ">=3.9"
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]
+1
View File
@@ -0,0 +1 @@
"""CIS benchmark parsing and checking helpers."""
File diff suppressed because it is too large. Load diff
Binary file not shown.
+309
View File
@@ -0,0 +1,309 @@
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()