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

@@ -29,6 +29,23 @@
<p class="small text-muted mt-2 mb-0">Dated by the timestamp inside each report, not the file date.</p>
</div>
<div class="section-card">
<div class="section-title">CMMC Evidence - {{ site }}</div>
<div class="d-flex flex-wrap align-items-center gap-3">
<form method="get" action="{{ url_for('blancco_evidence_pack') }}" class="d-flex flex-wrap align-items-center gap-2">
<label for="evidenceMonth" class="mb-0">Evidence pack (ENT-056/158) for</label>
<input id="evidenceMonth" type="month" name="month" value="{{ last_month }}" class="form-control form-control-sm w-auto" required>
<button type="submit" class="pxe-btn pxe-btn-secondary" title="summary.csv plus every report's XML and PDF for that month">
<i class="bi bi-file-zip"></i> Download
</button>
</form>
<a href="{{ url_for('blancco_servicenow') }}" class="pxe-btn pxe-btn-secondary" title="What to tick on each Onsite Bin Management RITM">
<i class="bi bi-list-check"></i> ServiceNow tick list (ENT-045/159)
</a>
</div>
<p class="small text-muted mt-2 mb-0">The evidence pack for last month is also saved automatically to <span class="mono">evidence\</span> on the reports share on the 1st. Certificates of record are in the Blancco Management Portal.</p>
</div>
<div class="section-card">
<div class="section-title">Drive Erasure Certificates</div>
{% if reports %}
@@ -40,6 +57,7 @@
<th>Model</th>
<th>Date</th>
<th>Result</th>
<th title="Whether Blancco reported uploading this report to the Blancco cloud">Cloud</th>
<th class="text-end">Actions</th>
</tr>
</thead>
@@ -55,6 +73,11 @@
{% elif r.state %}<span class="badge badge-secondary">{{ r.state }}</span>
{% else %}<span class="text-muted">-</span>{% endif %}
</td>
<td>
{% if r.cloud == 'sent' %}<span class="badge badge-success">Sent</span>
{% elif r.cloud == 'failed' %}<span class="badge badge-danger" title="Resend from Blancco, or upload the XML to the Blancco portal">Not sent</span>
{% else %}<span class="text-muted" title="No upload status recorded (older report, or saved manually)">-</span>{% endif %}
</td>
<td class="actions text-end text-nowrap">
{% if r.filename.lower().endswith('.xml') %}
<a href="{{ url_for('blancco_view_report', filename=r.filename) }}"

View File

@@ -0,0 +1,104 @@
{% extends "base.html" %}
{% block title %}ServiceNow Tick List - PXE Server Manager{% endblock %}
{% block content %}
<div class="page-header">
<h1><i class="bi bi-list-check"></i> ServiceNow Tick List</h1>
<div class="header-actions">
<a href="{{ url_for('blancco_reports') }}" class="pxe-btn pxe-btn-ghost"><i class="bi bi-arrow-left"></i> Erasure reports</a>
</div>
</div>
<div class="section-card">
<div class="section-title">Onsite Bin Management RITM (ENT-045 / ENT-159)</div>
<p class="mb-2">
For each wiped asset, open its Onsite Bin Management RITM and tick
<strong>Wiped</strong>, or <strong>Blancco Wipe failed</strong> with the destruction note below.
Then click <strong>Done</strong> here so it drops off the list.
</p>
<form method="get" class="d-flex flex-wrap align-items-center gap-2 mb-0">
<label for="days" class="mb-0">Last</label>
<select id="days" name="days" class="form-select form-select-sm w-auto" onchange="this.form.submit()">
{% for d in (7, 30, 90, 366) %}<option value="{{ d }}" {% if d == days %}selected{% endif %}>{{ d }} days</option>{% endfor %}
</select>
<label class="mb-0 ms-2"><input type="checkbox" name="all" value="1" {% if include_done %}checked{% endif %} onchange="this.form.submit()"> show done</label>
</form>
</div>
<div class="section-card">
{% if items %}
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>Serial</th>
<th>Model</th>
<th>Wiped (UTC)</th>
<th>Tick in ServiceNow</th>
<th>Destruction note</th>
<th>Cloud</th>
<th class="text-end">Done</th>
</tr>
</thead>
<tbody>
{% for it in items %}
<tr title="{{ it.file }}">
<td class="text-nowrap">
<span class="mono">{{ it.serial or '-' }}</span>
{% if it.serial %}<button type="button" class="btn btn-sm btn-outline-secondary btn-row-action copy-btn" data-copy-text="{{ it.serial }}">copy</button>{% endif %}
</td>
<td>{{ it.model or '-' }}</td>
<td class="mono text-nowrap">{{ it.when.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
{% if it.failed %}<span class="badge badge-danger">Blancco Wipe failed</span>
{% else %}<span class="badge badge-success">Wiped</span>{% endif %}
</td>
<td style="max-width: 28rem;">
{% if it.note %}
<div class="small">{{ it.note }}</div>
<button type="button" class="btn btn-sm btn-outline-secondary btn-row-action copy-btn mt-1" data-copy-text="{{ it.note }}">copy note</button>
{% else %}<span class="text-muted">-</span>{% endif %}
</td>
<td>
{% if it.cloud == 'sent' %}<span class="badge badge-success">Sent</span>
{% elif it.cloud == 'failed' %}<span class="badge badge-danger">Not sent</span>
{% else %}<span class="text-muted">-</span>{% endif %}
</td>
<td class="text-end text-nowrap">
<form method="post" action="{{ url_for('blancco_servicenow_mark', document_id=it.document_id, all='1' if include_done else None) }}" style="display:inline;">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
{% if it.done %}
<input type="hidden" name="done" value="0">
<button type="submit" class="pxe-btn pxe-btn-ghost btn-row-action" title="Ticked {{ it.done }}">Undo</button>
{% else %}
<input type="hidden" name="done" value="1">
<button type="submit" class="pxe-btn pxe-btn-success btn-row-action">Done</button>
{% endif %}
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center text-muted py-5">
<p class="mb-1">Nothing left to tick for the last {{ days }} days.</p>
</div>
{% endif %}
</div>
{% endblock %}
{% block extra_scripts %}
<script>
document.querySelectorAll('.copy-btn').forEach(function (b) {
b.addEventListener('click', function () {
var t = b.getAttribute('data-copy-text') || '';
var done = function () { var o = b.textContent; b.textContent = 'copied'; setTimeout(function () { b.textContent = o; }, 1200); };
if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(t).then(done); return; }
var ta = document.createElement('textarea'); ta.value = t; document.body.appendChild(ta); ta.select();
try { document.execCommand('copy'); done(); } finally { document.body.removeChild(ta); }
});
});
</script>
{% endblock %}