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:
cproudlock
2026-10-14 14:42:21 -04:00
parent fd00d5ff89
commit 2bcc9d8016
10 changed files with 131 additions and 36 deletions

View File

@@ -86,6 +86,13 @@ def update_session(payload: dict) -> dict:
serial = (payload.get("serial") or "").strip()
if not serial:
raise ValueError("payload missing 'serial'")
# "inferred" is the server's own label for log-only bays; a client that
# sends it would hide its row from the floor's failure-first order.
if payload.get("source") not in (None, "client", "blancco"):
payload = {k: v for k, v in payload.items() if k != "source"}
# Serials end up in file names and in page markup: keep them plain.
if not re.fullmatch(r"[A-Za-z0-9._-]{1,64}", serial):
raise ValueError("serial must be 1-64 letters, digits, '.', '_' or '-'")
_ensure_dir()
path = _path_for(serial)
@@ -234,7 +241,7 @@ def hide_stale(sessions: list[dict], hours: float = ACTIVE_WINDOW_HOURS) -> tupl
when = datetime.fromisoformat(s.get("last_updated") or "")
if when.tzinfo is None:
when = when.astimezone()
except ValueError:
except (ValueError, TypeError):
when = None
if when is not None and when < cutoff:
hidden += 1