test(ci): extend zero-code-sink lint to console/static/app.js

Adds ``console/static/app.js`` to ``_UNSAFE_CODE_SINK_LINT_TARGETS``.
The parametrized scan now covers 8 static JS bundles — all admin-side
bundles are clean.

Docstring updates:

- Fully qualify the bundle paths in both posture lists
  (``ui/static/app.js``, ``shared_static/utils.js``, etc.) so the two
  ``app.js`` files are unambiguous now that both are in the targets
  list.
- ``console/static/app.js`` joins the **strict DOM-construction**
  list (alongside the interactive surface + shared helpers + coord
  chat entry) — the cluster-dashboard renderer is now full
  createElement / textContent construction, no HTML strings ever
  interpolated.
- ``console/static/admin.js`` + ``console/static/governance.js``
  remain in the **sink-free string-concat** list — they retain the
  escapeHtml + concat builder shape with the unsafe sink off the call
  site.

The ``insertAdjacent`` carve-out note (verdict-badge writers in
``ui/static/app.js``) is now qualified to avoid ambiguity.
This commit is contained in:
Patrick Buckley
2026-05-20 18:52:51 -07:00
parent 7f850c3acb
commit bc22dc5cb1
+25 -24
View File
@@ -263,9 +263,9 @@ _STYLE_CSS = Path(__file__).resolve().parent.parent / "turnstone/ui/static/style
# ``el.innerHTML\n = X``.
#
# ``insertAdjacent`` + HTML is *not* covered here because two existing
# call sites (``app.js:1287`` and ``app.js:1538``) consume
# ``renderVerdictBadge`` HTML output; broadening the lint would require
# cleaning that helper first. Tracked as a follow-up.
# call sites (``ui/static/app.js:1287`` and ``ui/static/app.js:1538``)
# consume ``renderVerdictBadge`` HTML output; broadening the lint
# would require cleaning that helper first. Tracked as a follow-up.
_UNSAFE_CODE_SINK_RE = re.compile(
r"\.(?:inner|outer)"
+ r"HTML\s*\+?=(?!=)"
@@ -364,6 +364,7 @@ _CONSOLE_ADMIN_JS = Path(__file__).resolve().parent.parent / "turnstone/console/
_CONSOLE_GOVERNANCE_JS = (
Path(__file__).resolve().parent.parent / "turnstone/console/static/governance.js"
)
_CONSOLE_APP_JS = Path(__file__).resolve().parent.parent / "turnstone/console/static/app.js"
_UNSAFE_CODE_SINK_LINT_TARGETS = [
@@ -374,6 +375,7 @@ _UNSAFE_CODE_SINK_LINT_TARGETS = [
("turnstone/console/static/coordinator/coordinator.js", _COORD_JS),
("turnstone/console/static/admin.js", _CONSOLE_ADMIN_JS),
("turnstone/console/static/governance.js", _CONSOLE_GOVERNANCE_JS),
("turnstone/console/static/app.js", _CONSOLE_APP_JS),
]
@@ -392,33 +394,32 @@ def test_no_unsafe_code_sinks_in_static_assets(label: str, path: Path) -> None:
Two distinct cleanup postures across the targets:
1. **Strict DOM-construction** (``app.js``, ``utils.js``,
``auth.js``, ``kb.js``, ``coordinator.js`` chat entry):
renderer output routes through ``setMarkdown`` (or
``setSafeHtml`` for pre-baked HTML strings); every other site
uses ``createElement`` + ``textContent`` + ``append`` /
``replaceChildren``. Missing escapes are structurally
impossible — no HTML string is ever interpolated.
2. **Sink-free string-concat** (``admin.js``, ``governance.js``):
operator-facing admin / governance pages still build HTML via
``escapeHtml`` + string concat, but the unsafe sink is off the
call site (everything routes through ``setSafeHtml``). XSS
defence still depends on every interpolated value going through
escapeHtml; the lint catches the sink but cannot catch a missing
escape.
1. **Strict DOM-construction** (``ui/static/app.js``,
``shared_static/utils.js``, ``shared_static/auth.js``,
``shared_static/kb.js``, ``coordinator.js`` chat entry,
``console/static/app.js``): renderer output routes through
``setMarkdown`` (or ``setSafeHtml`` for pre-baked HTML strings);
every other site uses ``createElement`` + ``textContent`` +
``append`` / ``replaceChildren``. Missing escapes are
structurally impossible — no HTML string is ever interpolated.
2. **Sink-free string-concat** (``console/static/admin.js``,
``console/static/governance.js``): operator-facing admin /
governance pages still build HTML via ``escapeHtml`` + string
concat, but the unsafe sink is off the call site (everything
routes through ``setSafeHtml``). XSS defence still depends on
every interpolated value going through escapeHtml; the lint
catches the sink but cannot catch a missing escape.
``console/static/app.js`` (the cluster dashboard / node table
surface) is the last admin-side bundle still pending — same posture
as admin.js once cleaned.
All admin-side bundles are now covered.
The regex covers inner/outer-HTML assignment (plain and
concat-assignment), legacy doc-write, and the dynamic-code
constructors (string-eval, dynamic-Function, string-first-arg
timer scheduling). ``insertAdjacent`` + HTML is deliberately
*not* covered yet; two existing app.js sites (the verdict-badge
writers at lines 1287 and 1538) consume the HTML-string output of
``renderVerdictBadge``, so broadening the lint requires cleaning
that helper first.
*not* covered yet; two existing ``ui/static/app.js`` sites (the
verdict-badge writers at lines 1287 and 1538) consume the
HTML-string output of ``renderVerdictBadge``, so broadening the
lint requires cleaning that helper first.
Parametrized so each target is its own pytest case — a failure on
one file is attributed precisely without masking offenders in the