blancco: CMMC evidence pack, ServiceNow tick list, cloud-upload status

Automates the Blancco parts of the onsite-support CMMC playbook:

- ENT-056/158 evidence pack: /reports/evidence?month=YYYY-MM returns
  ENT-056_<Site>_<YYYY-MM>.zip (summary.csv, README, each report's XML
  and PDF). pxe-blancco-evidence.timer saves last month's pack to the
  reports share's evidence/ folder on the 1st (Persistent).
- ENT-045/159 ServiceNow tick list: /reports/servicenow lists recent
  reports as "Wiped" or "Blancco Wipe failed" with a pre-written
  destruction note, copy buttons and Done (CSRF-protected).
- Cloud upload: blancco-progress.py posts each report's upload result
  ("Sending report document id" -> "Report sent to MC") to
  /reports/cloud-status (CSRF-exempt like /imaging/status); the Reports
  page and the CSVs show it, so a wipe missing from BMP is visible.
- CSV gains failure_reason (exception messages, failed steps, error
  counts), business_location and cloud_upload.
- Preferences: business_location "West Jefferson" (config.BLANCCO_SITE)
  and pdf+xml reports; the report copier now copies the PDF too, named
  after its XML. .part and hidden files are not listed as reports.

State (cloud confirmations, ServiceNow ticks) is a JSON file at
/var/lib/pxe-webapp/blancco-state.json, off the report share.
This commit is contained in:
cproudlock
2026-10-05 13:43:16 -04:00
parent eff956ae6c
commit 44a7eeae95
12 changed files with 590 additions and 26 deletions

View File

@@ -125,12 +125,54 @@ EXPORT_COLUMNS = [
"system_manufacturer", "system_model", "system_serial",
"drive_model", "drive_serial", "drive_capacity_gb", "drive_interface",
"erasure_standard", "state", "start_time", "end_time", "elapsed_time",
"total_errors", "license_consumption_id", "business_name",
"total_errors", "failure_reason", "license_consumption_id",
"business_name", "business_location", "cloud_upload",
]
def export_rows(filename: str, data: dict, when: str) -> list[dict]:
"""One CSV row per erased drive; a report with no erasures still gets a row."""
def _as_list(x):
if x is None or x == "":
return []
return x if isinstance(x, list) else [x]
def failure_reason(er: dict) -> str:
"""Why an erasure did not succeed, from the report itself; "" if it did.
Blancco records exception messages under erasure_details (e.g.
"NBP area content was not erased."), a state and error count per step,
and total/read/mismatch error counts. Auditors (ENT-158/159) want the
cause of failure next to each failed wipe.
"""
if (er.get("state") or "").strip().lower() == "successful":
return ""
parts = []
details = er.get("erasure_details") or {}
for exc in _as_list(details.get("exception")) + _as_list(details.get("messages")):
msg = exc.get("message") if isinstance(exc, dict) else exc
if msg and msg not in parts:
parts.append(msg)
steps = (er.get("steps") or {}).get("step")
for i, st in enumerate(_as_list(steps), 1):
if isinstance(st, dict) and (st.get("state") or "completed") != "completed":
name = st.get("name") or st.get("step_name") or f"step {i}"
parts.append(f"{name}: {st.get('state')}")
for key in ("total_errors", "read_errors", "mismatch_errors"):
try:
n = int(er.get(key) or 0)
except ValueError:
n = 0
if n:
parts.append(f"{key.replace('_', ' ')}: {n}")
return "; ".join(parts) or f"state: {er.get('state') or 'unknown'}"
def export_rows(filename: str, data: dict, when: str, cloud: str = "") -> list[dict]:
"""One CSV row per erased drive; a report with no erasures still gets a row.
cloud is the upload status recorded by the webapp for this report
("sent", "failed" or "" when unknown).
"""
meta = data.get("meta") or {}
system = (data.get("hardware") or {}).get("system") or {}
if isinstance(system, list):
@@ -145,6 +187,8 @@ def export_rows(filename: str, data: dict, when: str) -> list[dict]:
"system_serial": system.get("serial", ""),
"license_consumption_id": ";".join(data.get("license_consumption_ids") or []),
"business_name": (data.get("company") or {}).get("business_name", ""),
"business_location": (data.get("company") or {}).get("business_location", ""),
"cloud_upload": cloud,
}
rows = []
for er in data.get("erasures") or []:
@@ -167,5 +211,6 @@ def export_rows(filename: str, data: dict, when: str) -> list[dict]:
"end_time": er.get("end_time", ""),
"elapsed_time": er.get("elapsed_time", ""),
"total_errors": er.get("total_errors", ""),
"failure_reason": failure_reason(er),
})
return rows or [base]