Webapp redesign: fixes from the code review
Blocker - /reports/export returned 500: REPORT_EXPORT_RANGES and _months_ago were deleted with the old reports route. Restored. Major - A session without stage_index/stage_total took down /, /imaging and /imaging/tiles (int filter on Undefined). The templates default both; saving a LAPS password for a session that no longer exists is refused (it created a bare session). - /reports/todo parsed every report four times (23 s at 700 reports). blancco_report.parse() now caches by path, mtime and size: 0.09 s. - Stored XSS through the session serial in the Clear confirm (the browser decodes ' before running an inline handler). The message moves to data-confirm, and update_session() only accepts serials of letters, digits, '.', '_' and '-' (400 from /imaging/status otherwise). - Reports without a document id and with odd characters in the file name got a key the mark routes rejected, so they could never be cleared. report_key() now always produces a valid key. Minor - failure_note() handles a list-valued erasure target (500 on the report and to-do pages). - _recently_marked() tolerates naive, 'Z' and non-dict state entries. - Key regexes use fullmatch (a trailing newline was accepted). - Unreadable reports are no longer counted as to tick / to destroy; the reports page already flags them as Unreadable. - Rollback review recounts restorable files after reading the backups. - Enrollment: the startnet fallback package (no or empty ppkg.conf) and a quoted SOURCE_PPKG are protected too. - Backup upload overwrite check is case-insensitive (SMB share) and replaces the existing file rather than adding a case twin. - Overview polling keeps the last good rows on an error and pauses while the tab is hidden. A shown or typed LAPS password survives the floor's refresh. - hardware_dismiss only redirects to same-site paths; tiles ?limit is clamped; the Updates overview survives an unreadable Dell scan or iPXE state; describe() uses aversion(); a client cannot label its session 'inferred'.
This commit is contained in:
@@ -38,7 +38,11 @@ _REPORT_KEY = re.compile(f"{_DOC_ID.pattern}|{_FILE_KEY.pattern}")
|
||||
|
||||
def report_key(document_id: str, filename: str) -> str:
|
||||
"""The state-file key for one report: its document id, else "file:<name>"."""
|
||||
return document_id if _DOC_ID.match(document_id or "") else f"file:{filename}"
|
||||
if _DOC_ID.fullmatch(document_id or ""):
|
||||
return document_id
|
||||
# Keys travel in URLs and must match _FILE_KEY, so odd characters in a
|
||||
# hand-made report's name ("report (1).xml") become "_".
|
||||
return "file:" + re.sub(r"[^A-Za-z0-9._-]", "_", filename)[:200]
|
||||
|
||||
|
||||
# --- state ------------------------------------------------------------------
|
||||
@@ -68,7 +72,7 @@ def _now() -> str:
|
||||
|
||||
|
||||
def record_cloud(document_id: str, sent: bool, serial: str = "", detail: str = ""):
|
||||
if not _DOC_ID.match(document_id or ""):
|
||||
if not _DOC_ID.fullmatch(document_id or ""):
|
||||
raise ValueError("bad document_id")
|
||||
state = load_state()
|
||||
state["cloud"][document_id] = {
|
||||
@@ -80,7 +84,7 @@ def record_cloud(document_id: str, sent: bool, serial: str = "", detail: str = "
|
||||
|
||||
|
||||
def mark_servicenow(document_id: str, done: bool = True):
|
||||
if not _REPORT_KEY.match(document_id or ""):
|
||||
if not _REPORT_KEY.fullmatch(document_id or ""):
|
||||
raise ValueError("bad document_id")
|
||||
state = load_state()
|
||||
if done:
|
||||
@@ -118,10 +122,11 @@ def collect(since=None, until=None) -> list[dict]:
|
||||
path = os.path.join(folder, f)
|
||||
if not f.lower().endswith(".xml") or f.startswith(".") or not os.path.isfile(path):
|
||||
continue
|
||||
unreadable = False
|
||||
try:
|
||||
data = blancco_report.parse(path)
|
||||
except Exception:
|
||||
data = {}
|
||||
data, unreadable = {}, True
|
||||
when = blancco_report.report_datetime(data) or datetime.fromtimestamp(
|
||||
os.stat(path).st_mtime, timezone.utc)
|
||||
if since and when < since:
|
||||
@@ -135,6 +140,7 @@ def collect(since=None, until=None) -> list[dict]:
|
||||
"when": when,
|
||||
"data": data,
|
||||
"document_id": (data.get("meta") or {}).get("document_id", ""),
|
||||
"unreadable": unreadable,
|
||||
})
|
||||
out[-1]["key"] = report_key(out[-1]["document_id"], f)
|
||||
return out
|
||||
@@ -197,6 +203,14 @@ def build_evidence_zip(month: str, fileobj) -> int:
|
||||
return len(reports)
|
||||
|
||||
|
||||
def _target(er: dict) -> dict:
|
||||
"""An erasure's target; parse() makes a repeated target a list."""
|
||||
t = er.get("target") or {}
|
||||
if isinstance(t, list):
|
||||
t = t[0] if t else {}
|
||||
return t if isinstance(t, dict) else {}
|
||||
|
||||
|
||||
def failure_note(data: dict):
|
||||
"""(failed erasures, ServiceNow note) for a parsed report. A drive is
|
||||
failed only if none of its erasures succeeded (a Purge failure followed
|
||||
@@ -210,7 +224,7 @@ def failure_note(data: dict):
|
||||
return failed, ""
|
||||
reasons = [blancco_report.failure_reason(e) for e in failed]
|
||||
drives = ", ".join(
|
||||
f"{(e.get('target') or {}).get('model', '')} {(e.get('target') or {}).get('serial', '')}".strip()
|
||||
f"{_target(e).get('model', '')} {_target(e).get('serial', '')}".strip()
|
||||
for e in failed)
|
||||
return failed, (f"Blancco wipe failed on {drives or 'drive'}: "
|
||||
f"{'; '.join(x.rstrip('. ') for x in reasons if x) or 'no erasure recorded'}. "
|
||||
@@ -223,6 +237,8 @@ def servicenow_items(days: int = 30, include_done: bool = False) -> list[dict]:
|
||||
state = load_state()
|
||||
items = []
|
||||
for r in reversed(collect(since)):
|
||||
if r.get("unreadable"):
|
||||
continue # /reports flags it as Unreadable; nothing to tick or destroy
|
||||
done = state["servicenow"].get(r["key"])
|
||||
if done and not include_done:
|
||||
continue
|
||||
@@ -260,7 +276,7 @@ _EVIDENCE_ZIP = re.compile(r"^ENT-056_.+_(20\d\d-\d\d)\.zip$")
|
||||
|
||||
def mark(kind: str, key: str, done: bool = True):
|
||||
pattern = TODO_KINDS.get(kind)
|
||||
if not pattern or not pattern.match(key or ""):
|
||||
if not pattern or not pattern.fullmatch(key or ""):
|
||||
raise ValueError("bad to-do item")
|
||||
state = load_state()
|
||||
bucket = state.setdefault(kind, {})
|
||||
|
||||
Reference in New Issue
Block a user