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:
@@ -40,6 +40,7 @@ from werkzeug.utils import secure_filename
|
||||
|
||||
import config
|
||||
from services import (
|
||||
blancco_evidence,
|
||||
blancco_report,
|
||||
deploy,
|
||||
fs,
|
||||
@@ -541,10 +542,12 @@ def clonezilla_delete(filename):
|
||||
@app.route("/reports")
|
||||
def blancco_reports():
|
||||
reports = []
|
||||
state = blancco_evidence.load_state()
|
||||
if os.path.isdir(config.BLANCCO_REPORTS):
|
||||
for f in sorted(os.listdir(config.BLANCCO_REPORTS), reverse=True):
|
||||
fpath = os.path.join(config.BLANCCO_REPORTS, f)
|
||||
if not os.path.isfile(fpath):
|
||||
# .part files are reports still being copied in by Blancco.
|
||||
if not os.path.isfile(fpath) or f.startswith(".") or f.endswith(".part"):
|
||||
continue
|
||||
stat = os.stat(fpath)
|
||||
ext = os.path.splitext(f)[1].lower()
|
||||
@@ -554,7 +557,8 @@ def blancco_reports():
|
||||
# negligible at fleet sizes).
|
||||
serial = ""
|
||||
model = ""
|
||||
state = ""
|
||||
result = ""
|
||||
cloud = ""
|
||||
if ext == ".xml":
|
||||
try:
|
||||
data = blancco_report.parse(fpath)
|
||||
@@ -567,11 +571,13 @@ def blancco_reports():
|
||||
erasures = data.get("erasures") or []
|
||||
states = [(e.get("state") or "").strip() for e in erasures if e.get("state")]
|
||||
if not states:
|
||||
state = ""
|
||||
result = ""
|
||||
elif any(s.lower() != "successful" for s in states):
|
||||
state = "Failed"
|
||||
result = "Failed"
|
||||
else:
|
||||
state = "Successful"
|
||||
result = "Successful"
|
||||
cloud = blancco_evidence.cloud_status(
|
||||
state, (data.get("meta") or {}).get("document_id", ""))
|
||||
except Exception:
|
||||
pass
|
||||
reports.append({
|
||||
@@ -581,11 +587,14 @@ def blancco_reports():
|
||||
"type": ext.lstrip(".").upper() or "FILE",
|
||||
"serial": serial,
|
||||
"model": model,
|
||||
"state": state,
|
||||
"state": result,
|
||||
"cloud": cloud,
|
||||
})
|
||||
return render_template(
|
||||
"reports.html",
|
||||
reports=reports,
|
||||
site=config.BLANCCO_SITE,
|
||||
last_month=blancco_evidence.previous_month(),
|
||||
image_types=config.IMAGE_TYPES,
|
||||
friendly_names=config.FRIENDLY_NAMES,
|
||||
)
|
||||
@@ -620,6 +629,7 @@ def blancco_export_reports():
|
||||
# when a report has no parseable date. PDFs (7.20 writes pdf+xml) ride
|
||||
# along with the XML of the same name.
|
||||
rows, files = [], []
|
||||
cloud_state = blancco_evidence.load_state()
|
||||
folder = config.BLANCCO_REPORTS
|
||||
names = sorted(os.listdir(folder)) if os.path.isdir(folder) else []
|
||||
for f in names:
|
||||
@@ -634,7 +644,9 @@ def blancco_export_reports():
|
||||
os.stat(fpath).st_mtime, timezone.utc)
|
||||
if when < since:
|
||||
continue
|
||||
rows.extend(blancco_report.export_rows(f, data, when.isoformat()))
|
||||
rows.extend(blancco_report.export_rows(
|
||||
f, data, when.isoformat(),
|
||||
blancco_evidence.cloud_status(cloud_state, (data.get("meta") or {}).get("document_id", ""))))
|
||||
files.append(f)
|
||||
pdf = os.path.splitext(f)[0] + ".pdf"
|
||||
if pdf in names:
|
||||
@@ -664,6 +676,54 @@ def blancco_export_reports():
|
||||
download_name=f"{stem}.zip")
|
||||
|
||||
|
||||
@app.route("/reports/cloud-status", methods=["POST"])
|
||||
def blancco_cloud_status():
|
||||
# CSRF-exempt machine-to-machine endpoint: blancco-progress.py, running
|
||||
# inside Blancco, reports whether each report reached the Blancco cloud.
|
||||
p = request.get_json(silent=True) or {}
|
||||
try:
|
||||
blancco_evidence.record_cloud(
|
||||
p.get("document_id", ""), bool(p.get("sent")), p.get("serial", ""), p.get("detail", ""))
|
||||
except ValueError as ex:
|
||||
return jsonify({"error": str(ex)}), 400
|
||||
return jsonify({"ok": True}), 200
|
||||
|
||||
|
||||
@app.route("/reports/evidence")
|
||||
def blancco_evidence_pack():
|
||||
"""CMMC evidence pack (ENT-056/158): one ZIP per site and month."""
|
||||
month = request.args.get("month") or blancco_evidence.previous_month()
|
||||
if not re.fullmatch(r"20\d\d-(0[1-9]|1[0-2])", month):
|
||||
abort(400)
|
||||
out = tempfile.SpooledTemporaryFile(max_size=64 * 1024 * 1024)
|
||||
count = blancco_evidence.build_evidence_zip(month, out)
|
||||
out.seek(0)
|
||||
audit("REPORT_EVIDENCE", f"{month} {count} reports")
|
||||
return send_file(out, mimetype="application/zip", as_attachment=True,
|
||||
download_name=blancco_evidence.evidence_name(month) + ".zip")
|
||||
|
||||
|
||||
@app.route("/reports/servicenow")
|
||||
def blancco_servicenow():
|
||||
"""What to tick on each asset's Onsite Bin Management RITM (ENT-045/159)."""
|
||||
include_done = request.args.get("all") == "1"
|
||||
days = request.args.get("days", 30, type=int)
|
||||
items = blancco_evidence.servicenow_items(days=max(1, min(days, 366)), include_done=include_done)
|
||||
return render_template("reports_servicenow.html", items=items,
|
||||
include_done=include_done, days=days)
|
||||
|
||||
|
||||
@app.route("/reports/servicenow/<document_id>", methods=["POST"])
|
||||
def blancco_servicenow_mark(document_id):
|
||||
done = request.form.get("done", "1") == "1"
|
||||
try:
|
||||
blancco_evidence.mark_servicenow(document_id, done)
|
||||
except ValueError:
|
||||
abort(400)
|
||||
audit("REPORT_SERVICENOW", f"{document_id} {'done' if done else 'undone'}")
|
||||
return redirect(url_for("blancco_servicenow", all=request.args.get("all")))
|
||||
|
||||
|
||||
@app.route("/reports/download/<filename>")
|
||||
def blancco_download_report(filename):
|
||||
filename = secure_filename(filename)
|
||||
|
||||
Reference in New Issue
Block a user