CIS_Benchmarks/nginx_audit/report.py
2026-06-18 20:47:15 +03:00

277 lines
9.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Generate an XLSX audit report with four sheets:
1. Summary – score, counts, target info
2. Passed – checks that passed, actual vs expected
3. Failed – checks that failed, actual vs expected, remediation
4. Manual Review – checks requiring human judgment, command output + criteria
5. Errors – checks that could not run
Uses openpyxl (bundled with most Python environments).
"""
from __future__ import annotations
from datetime import datetime
from pathlib import Path
from typing import List
import openpyxl
from openpyxl.styles import (
Alignment, Font, PatternFill, Border, Side
)
from openpyxl.utils import get_column_letter
from .models import AuditReport, CheckResult, PASS, FAIL, MANUAL, ERROR
# ── colour palette ────────────────────────────────────────────────────────────
C_HEADER_BG = "1F4E79" # dark blue – header row
C_HEADER_FG = "FFFFFF" # white
C_PASS_BG = "E2EFDA" # light green
C_FAIL_BG = "FCE4D6" # light red/orange
C_MANUAL_BG = "FFF2CC" # light yellow
C_ERROR_BG = "F2CEEF" # light purple
C_ALT_BG = "F5F5F5" # very light grey – alternating rows
C_SUMMARY_BG = "DEEAF1" # light blue – summary labels
# thin border
_THIN = Side(style="thin", color="BFBFBF")
_BORDER = Border(left=_THIN, right=_THIN, top=_THIN, bottom=_THIN)
# ── helpers ───────────────────────────────────────────────────────────────────
def _header_font() -> Font:
return Font(name="Calibri", bold=True, color=C_HEADER_FG, size=11)
def _bold(color: str = "000000") -> Font:
return Font(name="Calibri", bold=True, color=color, size=11)
def _normal(size: int = 11) -> Font:
return Font(name="Calibri", size=size)
def _fill(hex_color: str) -> PatternFill:
return PatternFill("solid", fgColor=hex_color)
def _wrap_align() -> Alignment:
return Alignment(wrap_text=True, vertical="top")
def _set_header_row(ws, headers: list[str], row: int = 1) -> None:
for col, title in enumerate(headers, 1):
cell = ws.cell(row=row, column=col, value=title)
cell.font = _header_font()
cell.fill = _fill(C_HEADER_BG)
cell.alignment = Alignment(wrap_text=True, vertical="center", horizontal="center")
cell.border = _BORDER
def _write_row(ws, row: int, values: list, bg_color: str | None = None) -> None:
for col, val in enumerate(values, 1):
cell = ws.cell(row=row, column=col, value=str(val) if val is not None else "")
cell.font = _normal()
cell.alignment = _wrap_align()
cell.border = _BORDER
if bg_color:
cell.fill = _fill(bg_color)
elif row % 2 == 0:
cell.fill = _fill(C_ALT_BG)
def _auto_width(ws, min_w: int = 12, max_w: int = 60) -> None:
for col_cells in ws.columns:
length = max(
len(str(cell.value or "")) for cell in col_cells
)
col_letter = get_column_letter(col_cells[0].column)
ws.column_dimensions[col_letter].width = max(min_w, min(length + 2, max_w))
def _freeze(ws, cell: str = "A2") -> None:
ws.freeze_panes = cell
# ── sheet builders ────────────────────────────────────────────────────────────
def _build_summary(wb: openpyxl.Workbook, report: AuditReport) -> None:
ws = wb.active
ws.title = "Summary"
ws.sheet_view.showGridLines = False
rows = [
("CIS NGINX Benchmark v3.0.0 – Audit Report", ""),
("", ""),
("Target", report.target),
("Profile", report.profile),
("Timestamp", report.timestamp.strftime("%Y-%m-%d %H:%M:%S UTC")),
("", ""),
("Total Checks", report.total),
("✅ PASS", len(report.passed)),
("❌ FAIL", len(report.failed)),
("⚠️ MANUAL", len(report.manual)),
("🔴 ERROR", len(report.errors)),
("", ""),
]
if report.total:
pct_pass = round(len(report.passed) / report.total * 100, 1)
pct_fail = round(len(report.failed) / report.total * 100, 1)
rows.append(("Pass Rate", f"{pct_pass}%"))
rows.append(("Fail Rate", f"{pct_fail}%"))
for r_idx, (label, value) in enumerate(rows, 1):
if r_idx == 1:
cell = ws.cell(row=r_idx, column=1, value=label)
cell.font = Font(name="Calibri", bold=True, size=16, color=C_HEADER_BG)
cell.alignment = Alignment(vertical="center")
ws.row_dimensions[r_idx].height = 30
ws.merge_cells(start_row=r_idx, start_column=1, end_row=r_idx, end_column=2)
else:
lbl_cell = ws.cell(row=r_idx, column=1, value=label)
val_cell = ws.cell(row=r_idx, column=2, value=value)
if label and label not in ("",):
lbl_cell.font = _bold(C_HEADER_BG)
lbl_cell.fill = _fill(C_SUMMARY_BG)
val_cell.font = _normal()
ws.column_dimensions["A"].width = 22
ws.column_dimensions["B"].width = 40
def _build_passed(wb: openpyxl.Workbook, results: list[CheckResult]) -> None:
ws = wb.create_sheet("Passed")
ws.sheet_view.showGridLines = False
headers = ["Check ID", "Title", "Commands Run", "Actual Value", "Expected Value"]
_set_header_row(ws, headers)
for r_idx, res in enumerate(results, 2):
_write_row(ws, r_idx, [
res.check_id,
res.title,
"\n".join(res.commands_run),
res.actual_value,
res.expected_value,
], bg_color=C_PASS_BG)
ws.row_dimensions[r_idx].height = max(
30, min(len(res.actual_value) // 3, 120)
)
_auto_width(ws)
_freeze(ws)
ws.column_dimensions["A"].width = 12
ws.column_dimensions["B"].width = 52
ws.column_dimensions["C"].width = 55
ws.column_dimensions["D"].width = 55
ws.column_dimensions["E"].width = 45
def _build_failed(wb: openpyxl.Workbook, results: list[CheckResult]) -> None:
ws = wb.create_sheet("Failed")
ws.sheet_view.showGridLines = False
headers = [
"Check ID", "Title", "Commands Run",
"Actual Value", "Expected Value", "Remediation Steps", "Notes"
]
_set_header_row(ws, headers)
for r_idx, res in enumerate(results, 2):
_write_row(ws, r_idx, [
res.check_id,
res.title,
"\n".join(res.commands_run),
res.actual_value,
res.expected_value,
res.remediation,
res.notes,
], bg_color=C_FAIL_BG)
ws.row_dimensions[r_idx].height = max(
30, min(max(len(res.actual_value), len(res.remediation)) // 3, 150)
)
_auto_width(ws)
_freeze(ws)
ws.column_dimensions["A"].width = 12
ws.column_dimensions["B"].width = 52
ws.column_dimensions["C"].width = 55
ws.column_dimensions["D"].width = 55
ws.column_dimensions["E"].width = 45
ws.column_dimensions["F"].width = 60
ws.column_dimensions["G"].width = 40
def _build_manual(wb: openpyxl.Workbook, results: list[CheckResult]) -> None:
ws = wb.create_sheet("Manual Review")
ws.sheet_view.showGridLines = False
headers = [
"Check ID", "Title", "Commands Run",
"Command Output (Actual)", "What to Look For (Expected)", "Notes"
]
_set_header_row(ws, headers)
for r_idx, res in enumerate(results, 2):
_write_row(ws, r_idx, [
res.check_id,
res.title,
"\n".join(res.commands_run),
res.actual_value,
res.expected_value,
res.notes,
], bg_color=C_MANUAL_BG)
ws.row_dimensions[r_idx].height = max(
30, min(len(res.actual_value) // 3, 150)
)
_auto_width(ws)
_freeze(ws)
ws.column_dimensions["A"].width = 12
ws.column_dimensions["B"].width = 52
ws.column_dimensions["C"].width = 55
ws.column_dimensions["D"].width = 60
ws.column_dimensions["E"].width = 55
ws.column_dimensions["F"].width = 40
def _build_errors(wb: openpyxl.Workbook, results: list[CheckResult]) -> None:
if not results:
return
ws = wb.create_sheet("Errors")
ws.sheet_view.showGridLines = False
headers = ["Check ID", "Title", "Commands Run", "Error Detail", "Notes"]
_set_header_row(ws, headers)
for r_idx, res in enumerate(results, 2):
_write_row(ws, r_idx, [
res.check_id,
res.title,
"\n".join(res.commands_run),
res.actual_value,
res.notes,
], bg_color=C_ERROR_BG)
_auto_width(ws)
_freeze(ws)
# ── public API ────────────────────────────────────────────────────────────────
def generate_report(report: AuditReport, output_path: str | Path) -> Path:
"""
Write the audit report as an XLSX file to *output_path*.
Returns the resolved Path.
"""
wb = openpyxl.Workbook()
_build_summary(wb, report)
_build_passed(wb, report.passed)
_build_failed(wb, report.failed)
_build_manual(wb, report.manual)
_build_errors(wb, report.errors)
path = Path(output_path)
path.parent.mkdir(parents=True, exist_ok=True)
wb.save(path)
return path