mirror of
https://github.com/usestrix/strix.git
synced 2026-08-16 09:26:39 +02:00
refactor: move strix/viewer under strix/interface
This commit is contained in:
+3
-3
@@ -1,8 +1,8 @@
|
||||
# Node / local-viewer SPA source (the built bundle in
|
||||
# strix/viewer/static/ is committed and shipped; do not ignore it)
|
||||
# strix/interface/viewer/static/ is committed and shipped; do not ignore it)
|
||||
node_modules/
|
||||
strix/viewer/frontend/node_modules/
|
||||
strix/viewer/frontend/.vite/
|
||||
strix/interface/viewer/frontend/node_modules/
|
||||
strix/interface/viewer/frontend/.vite/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
|
||||
+5
-5
@@ -102,16 +102,16 @@ We welcome feature ideas! Please:
|
||||
## 🖥️ Local viewer SPA
|
||||
|
||||
`strix view` serves a prebuilt web UI whose source lives in
|
||||
`strix/viewer/frontend/` (a Vite + React project) and whose built output is
|
||||
committed to `strix/viewer/static/` and shipped in the package. End users never
|
||||
run a JS build. If you change anything under `strix/viewer/frontend/`, rebuild
|
||||
`strix/interface/viewer/frontend/` (a Vite + React project) and whose built output is
|
||||
committed to `strix/interface/viewer/static/` and shipped in the package. End users never
|
||||
run a JS build. If you change anything under `strix/interface/viewer/frontend/`, rebuild
|
||||
and commit the output:
|
||||
|
||||
```bash
|
||||
make viewer # or: cd strix/viewer/frontend && npm ci && npm run build
|
||||
make viewer # or: cd strix/interface/viewer/frontend && npm ci && npm run build
|
||||
```
|
||||
|
||||
Commit both the source change and the regenerated `strix/viewer/static/`.
|
||||
Commit both the source change and the regenerated `strix/interface/viewer/static/`.
|
||||
|
||||
## 🤝 Community
|
||||
|
||||
|
||||
@@ -69,8 +69,8 @@ clean:
|
||||
|
||||
viewer:
|
||||
@echo "🖥️ Building the local-viewer SPA..."
|
||||
cd strix/viewer/frontend && npm ci && npm run build
|
||||
@echo "✅ Viewer built to strix/viewer/static/ (commit the changes)."
|
||||
cd strix/interface/viewer/frontend && npm ci && npm run build
|
||||
@echo "✅ Viewer built to strix/interface/viewer/static/ (commit the changes)."
|
||||
|
||||
dev: format lint type-check
|
||||
@echo "✅ Development cycle complete!"
|
||||
|
||||
+6
-6
@@ -79,10 +79,10 @@ build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["strix"]
|
||||
# The prebuilt viewer bundle under strix/viewer/static/ ships automatically
|
||||
# The prebuilt viewer bundle under strix/interface/viewer/static/ ships automatically
|
||||
# (hatchling includes non-.py files under the package). The Vite SOURCE lives
|
||||
# under the package dir too (strix/viewer/frontend/) but must never ship in the wheel.
|
||||
exclude = ["strix/viewer/frontend", "strix/viewer/frontend/**"]
|
||||
# under the package dir too (strix/interface/viewer/frontend/) but must never ship in the wheel.
|
||||
exclude = ["strix/interface/viewer/frontend", "strix/interface/viewer/frontend/**"]
|
||||
|
||||
# ============================================================================
|
||||
# Type Checking Configuration
|
||||
@@ -222,10 +222,10 @@ ignore = [
|
||||
"tests/test_codex_streaming.py" = ["N802"]
|
||||
"tests/test_report_pdf.py" = ["S105", "S106"]
|
||||
# Stdlib HTTP handler overrides (do_GET/do_POST) and lazy imports that avoid a
|
||||
# circular dependency with strix.telemetry / strix.viewer.report_pdf.
|
||||
"strix/viewer/server.py" = ["N802", "PLC0415"]
|
||||
# circular dependency with strix.telemetry / strix.interface.viewer.report_pdf.
|
||||
"strix/interface/viewer/server.py" = ["N802", "PLC0415"]
|
||||
# Lazy telemetry import to avoid importing PostHog before the viewer starts.
|
||||
"strix/viewer/cli.py" = ["PLC0415"]
|
||||
"strix/interface/viewer/cli.py" = ["PLC0415"]
|
||||
# Lazy imports inside functions to avoid circular dependency with
|
||||
# strix.telemetry / strix.report.dedupe / cvss.
|
||||
"strix/tools/notes/tools.py" = ["PLC0415", "TC002"]
|
||||
|
||||
+7
-7
@@ -26,7 +26,7 @@ for tcss_file in strix_root.rglob('*.tcss'):
|
||||
datas.append((str(tcss_file), str(rel_path.parent)))
|
||||
|
||||
# Prebuilt local-viewer SPA (served by `strix view`).
|
||||
viewer_static = strix_root / 'viewer' / 'static'
|
||||
viewer_static = strix_root / 'interface' / 'viewer' / 'static'
|
||||
for asset in viewer_static.rglob('*'):
|
||||
if asset.is_file():
|
||||
rel_path = asset.relative_to(project_root)
|
||||
@@ -158,12 +158,12 @@ hiddenimports = [
|
||||
'strix.report.dedupe',
|
||||
'strix.report.state',
|
||||
'strix.report.writer',
|
||||
'strix.viewer',
|
||||
'strix.viewer.auth',
|
||||
'strix.viewer.cli',
|
||||
'strix.viewer.report_pdf',
|
||||
'strix.viewer.server',
|
||||
'strix.viewer.transcript',
|
||||
'strix.interface.viewer',
|
||||
'strix.interface.viewer.auth',
|
||||
'strix.interface.viewer.cli',
|
||||
'strix.interface.viewer.report_pdf',
|
||||
'strix.interface.viewer.server',
|
||||
'strix.interface.viewer.transcript',
|
||||
|
||||
# PDF report generation + encryption
|
||||
'reportlab',
|
||||
|
||||
@@ -952,7 +952,7 @@ def main() -> None:
|
||||
# `strix view [<run>]` is a viewer-only subcommand, dispatched before the
|
||||
# scan argument parser (which requires a target) and before any scan setup.
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "view":
|
||||
from strix.viewer.cli import run_view
|
||||
from strix.interface.viewer.cli import run_view
|
||||
|
||||
run_view(sys.argv[2:])
|
||||
return
|
||||
|
||||
@@ -1862,7 +1862,7 @@ class StrixTUIApp(App): # type: ignore[misc]
|
||||
webbrowser.open(self._viewer_url)
|
||||
return
|
||||
try:
|
||||
from strix.viewer.server import authorized_url, bundle_is_built, serve
|
||||
from strix.interface.viewer.server import authorized_url, bundle_is_built, serve
|
||||
|
||||
if not bundle_is_built():
|
||||
self._set_viewer_cta("[#eab308]Viewer UI not built[/]")
|
||||
|
||||
@@ -6,7 +6,7 @@ directly from the run's on-disk files. No cloud dependency, no file picker.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from strix.viewer.server import serve
|
||||
from strix.interface.viewer.server import serve
|
||||
|
||||
|
||||
__all__ = ["serve"]
|
||||
@@ -4,7 +4,7 @@ The local viewer proxies email verification and encrypted-report delivery to
|
||||
the Strix relay (``STRIX_APP_URL``). The browser never talks to the relay
|
||||
directly, and the report password generated locally is never sent to it.
|
||||
|
||||
State lives in ``~/.strix/viewer-auth.json`` (0600). ``is_verified`` is a local
|
||||
State lives in ``~/.strix/interface/viewer-auth.json`` (0600). ``is_verified`` is a local
|
||||
flag that unlocks browsing the run history list; the relay still enforces token
|
||||
expiry when a report is actually sent.
|
||||
"""
|
||||
@@ -155,7 +155,7 @@ def _post_json(path: str, payload: dict[str, Any], *, timeout: int) -> tuple[int
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=timeout) as response: # noqa: S310
|
||||
with urllib.request.urlopen(request, timeout=timeout) as response: # noqa: S310 # nosec B310
|
||||
return response.status, _parse_body(response.read())
|
||||
except urllib.error.HTTPError as exc:
|
||||
return exc.code, _parse_body(exc.read())
|
||||
@@ -16,8 +16,8 @@ from strix.core.paths import (
|
||||
run_record_path,
|
||||
runs_base_dir,
|
||||
)
|
||||
from strix.viewer.server import authorized_url, bundle_is_built, serve
|
||||
from strix.viewer.transcript import read_run_summary
|
||||
from strix.interface.viewer.server import authorized_url, bundle_is_built, serve
|
||||
from strix.interface.viewer.transcript import read_run_summary
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -58,7 +58,7 @@ def run_view(argv: list[str]) -> None:
|
||||
if not bundle_is_built():
|
||||
console.print(
|
||||
"[bold red]Viewer UI is not built.[/]\n"
|
||||
"Build it with: [cyan]cd strix/viewer/frontend && npm ci && npm run build[/]"
|
||||
"Build it with: [cyan]cd strix/interface/viewer/frontend && npm ci && npm run build[/]"
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
Generated
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
+1
-1
@@ -5,7 +5,7 @@ import { fileURLToPath, URL } from "node:url";
|
||||
|
||||
// The viewer is served as static files by a stdlib Python server on an
|
||||
// arbitrary ephemeral port, so all asset URLs must be relative (base: "./").
|
||||
// The build output is committed at strix/viewer/static and shipped.
|
||||
// The build output is committed at strix/interface/viewer/static and shipped.
|
||||
export default defineConfig({
|
||||
base: "./",
|
||||
plugins: [react(), tailwindcss()],
|
||||
@@ -38,7 +38,7 @@ from reportlab.platypus import (
|
||||
TableStyle,
|
||||
)
|
||||
|
||||
from strix.viewer.transcript import (
|
||||
from strix.interface.viewer.transcript import (
|
||||
primary_target,
|
||||
read_run_summary,
|
||||
read_vulnerabilities,
|
||||
@@ -27,8 +27,8 @@ from typing import TYPE_CHECKING, Any
|
||||
from urllib.parse import parse_qs, unquote, urlencode, urlsplit
|
||||
|
||||
from strix.core.paths import run_record_path
|
||||
from strix.viewer import auth
|
||||
from strix.viewer.transcript import (
|
||||
from strix.interface.viewer import auth
|
||||
from strix.interface.viewer.transcript import (
|
||||
build_run_state,
|
||||
primary_target,
|
||||
read_report_markdown,
|
||||
@@ -367,7 +367,7 @@ def _make_handler(state: _ViewerState) -> type[BaseHTTPRequestHandler]:
|
||||
self._send_json(HTTPStatus.CONFLICT, {"error": "run_not_finished"})
|
||||
return
|
||||
|
||||
from strix.viewer.report_pdf import build_encrypted_report
|
||||
from strix.interface.viewer.report_pdf import build_encrypted_report
|
||||
|
||||
pdf_bytes, password, filename = build_encrypted_report(run_dir)
|
||||
run_name = str(summary.get("run_name") or run_dir.name)
|
||||
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
@@ -44,8 +44,8 @@ def build_run_state(run_dir: Path) -> dict[str, Any]:
|
||||
Reuses the Textual-free ``TuiLiveView`` projection so the viewer and the TUI
|
||||
share one parser for ``agents.json`` + ``agents.db`` and never drift.
|
||||
"""
|
||||
# Imported lazily so importing strix.viewer does not eagerly pull the TUI.
|
||||
from strix.interface.tui.live_view import TuiLiveView # noqa: PLC0415
|
||||
# Imported lazily so importing strix.interface.viewer does not eagerly pull the TUI.
|
||||
from strix.interface.tui.live_view import TuiLiveView
|
||||
|
||||
view = TuiLiveView()
|
||||
view.hydrate_from_run_dir(run_dir)
|
||||
@@ -4,13 +4,13 @@ from __future__ import annotations
|
||||
|
||||
from pygments.lexers import BashLexer, PythonLexer
|
||||
|
||||
from strix.interface.viewer.report_pdf import _strip_code_fence
|
||||
from strix.report.writer import (
|
||||
guess_language_name,
|
||||
parse_fenced_code,
|
||||
resolve_lexer,
|
||||
safe_fence,
|
||||
)
|
||||
from strix.viewer.report_pdf import _strip_code_fence
|
||||
|
||||
|
||||
def test_parse_fenced_code_extracts_language_and_body() -> None:
|
||||
|
||||
@@ -10,7 +10,7 @@ import pytest
|
||||
from pypdf import PdfReader
|
||||
from pypdf.errors import WrongPasswordError
|
||||
|
||||
from strix.viewer.report_pdf import (
|
||||
from strix.interface.viewer.report_pdf import (
|
||||
build_encrypted_report,
|
||||
encrypt_pdf,
|
||||
generate_password,
|
||||
@@ -91,7 +91,7 @@ def test_wrong_password_is_rejected(tmp_path: Path) -> None:
|
||||
run_dir = _make_run(tmp_path)
|
||||
encrypted = encrypt_pdf(generate_report_pdf(run_dir), "correct-horse-battery")
|
||||
with pytest.raises(WrongPasswordError):
|
||||
PdfReader(BytesIO(encrypted), password="not-the-password")
|
||||
PdfReader(BytesIO(encrypted), password="not-the-password") # nosec B106
|
||||
|
||||
|
||||
def test_build_encrypted_report(tmp_path: Path) -> None:
|
||||
|
||||
+36
-30
@@ -1,4 +1,4 @@
|
||||
"""Tests for the local run viewer (strix.viewer) and its path helpers."""
|
||||
"""Tests for the local run viewer (strix.interface.viewer) and its path helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -9,8 +9,8 @@ import urllib.request
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from strix.core.paths import latest_run_dir, runs_base_dir
|
||||
from strix.viewer.server import serve
|
||||
from strix.viewer.transcript import (
|
||||
from strix.interface.viewer.server import serve
|
||||
from strix.interface.viewer.transcript import (
|
||||
build_run_state,
|
||||
read_report_markdown,
|
||||
read_run_summary,
|
||||
@@ -89,7 +89,7 @@ def test_build_run_state_from_agents_json(tmp_path: Path) -> None:
|
||||
def _get(url: str, *, cookie: str | None = None) -> tuple[int, str, bytes]:
|
||||
headers = {"Cookie": cookie} if cookie else {}
|
||||
req = urllib.request.Request(url, headers=headers) # noqa: S310 - localhost test server
|
||||
with urllib.request.urlopen(req) as resp: # noqa: S310 - localhost test server
|
||||
with urllib.request.urlopen(req) as resp: # noqa: S310 - localhost test server # nosec B310
|
||||
return resp.status, resp.headers.get("Content-Type", ""), resp.read()
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ def test_server_serves_api_and_static(tmp_path: Path, monkeypatch: pytest.Monkey
|
||||
(assets / "assets").mkdir(parents=True)
|
||||
(assets / "index.html").write_text("<!doctype html><div id=root></div>", encoding="utf-8")
|
||||
(assets / "assets" / "app.js").write_text("console.log(1)", encoding="utf-8")
|
||||
monkeypatch.setattr("strix.viewer.server.bundle_dir", lambda: assets)
|
||||
monkeypatch.setattr("strix.interface.viewer.server.bundle_dir", lambda: assets)
|
||||
|
||||
httpd, url, _ = serve(run_dir, open_browser=False)
|
||||
try:
|
||||
@@ -132,7 +132,7 @@ def test_server_event_endpoint_forwards_cta(
|
||||
assets = tmp_path / "bundle"
|
||||
assets.mkdir()
|
||||
(assets / "index.html").write_text("x", encoding="utf-8")
|
||||
monkeypatch.setattr("strix.viewer.server.bundle_dir", lambda: assets)
|
||||
monkeypatch.setattr("strix.interface.viewer.server.bundle_dir", lambda: assets)
|
||||
|
||||
seen: list[tuple[str, str | None]] = []
|
||||
monkeypatch.setattr(
|
||||
@@ -148,7 +148,7 @@ def test_server_event_endpoint_forwards_cta(
|
||||
req = urllib.request.Request( # noqa: S310 - localhost test server
|
||||
f"{url}/api/event", data=body, headers={"Content-Type": "application/json"}
|
||||
)
|
||||
with urllib.request.urlopen(req) as resp: # noqa: S310
|
||||
with urllib.request.urlopen(req) as resp: # noqa: S310 # nosec B310
|
||||
assert resp.status == 204
|
||||
assert seen == [("PR reviews", "sidebar_nav")]
|
||||
finally:
|
||||
@@ -163,7 +163,7 @@ def test_server_event_endpoint_forwards_email_funnel(
|
||||
assets = tmp_path / "bundle"
|
||||
assets.mkdir()
|
||||
(assets / "index.html").write_text("x", encoding="utf-8")
|
||||
monkeypatch.setattr("strix.viewer.server.bundle_dir", lambda: assets)
|
||||
monkeypatch.setattr("strix.interface.viewer.server.bundle_dir", lambda: assets)
|
||||
|
||||
seen: list[tuple[str, str | None]] = []
|
||||
monkeypatch.setattr(
|
||||
@@ -183,7 +183,7 @@ def test_server_event_endpoint_forwards_email_funnel(
|
||||
data=json.dumps(payload).encode(),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
with urllib.request.urlopen(req) as resp: # noqa: S310
|
||||
with urllib.request.urlopen(req) as resp: # noqa: S310 # nosec B310
|
||||
assert resp.status == 204
|
||||
assert seen == expected
|
||||
finally:
|
||||
@@ -207,7 +207,7 @@ def test_server_event_endpoint_forwards_agent_steered(
|
||||
data=json.dumps({"event": "agent_steered"}).encode(),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
with urllib.request.urlopen(req) as resp: # noqa: S310
|
||||
with urllib.request.urlopen(req) as resp: # noqa: S310 # nosec B310
|
||||
assert resp.status == 204
|
||||
assert seen == [True]
|
||||
finally:
|
||||
@@ -222,7 +222,7 @@ def test_feedback_records_telemetry_on_success(
|
||||
_bundle(tmp_path, monkeypatch)
|
||||
|
||||
sent: list[bool] = []
|
||||
monkeypatch.setattr("strix.viewer.auth.feedback_submit", lambda *_a: None)
|
||||
monkeypatch.setattr("strix.interface.viewer.auth.feedback_submit", lambda *_a: None)
|
||||
monkeypatch.setattr(
|
||||
"strix.telemetry.posthog.viewer_feedback_submitted", lambda: sent.append(True)
|
||||
)
|
||||
@@ -257,7 +257,7 @@ def _post(
|
||||
url + path, data=json.dumps(payload).encode(), headers=headers, method="POST"
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp: # noqa: S310
|
||||
with urllib.request.urlopen(req) as resp: # noqa: S310 # nosec B310
|
||||
return resp.status, resp.read()
|
||||
except urllib.error.HTTPError as exc:
|
||||
return exc.code, exc.read()
|
||||
@@ -266,7 +266,7 @@ def _post(
|
||||
def _session_cookie(url: str, token: str) -> str:
|
||||
"""Bootstrap a session via the tokened URL and return its ``name=value`` cookie."""
|
||||
bootstrap = f"{url}/?token={token}"
|
||||
with urllib.request.urlopen(bootstrap) as resp: # noqa: S310 - localhost test server
|
||||
with urllib.request.urlopen(bootstrap) as resp: # noqa: S310 - localhost test server # nosec B310
|
||||
raw = str(resp.headers.get("Set-Cookie", ""))
|
||||
return raw.split(";", 1)[0]
|
||||
|
||||
@@ -275,7 +275,7 @@ def _get_status(url: str, *, cookie: str | None = None) -> int:
|
||||
headers = {"Cookie": cookie} if cookie else {}
|
||||
req = urllib.request.Request(url, headers=headers) # noqa: S310 - localhost test server
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp: # noqa: S310
|
||||
with urllib.request.urlopen(req) as resp: # noqa: S310 # nosec B310
|
||||
return int(resp.status)
|
||||
except urllib.error.HTTPError as exc:
|
||||
return int(exc.code)
|
||||
@@ -285,7 +285,7 @@ def _bundle(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
assets = tmp_path / "bundle"
|
||||
assets.mkdir()
|
||||
(assets / "index.html").write_text("<!doctype html><div id=root></div>", encoding="utf-8")
|
||||
monkeypatch.setattr("strix.viewer.server.bundle_dir", lambda: assets)
|
||||
monkeypatch.setattr("strix.interface.viewer.server.bundle_dir", lambda: assets)
|
||||
|
||||
|
||||
def test_capability_issued_only_for_tokened_bootstrap(
|
||||
@@ -296,26 +296,26 @@ def test_capability_issued_only_for_tokened_bootstrap(
|
||||
(assets / "assets").mkdir(parents=True)
|
||||
(assets / "index.html").write_text("<!doctype html>index", encoding="utf-8")
|
||||
(assets / "assets" / "app.js").write_text("1", encoding="utf-8")
|
||||
monkeypatch.setattr("strix.viewer.server.bundle_dir", lambda: assets)
|
||||
monkeypatch.setattr("strix.interface.viewer.server.bundle_dir", lambda: assets)
|
||||
|
||||
httpd, url, token = serve(run_dir, open_browser=False)
|
||||
try:
|
||||
# A bare index load -- all a reachable client can do -- hands out nothing.
|
||||
with urllib.request.urlopen(url + "/") as resp: # noqa: S310
|
||||
with urllib.request.urlopen(url + "/") as resp: # noqa: S310 # nosec B310
|
||||
assert resp.headers.get("Set-Cookie") is None
|
||||
|
||||
# A wrong token is likewise refused the capability.
|
||||
with urllib.request.urlopen(f"{url}/?token=wrong") as resp: # noqa: S310
|
||||
with urllib.request.urlopen(f"{url}/?token=wrong") as resp: # noqa: S310 # nosec B310
|
||||
assert resp.headers.get("Set-Cookie") is None
|
||||
|
||||
# Only the correct bootstrap token mints the session cookie.
|
||||
with urllib.request.urlopen(f"{url}/?token={token}") as resp: # noqa: S310
|
||||
with urllib.request.urlopen(f"{url}/?token={token}") as resp: # noqa: S310 # nosec B310
|
||||
cookie = str(resp.headers.get("Set-Cookie", ""))
|
||||
assert "strix_viewer_session=" in cookie
|
||||
assert "HttpOnly" in cookie and "SameSite=Strict" in cookie
|
||||
|
||||
# Static assets never carry it.
|
||||
with urllib.request.urlopen(url + "/assets/app.js") as resp: # noqa: S310
|
||||
with urllib.request.urlopen(url + "/assets/app.js") as resp: # noqa: S310 # nosec B310
|
||||
assert resp.headers.get("Set-Cookie") is None
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
@@ -338,7 +338,7 @@ def test_unauthorized_client_cannot_acquire_capability(
|
||||
try:
|
||||
# A direct network client can reach the page but is handed no capability,
|
||||
# so replaying an empty/guessed cookie cannot steer a live scan.
|
||||
with urllib.request.urlopen(url + "/") as resp: # noqa: S310
|
||||
with urllib.request.urlopen(url + "/") as resp: # noqa: S310 # nosec B310
|
||||
assert resp.headers.get("Set-Cookie") is None
|
||||
status, _ = _post(
|
||||
url,
|
||||
@@ -356,9 +356,11 @@ def test_unauthorized_client_cannot_acquire_capability(
|
||||
def test_auth_status_reflects_expiry(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
run_dir = _make_run(tmp_path, "status", status="running", end_time=None)
|
||||
_bundle(tmp_path, monkeypatch)
|
||||
monkeypatch.setattr("strix.viewer.auth.read_auth", lambda: {"email": "a@b.com", "token": "t"})
|
||||
monkeypatch.setattr(
|
||||
"strix.interface.viewer.auth.read_auth", lambda: {"email": "a@b.com", "token": "t"}
|
||||
)
|
||||
verified = {"value": True}
|
||||
monkeypatch.setattr("strix.viewer.auth.is_verified", lambda: verified["value"])
|
||||
monkeypatch.setattr("strix.interface.viewer.auth.is_verified", lambda: verified["value"])
|
||||
|
||||
httpd, url, token = serve(run_dir, open_browser=False)
|
||||
try:
|
||||
@@ -384,7 +386,7 @@ def test_auth_mutations_require_session(tmp_path: Path, monkeypatch: pytest.Monk
|
||||
run_dir = _make_run(tmp_path, "authmut", status="running", end_time=None)
|
||||
_bundle(tmp_path, monkeypatch)
|
||||
forgotten = {"value": False}
|
||||
monkeypatch.setattr("strix.viewer.auth.forget", lambda: forgotten.update(value=True))
|
||||
monkeypatch.setattr("strix.interface.viewer.auth.forget", lambda: forgotten.update(value=True))
|
||||
|
||||
httpd, url, _ = serve(run_dir, open_browser=False)
|
||||
try:
|
||||
@@ -432,7 +434,9 @@ def test_report_send_requires_session_cookie(
|
||||
_bundle(tmp_path, monkeypatch)
|
||||
|
||||
# A verified machine token exists, but that alone must not authorize a caller.
|
||||
monkeypatch.setattr("strix.viewer.auth.read_auth", lambda: {"email": "a@b.com", "token": "t"})
|
||||
monkeypatch.setattr(
|
||||
"strix.interface.viewer.auth.read_auth", lambda: {"email": "a@b.com", "token": "t"}
|
||||
)
|
||||
|
||||
httpd, url, token = serve(run_dir, open_browser=False)
|
||||
try:
|
||||
@@ -456,7 +460,9 @@ def test_report_send_rejects_live_run(tmp_path: Path, monkeypatch: pytest.Monkey
|
||||
# fail closed even for a verified, session-holding caller.
|
||||
run_dir = _make_run(tmp_path, "live", status="running", end_time=None)
|
||||
_bundle(tmp_path, monkeypatch)
|
||||
monkeypatch.setattr("strix.viewer.auth.read_auth", lambda: {"email": "a@b.com", "token": "t"})
|
||||
monkeypatch.setattr(
|
||||
"strix.interface.viewer.auth.read_auth", lambda: {"email": "a@b.com", "token": "t"}
|
||||
)
|
||||
|
||||
httpd, url, token = serve(run_dir, open_browser=False)
|
||||
try:
|
||||
@@ -475,7 +481,7 @@ def test_historical_run_data_requires_verification(
|
||||
_bundle(tmp_path, monkeypatch)
|
||||
|
||||
verified = {"value": False}
|
||||
monkeypatch.setattr("strix.viewer.auth.is_verified", lambda: verified["value"])
|
||||
monkeypatch.setattr("strix.interface.viewer.auth.is_verified", lambda: verified["value"])
|
||||
|
||||
httpd, url, token = serve(launched, open_browser=False)
|
||||
try:
|
||||
@@ -509,12 +515,12 @@ def test_runs_list_requires_session_and_verification(
|
||||
_make_run(tmp_path, "other", status="completed", end_time="2026-01-01T00:00:00Z")
|
||||
_bundle(tmp_path, monkeypatch)
|
||||
|
||||
monkeypatch.setattr("strix.viewer.auth.is_verified", lambda: True)
|
||||
monkeypatch.setattr("strix.interface.viewer.auth.is_verified", lambda: True)
|
||||
|
||||
def _runs(cookie: str | None) -> dict[str, object]:
|
||||
headers = {"Cookie": cookie} if cookie else {}
|
||||
req = urllib.request.Request(f"{url}/api/runs", headers=headers) # noqa: S310
|
||||
with urllib.request.urlopen(req) as resp: # noqa: S310 - localhost test server
|
||||
with urllib.request.urlopen(req) as resp: # noqa: S310 - localhost test server # nosec B310
|
||||
return dict(json.loads(resp.read()))
|
||||
|
||||
httpd, url, token = serve(launched, open_browser=False)
|
||||
@@ -543,7 +549,7 @@ def test_server_rejects_path_traversal(tmp_path: Path, monkeypatch: pytest.Monke
|
||||
assets = tmp_path / "bundle"
|
||||
assets.mkdir()
|
||||
(assets / "index.html").write_text("<!doctype html>index", encoding="utf-8")
|
||||
monkeypatch.setattr("strix.viewer.server.bundle_dir", lambda: assets)
|
||||
monkeypatch.setattr("strix.interface.viewer.server.bundle_dir", lambda: assets)
|
||||
|
||||
httpd, url, _ = serve(run_dir, open_browser=False)
|
||||
try:
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
import pytest
|
||||
|
||||
from strix.viewer import auth
|
||||
from strix.interface.viewer import auth
|
||||
|
||||
|
||||
def _iso(delta: timedelta) -> str:
|
||||
@@ -30,7 +30,7 @@ def test_write_read_forget_roundtrip() -> None:
|
||||
assert auth.read_auth() is None
|
||||
assert auth.is_verified() is False
|
||||
|
||||
auth.write_auth(email="user@example.com", token="tok-123", verified_at=_iso(timedelta(days=30)))
|
||||
auth.write_auth(email="user@example.com", token="tok-123", verified_at=_iso(timedelta(days=30))) # nosec B106
|
||||
|
||||
record = auth.read_auth()
|
||||
assert record is not None
|
||||
@@ -47,23 +47,23 @@ def test_write_read_forget_roundtrip() -> None:
|
||||
|
||||
def test_is_verified_enforces_expiry() -> None:
|
||||
# An expired record still reads back, but no longer unlocks history.
|
||||
auth.write_auth(email="a@b.com", token="t", verified_at=_iso(timedelta(hours=-1)))
|
||||
auth.write_auth(email="a@b.com", token="t", verified_at=_iso(timedelta(hours=-1))) # nosec B106
|
||||
assert auth.read_auth() is not None
|
||||
assert auth.is_verified() is False
|
||||
|
||||
# A future expiry unlocks it.
|
||||
auth.write_auth(email="a@b.com", token="t", verified_at=_iso(timedelta(hours=1)))
|
||||
auth.write_auth(email="a@b.com", token="t", verified_at=_iso(timedelta(hours=1))) # nosec B106
|
||||
assert auth.is_verified() is True
|
||||
|
||||
|
||||
def test_is_verified_fails_closed_when_expiry_absent_or_unparseable() -> None:
|
||||
# No/blank expiry: fail closed rather than unlocking history forever.
|
||||
auth.write_auth(email="a@b.com", token="t", verified_at="")
|
||||
auth.write_auth(email="a@b.com", token="t", verified_at="") # nosec B106
|
||||
assert auth.read_auth() is not None
|
||||
assert auth.is_verified() is False
|
||||
|
||||
# Garbage expiry likewise requires re-verification.
|
||||
auth.write_auth(email="a@b.com", token="t", verified_at="not-a-date")
|
||||
auth.write_auth(email="a@b.com", token="t", verified_at="not-a-date") # nosec B106
|
||||
assert auth.is_verified() is False
|
||||
|
||||
|
||||
@@ -73,9 +73,9 @@ def test_is_verified_accepts_epoch_expiry() -> None:
|
||||
past = (datetime.now(UTC) - timedelta(hours=1)).timestamp()
|
||||
|
||||
# As a numeric string (how write_auth persists it).
|
||||
auth.write_auth(email="a@b.com", token="t", verified_at=str(future))
|
||||
auth.write_auth(email="a@b.com", token="t", verified_at=str(future)) # nosec B106
|
||||
assert auth.is_verified() is True
|
||||
auth.write_auth(email="a@b.com", token="t", verified_at=str(past))
|
||||
auth.write_auth(email="a@b.com", token="t", verified_at=str(past)) # nosec B106
|
||||
assert auth.is_verified() is False
|
||||
|
||||
# As a raw JSON number, if a record is written that way.
|
||||
@@ -87,7 +87,7 @@ def test_is_verified_accepts_epoch_expiry() -> None:
|
||||
|
||||
|
||||
def test_write_auth_is_0600() -> None:
|
||||
auth.write_auth(email="a@b.com", token="t", verified_at="")
|
||||
auth.write_auth(email="a@b.com", token="t", verified_at="") # nosec B106
|
||||
mode = stat.S_IMODE(auth.AUTH_PATH.stat().st_mode)
|
||||
assert mode == 0o600
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from strix.viewer.server import build_runs_payload, resolve_run_dir
|
||||
from strix.interface.viewer.server import build_runs_payload, resolve_run_dir
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
Reference in New Issue
Block a user