Skip to content

feat(webui): add a local web UI for the scan pipeline - #235

Merged
gadievron merged 1 commit into
masterfrom
feat/webui
Aug 14, 2026
Merged

feat(webui): add a local web UI for the scan pipeline#235
gadievron merged 1 commit into
masterfrom
feat/webui

Conversation

@gadievron

Copy link
Copy Markdown
Collaborator

feat(webui): add a local web UI for the scan pipeline

Adds an opt-in, loopback-only web UI (openant serve) that drives the existing
scan pipeline from a browser: submit a repo URL or local path, watch scan logs
stream over SSE, and read the HTML report / markdown summary / disclosures.

Original work by Sounil Yu (PR #25, branch knostic/webui-2); this reconciles it
onto current master (taking master on the 5 superseded Python-side conflicts,
keeping only the additive Go/UI surface + a one-line serveCmd registration) and
hardens it. Supersedes #25.

Security posture — the server binds a loopback address (127.0.0.1 by default), EVERY route rejects a
non-loopback Host (DNS-rebinding guard in the securityHeaders middleware, GET
routes included), and every state-changing request additionally requires a
per-instance crypto/rand CSRF token (constant-time compare) plus a same-origin
check. So the read and write surfaces are both reachable only by a caller
already on the local origin — this is defense-in-depth hardening, not a
remotely-exploitable vulnerability. One residual caveat, tracked below: the live
HTML report loads Tailwind/Chart.js from CDNs, so a CDN compromise could run
script in the server origin.

  • git-clone SSRF guard (repoHostBlocked): classifies canonical IPs by value
    (loopback/link-local/unspecified/AWS IPv4+IPv6 IMDS/Alibaba blocked; RFC1918 +
    IPv6-ULA allowed for internal servers), resolves DNS names and blocks any
    sensitive result, and blocks ANY non-canonical numeric literal outright
    (encoding-proof for literals: decimal/octal/hex/short-form/mod-2^32-wrap, IPv6
    zone-ids, IPv4-mapped forms). Fails closed on non-ASCII/IDNA hosts, backslash/
    unparseable URLs, and scp multi-userinfo; blocks the .localhost TLD; passes
    git -c http.followRedirects=false. DNS-rebinding of a repo hostname between the
    check and the clone is out of scope (the guard covers literals + one resolution).
  • Rejects credentials embedded in an http(s) repo URL (they would be logged
    verbatim); the python subprocess runs with -P so a hostile openant/ package in
    the scanned repo can't shadow the real module.
  • DOMPurify 3.4.13 (vendored, no CDN; updated from 3.1.6 = CVE-2025-15599 /
    CVE-2026-41238) with a strict INERT allowlist (no style/form/img/svg — a repo
    can break out of a markdown code fence, so the default allowlist's active
    content is refused) on client-rendered markdown + a strict bluemonday allowlist on
    the report SafeRemediation path; the API-key value is never sent to any page.
  • Robustness: managed-writer stderr with WaitDelay (no deadlock on a detached
    child; a WaitDelay-tripped exit-0 scan keeps its output rather than erroring),
    signal-exit treated as error, LogBuf bounded by line count AND bytes, delete
    that waits (up to 5s) for the runner before removing the dir, and a synchronized
    shutdown (drainMu + WaitShutdown) that cancels in-flight scans before exit.
  • The job output dir holds files derived from the untrusted repo, so served
    files (report.html, summary, disclosures) are opened O_NOFOLLOW and confirmed to
    be regular files within the job dir (a symlink to a host secret is refused
    atomically at open — no check-then-read race), and the clone
    is time-bounded (15m) so a hostile remote can't hold a scan slot forever.

Tested: host-classification suite (41 blocked forms + a legit allow-set),
auth-gate rejections (rebinding Host, cross-site, foreign Origin, credential URL),
remediation-XSS, job-lifecycle (drain/cancel/recover/delete-waits-for-runner),
the SSE Last-Event-ID overflow clamp, and the stderr WaitDelay mechanism. go
build / go vet / go test ./... green; go test -race clean; semgrep + CodeQL
(CWE-918 SSRF = 0); real end-to-end scans of Python/Go/JS repos through the UI.

Deferred (tracked, not in this PR): vendoring the report-template CDN scripts
(Tailwind is a CDN JIT; needs an asset build step, and the template is shared
with openant report -f html) and the CSP/COEP that depends on it; the marked
12.0.2 update (no known CVE; output is DOMPurify-sanitized); the unbounded stdout
capture buffer (openant's stdout is its own bounded JSON envelope).

Reproduce:
git fetch origin && git checkout
cd apps/openant-cli && go build -o /tmp/openant-cli . && /tmp/openant-cli serve --addr 127.0.0.1:8765
go test ./...

Coordination

Known limitations (documented, not addressed here)

  • Report templates load Tailwind/Chart.js from CDNs (shared with openant report -f html, rendered same-origin at /report/{id}); vendoring needs an asset build step. Full CSP/COEP depend on it.
  • --dynamic-test exposes the existing core dynamic-tester (unchanged by this PR).
  • Disk quota / job GC and multi-user-host isolation are out of scope for a local single-user tool.

Reviewed via an independent multi-lens security pass (SSRF, CSRF, DNS-rebinding, XSS, markdown-injection, filesystem/symlink, DoS) with all in-scope findings fixed.

Adds an opt-in, loopback-only web UI (`openant serve`) that drives the existing
scan pipeline from a browser: submit a repo URL or local path, watch scan logs
stream over SSE, and read the HTML report / markdown summary / disclosures.

Original work by Sounil Yu (PR #25, branch knostic/webui-2); this reconciles it
onto current master (taking master on the 5 superseded Python-side conflicts,
keeping only the additive Go/UI surface + a one-line serveCmd registration) and
hardens it. Supersedes #25.

Security posture — the server binds a loopback address (127.0.0.1 by default), EVERY route rejects a
non-loopback Host (DNS-rebinding guard in the securityHeaders middleware, GET
routes included), and every state-changing request additionally requires a
per-instance crypto/rand CSRF token (constant-time compare) plus a same-origin
check. So the read and write surfaces are both reachable only by a caller
already on the local origin — this is defense-in-depth hardening, not a
remotely-exploitable vulnerability. One residual caveat, tracked below: the live
HTML report loads Tailwind/Chart.js from CDNs, so a CDN compromise could run
script in the server origin.

- git-clone SSRF guard (repoHostBlocked): classifies canonical IPs by value
  (loopback/link-local/unspecified/AWS IPv4+IPv6 IMDS/Alibaba blocked; RFC1918 +
  IPv6-ULA allowed for internal servers), resolves DNS names and blocks any
  sensitive result, and blocks ANY non-canonical numeric literal outright
  (encoding-proof for literals: decimal/octal/hex/short-form/mod-2^32-wrap, IPv6
  zone-ids, IPv4-mapped forms). Fails closed on non-ASCII/IDNA hosts, backslash/
  unparseable URLs, and scp multi-userinfo; blocks the .localhost TLD; passes
  git -c http.followRedirects=false. DNS-rebinding of a repo hostname between the
  check and the clone is out of scope (the guard covers literals + one resolution).
- Rejects credentials embedded in an http(s) repo URL (they would be logged
  verbatim); the python subprocess runs with -P so a hostile openant/ package in
  the scanned repo can't shadow the real module.
- DOMPurify 3.4.13 (vendored, no CDN; updated from 3.1.6 = CVE-2025-15599 /
  CVE-2026-41238) with a strict INERT allowlist (no style/form/img/svg — a repo
  can break out of a markdown code fence, so the default allowlist's active
  content is refused) on client-rendered markdown + a strict bluemonday allowlist on
  the report SafeRemediation path; the API-key value is never sent to any page.
- Robustness: managed-writer stderr with WaitDelay (no deadlock on a detached
  child; a WaitDelay-tripped exit-0 scan keeps its output rather than erroring),
  signal-exit treated as error, LogBuf bounded by line count AND bytes, delete
  that waits (up to 5s) for the runner before removing the dir, and a synchronized
  shutdown (drainMu + WaitShutdown) that cancels in-flight scans before exit.
- The job output dir holds files derived from the untrusted repo, so served
  files (report.html, summary, disclosures) are opened O_NOFOLLOW and confirmed to
  be regular files within the job dir (a symlink to a host secret is refused
  atomically at open — no check-then-read race), and the clone
  is time-bounded (15m) so a hostile remote can't hold a scan slot forever.

Tested: host-classification suite (41 blocked forms + a legit allow-set),
auth-gate rejections (rebinding Host, cross-site, foreign Origin, credential URL),
remediation-XSS, job-lifecycle (drain/cancel/recover/delete-waits-for-runner),
the SSE Last-Event-ID overflow clamp, and the stderr WaitDelay mechanism. go
build / go vet / go test ./... green; go test -race clean; semgrep + CodeQL
(CWE-918 SSRF = 0); real end-to-end scans of Python/Go/JS repos through the UI.

Deferred (tracked, not in this PR): vendoring the report-template CDN scripts
(Tailwind is a CDN JIT; needs an asset build step, and the template is shared
with `openant report -f html`) and the CSP/COEP that depends on it; the marked
12.0.2 update (no known CVE; output is DOMPurify-sanitized); the unbounded stdout
capture buffer (openant's stdout is its own bounded JSON envelope).

Process-group kill (Setpgid/SIGKILL) and O_NOFOLLOW are Unix-only, so they live
in procgroup_unix.go / procgroup_other.go build-tagged files; the code builds and
vets clean on linux, darwin, and windows.

Reproduce:
  git fetch origin && git checkout <this-branch>
  cd apps/openant-cli && go build -o /tmp/openant-cli . && /tmp/openant-cli serve --addr 127.0.0.1:8765
  go test ./...

Co-authored-by: Sounil Yu <4305467+sounil@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@gadievron
gadievron merged commit b8844d4 into master Aug 14, 2026
14 checks passed
gadievron added a commit that referenced this pull request Aug 14, 2026
The web UI (openant serve) landed in the code (#235) but its doc updates missed
that merge. Add them: a "Web UI" section in the README, a CHANGELOG entry, and
`serve` in the ARCHITECTURE command list. Docs-only; no code change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant