{# P3-T20 / P3-T21 — per-step Scrub Explorer. Reproduces §4·e of the scan-debug redesign. Renders the scrub step's redaction manifest (counts produced by ``pii_scrubber.py``) plus a hardcoded sample-redactions table -- real blob-diffing is non-trivial and intentionally out of scope (P5 follow-up). Inputs (assembled by ``routes.debug._scrub_step_view``): * ``view.scan_id`` / ``view.scan_id_short`` / ``view.back_to_trace_url`` * ``view.step_index`` / ``view.step_index_padded`` / ``view.total_step_count`` * ``view.step_name`` / ``view.step_type`` / ``view.duration`` * ``view.status_label`` / ``view.status_pill_class`` * ``view.is_in_progress`` / ``view.is_cancelled_scan`` * ``view.error_class`` / ``view.error_message`` / ``view.owner_email`` * ``view.capture_size_label`` — human "4.2 MB" or ``""`` * ``view.capture_blob_url`` — S3 path of the captured (pre-scrub) blob, or ``""`` * ``view.rules_version`` — int from the manifest, or ``""`` * ``view.manifest_rows`` — list of ``{key, value, value_class, annotation}`` * ``view.items_kept_label`` — "requests 164 · cookies 37 · interactions 22" or ``""`` * ``view.artifact_counts`` — list of ``{label, value}`` for the INPUT card * ``view.has_manifest`` — bool; false for pre-T14 scans Sample redactions are hardcoded (P3-T21 Option B) — a muted note marks them as not pulled from this scan's blob. #} {% extends "admin_layout.html" %} {% from 'partials/help_tooltip.html' import tip, tip_styles %} {% block page_title %}admin :: step :: {{ view.scan_id_short }} / {{ view.step_index_padded }}{% endblock %} {% block breadcrumb %} admin/ scans/ {{ view.scan_id_short }}/ step {{ view.step_index_padded }} {% endblock %} {% block head_extra %} {{ tip_styles() }} {% if view.is_in_progress %}{% endif %} {% endblock %} {% block content %} {# ============== Sticky breadcrumb strip ============== #}
← back to trace · step {{ view.step_index_padded }} of {{ view.total_step_count }} {{ tip("The Scrub Explorer is a read-only audit surface. The stored capture + scrubbed blobs are never mutated from this page.", position="bottom") }}
{# ============== Eyebrow + headline ============== #}
02.1 — scan debug · step {{ view.step_index_padded }} · scrub

Step {{ view.step_index_padded }} — scrub {% if view.rules_version != "" %}· rules version {{ view.rules_version }}{% endif %} {% if view.is_cancelled_scan %}[cancelled scan]{% endif %}

{{ view.duration }}· {{ view.status_label }} {% if view.capture_size_label %} · {{ view.capture_size_label }} capture {% endif %} {% if is_admin and view.owner_email %} ·{{ view.owner_email }} {% endif %}
{# ============== In-progress banner (P3-T23) ============== #} {% if view.is_in_progress %}
// in progress step {{ view.step_index_padded }} (scrub) is still running — page auto-refreshes every 30s.
{% endif %} {# ============== Error banner (§4·g) ============== #} {% if view.error_class or view.error_message %} {% endif %} {# ============== INPUT card ============== #}
input {{ tip("Source artifacts fed into the scrubber: the captured blob in object storage, the scrub rules version in effect, and the per-artifact counts that survive into the scrubbed output.") }}
source {{ tip("Object-store path of the gzipped capture blob the scrubber consumed.") }}
{% if view.capture_blob_url %} {{ view.capture_blob_url }} {% if view.capture_size_label %}{{ view.capture_size_label }}{% endif %} {% else %} — no capture blob recorded {% endif %}
rules_version {{ tip("Pinned scrub-rule revision (browser_recon_server/scrubber/scrub_rules.py RULES_VERSION). Bumped when a rule is added or tightened so old + new manifests stay comparable.") }}
{% if view.rules_version != "" %} {{ view.rules_version }} {% else %} — not recorded {% endif %}
artifacts {{ tip("Per-artifact counts kept in the scrubbed blob (requests / cookies / interactions / websockets). Counts that drop to zero usually mean the scrubber refused to ship that category.") }}
{% if view.artifact_counts %} {% for c in view.artifact_counts %}{{ c.label }} {{ c.value }}{% if not loop.last %}·{% endif %}{% endfor %} {% else %} — items_kept not recorded {% endif %}
{# ============== REDACTION MANIFEST card ============== #}
redaction manifest {{ tip("Counts emitted by pii_scrubber.py while walking the capture. Cookies / auth headers / form inputs / URL params / response bodies are stripped in place; dropped bodies are replaced with a shape stub.") }}
{% if not view.has_manifest %}
scrub_manifest is empty or pre-T14 — no per-category counts to display.
{% else %}
{% for row in view.manifest_rows %}
{{ row.key }} {%- if row.key == 'cookie_values_stripped' %} {{ tip("Number of cookie values replaced with . Cookie names are preserved; only values go.") }} {%- elif row.key == 'auth_headers_stripped' %} {{ tip("Authorization / cookie / API-key header values redacted across requests + responses.") }} {%- elif row.key == 'form_inputs_stripped' %} {{ tip("Form / multipart fields whose value matched a sensitive-name pattern (password / token / ssn / etc.) and were replaced with a string-shape stub.") }} {%- elif row.key == 'url_params_scrubbed' %} {{ tip("Query-string parameters whose name matched a sensitive-key pattern (email / token / api_key / etc.) and were replaced with .") }} {%- elif row.key == 'response_bodies_scrubbed' %} {{ tip("JSON response bodies walked + value-shape-stubbed. Top-level keys preserved; leaf values replaced with type-shape annotations.") }} {%- elif row.key == 'response_bodies_dropped' %} {{ tip("Bodies the scrubber refused to walk (binary, opaque, oversized). Replaced with a single empty-body marker.") }} {%- elif row.key == 'items_kept' %} {{ tip("Counts that survive into the scrubbed blob. Compare against the source capture's totals to spot scrubber over-deletion.") }} {%- endif %}
{% if row.value_class %}{{ row.value }}{% else %}{{ row.value }}{% endif %} {% if row.annotation %}{{ row.annotation }}{% endif %}
{% endfor %} {% if view.items_kept_label %}
items_kept {{ tip("Counts that survive into the scrubbed blob (requests / cookies / interactions).") }}
{{ view.items_kept_label }}
{% endif %}
{% endif %}
{# ============== SAMPLE REDACTIONS card (P3-T21) ============== #}
sample redactions {{ tip("Representative examples of the kinds of redactions the scrubber performs. These are illustrative — not pulled from this scan's blob. Real per-scan diffing is a P5 follow-up.") }}
example redactions — not pulled from this scan's blob
field {{ tip("Path into the captured artifact, e.g. cookie._abck or body.user.password.") }} before {{ tip("The original captured value (illustrative; truncated).") }} after {{ tip("The post-scrub replacement. for hard redactions; type-shape stub (e.g. \"\") for JSON-body walks.") }} rule {{ tip("The scrub_rules.py family that fired for this redaction.") }}
cookie._abck B7vN1f…83 chars <scrubbed> cookie_value
header.authorization Bearer eyJ0eXAiOiJKV1Q… <scrubbed> header_allowlist
header.x-csrf-token 9f3a-… <scrubbed> header_suffix
url.query.email user@example.com <scrubbed> url_param_name
body.user.password "swordfish" "<string:9>" json_shape
{# ============== RE-RUN placeholder card (button intentionally non-functional; P5) ============== #}
re-run {{ tip("Re-runs the scrubber against the stored capture blob using current scrub_rules.py — useful for checking whether a newly-added rule would catch anything missed at scan time. Wiring lands in P5.") }}
diffs against original manifest · never overwrites stored blob
{% endblock %}