mirror of
https://git.valherun.com/valherun/cis-benchmarks.git
synced 2026-10-14 17:32:03 -04:00
chore(git-walk): Git walk update for cis-benchmarks
This commit is contained in:
1 parent
093a846ac7
commit
d796d39326
9 files changed
+1613
No files matched your search
@@ -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
@@ -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 "$@"
|
||||
@@ -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"]
|
||||
@@ -0,0 +1 @@
|
||||
"""CIS benchmark parsing and checking helpers."""
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Binary file not shown.
@@ -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()
|
||||
Reference in new issue
Block a user