Local viewer: prominent scan switcher + rename to "pentest" terminology (#848)

This commit is contained in:
Ahmed Allam 2026-07-22 09:17:52 -07:00 committed by GitHub
parent ef07bad945
commit 1f7373714b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 150 additions and 67 deletions

View file

@ -180,6 +180,14 @@ def viewer_email_event(step: str, purpose: str | None = None) -> None:
)
def viewer_feedback_submitted() -> None:
_send("viewer_feedback_submitted", {**base_props()})
def viewer_agent_steered() -> None:
_send("viewer_agent_steered", {**base_props()})
def error(error_type: str) -> None:
props = {**base_props(), "error_type": error_type}
_send("error", props)

View file

@ -355,7 +355,7 @@ export default function App() {
{/* Tab strip: shown on small screens where the sidebar is hidden. */}
<div className="flex gap-5 border-b border-[#2a2a2a] lg:hidden">
<TabButton active={view === "overview"} onClick={() => userSetView("overview")}>
Overview
Pentest Overview
</TabButton>
<TabButton active={view === "issues"} onClick={() => userSetView("issues")}>
Issues{run.vulnerabilities.length > 0 ? ` (${run.vulnerabilities.length})` : ""}
@ -425,33 +425,37 @@ function RunSwitcher({
<button
onClick={() => setOpen((o) => !o)}
onBlur={() => setTimeout(() => setOpen(false), 150)}
className="flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs text-[#aaa] transition-colors hover:text-white"
style={{ border: "1px solid #2a2a2a" }}
aria-label="Switch pentest"
className="flex items-center gap-2 rounded-lg border border-[#3a3a3a] bg-[rgba(255,255,255,0.05)] px-3 py-2 text-sm text-white transition-colors hover:border-[#555] hover:bg-[rgba(255,255,255,0.09)]"
>
<History className="w-3.5 h-3.5" aria-hidden="true" />
<span className="max-w-[160px] truncate">{current}</span>
<ChevronDown className="w-3.5 h-3.5" aria-hidden="true" />
<History className="h-4 w-4 flex-shrink-0 text-[#888]" aria-hidden="true" />
<span className="flex-shrink-0 text-[#888]">Pentest</span>
<span className="max-w-[260px] truncate font-medium">{current}</span>
<ChevronDown className="h-4 w-4 flex-shrink-0 text-[#aaa]" aria-hidden="true" />
</button>
{open && (
<div
className="absolute right-0 z-50 mt-1.5 max-h-80 w-64 overflow-y-auto rounded-lg py-1 shadow-xl"
style={{ border: "1px solid #2a2a2a", background: "#0a0a0a" }}
className="absolute right-0 z-50 mt-2 max-h-96 w-96 overflow-y-auto rounded-xl py-1.5 shadow-2xl"
style={{ border: "1px solid #3a3a3a", background: "#0a0a0a" }}
>
<div className="border-b border-[#222] px-3 py-2 text-[11px] font-semibold uppercase tracking-wide text-[#666]">
Switch pentest
</div>
{runs.runs.map((r) => {
const active = r.name === activeRun;
return (
<button
key={r.name}
onMouseDown={() => onSelect(r.name)}
className={`flex w-full items-center gap-2 px-3 py-2 text-left text-xs transition-colors hover:bg-[rgba(255,255,255,0.06)] ${
active ? "text-white" : "text-[#aaa]"
className={`flex w-full items-center gap-2 px-3 py-2.5 text-left text-sm transition-colors hover:bg-[rgba(255,255,255,0.06)] ${
active ? "bg-[rgba(255,255,255,0.04)] text-white" : "text-[#aaa]"
}`}
>
<span className="min-w-0 flex-1">
<span className="block truncate">{runTitle(r.target, r.name)}</span>
{r.target && <span className="block truncate font-mono text-[#666]">{r.target}</span>}
<span className="block truncate font-medium">{runTitle(r.target, r.name)}</span>
{r.target && <span className="block truncate font-mono text-xs text-[#666]">{r.target}</span>}
</span>
{active && <span className="h-1.5 w-1.5 flex-shrink-0 rounded-full bg-emerald-400" />}
{active && <span className="h-2 w-2 flex-shrink-0 rounded-full bg-emerald-400" />}
</button>
);
})}
@ -495,7 +499,7 @@ function SummaryHeader({ summary }: { summary: ParsedRunSummary }) {
return (
<div>
<h1 className="text-2xl font-semibold text-white">
{runTitle(summary.targets[0] ?? null, summary.runName ?? summary.runId ?? "Scan results")}
{runTitle(summary.targets[0] ?? null, summary.runName ?? summary.runId ?? "Pentest results")}
</h1>
<div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-[#888]">
{summary.targets.length > 0 && (
@ -534,7 +538,7 @@ function FindingsList({
return (
<div className="space-y-4">
<div className="rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-8 text-center text-sm text-[#888]">
{finished ? "No findings in this run." : "No findings yet. The scan is still running…"}
{finished ? "No findings in this run." : "No findings yet. The pentest is still running…"}
</div>
{finished && (
<div className="rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5">
@ -760,12 +764,12 @@ function AgentsTab({ run, canSteer }: { run: LoadedRun; canSteer: boolean }) {
{/* Re-run always routes to Strix Cloud. */}
<div className="rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5">
<p className="text-sm font-semibold text-white">Run this scan with more depth</p>
<p className="mt-0.5 text-xs text-[#666]">Re-run this scan on managed infra in the cloud.</p>
<p className="text-sm font-semibold text-white">Run this pentest with more depth</p>
<p className="mt-0.5 text-xs text-[#666]">Re-run this pentest on managed infra in the cloud.</p>
<div className="mt-3 flex flex-wrap gap-2.5">
<ProInlineCta
label="Re-run in Strix Cloud with more depth"
desc="Run this scan on managed infra with more depth."
label="Re-run in Strix Pro with more depth"
desc="Run this pentest on managed infra with more depth."
slug="live_scan"
surface="agents"
icon={Rocket}

View file

@ -239,7 +239,7 @@ export default function EmailReportView({
onClick={startFlow}
className="w-full cursor-pointer rounded-lg bg-white px-4 py-2.5 text-sm font-semibold text-black transition-opacity hover:opacity-90"
>
{verified ? "Email me the encrypted PDF" : "Continue with your email"}
Export report
</button>
{verified && auth?.email && (
<p className="text-center text-xs text-[#666]">Sending to {auth.email}</p>

View file

@ -152,7 +152,7 @@ export function RunDetails({
<span className="text-[#666]">None</span>
)}
</Field>
{scanMode && <Field label="Scan mode">{scanMode}</Field>}
{scanMode && <Field label="Pentest mode">{scanMode}</Field>}
<Field label="Scope">{scope}</Field>
<Field label="Mode">{nonInteractive ? "Non-interactive" : "Interactive"}</Field>
{localSources.length > 0 && (

View file

@ -85,6 +85,13 @@ export default function Sidebar({
const [upgradeFeature, setUpgradeFeature] = useState<string | null>(null);
const userMenuRef = useRef<HTMLDivElement>(null);
// Open the upgrade dialog for a platform feature, recording which feature
// drove the open (the dialog's own CTAs track the deeper conversion).
const openUpgrade = (slug: string, description: string) => {
trackCta(slug, "sidebar");
setUpgradeFeature(description);
};
const persistWidth = useCallback((w: number) => {
setWidth(w);
try {
@ -219,7 +226,7 @@ export default function Sidebar({
<div className="relative flex flex-col gap-px px-2">
<NavItem
icon={<ProjectsIcon />}
label="Overview"
label="Pentest Overview"
active={view === "overview"}
onClick={() => onSelectView("overview")}
/>
@ -268,7 +275,8 @@ export default function Sidebar({
label="PR Security Reviews"
active={false}
onClick={() =>
setUpgradeFeature(
openUpgrade(
"pr_reviews",
"Strix reviews every pull request and flags exploitable changes before they merge."
)
}
@ -278,7 +286,8 @@ export default function Sidebar({
label="Integrations"
active={false}
onClick={() =>
setUpgradeFeature(
openUpgrade(
"integrations",
"Sync findings to Jira, Linear, and Slack so fixes happen where your team already works."
)
}
@ -288,7 +297,8 @@ export default function Sidebar({
label="Members"
active={false}
onClick={() =>
setUpgradeFeature(
openUpgrade(
"members",
"Invite your team, set roles, and share findings and run history across your org."
)
}

View file

@ -248,7 +248,7 @@ export function ScanPromptComposer({
void handleSend();
}
}}
placeholder="Send a live prompt to the running scan…"
placeholder="Send a live prompt to the running pentest…"
maxLength={4000}
disabled={sending}
className="block w-full resize-none border-0 bg-transparent p-0 text-[15px] leading-6 text-white placeholder:text-[#444] focus:outline-none disabled:opacity-60 max-h-[160px] overflow-y-auto"

View file

@ -57,5 +57,5 @@ export function parseTarget(target: string): ParsedTarget {
*/
export function runTitle(target: string | null, fallback: string): string {
if (target) return parseTarget(target).display.replace(/\/$/, "");
return fallback || "Untitled scan";
return fallback || "Untitled pentest";
}

View file

@ -220,6 +220,10 @@ def _make_handler(state: _ViewerState) -> type[BaseHTTPRequestHandler]:
purpose = body.get("purpose")
posthog.viewer_email_event(str(event), purpose=str(purpose) if purpose else None)
elif event == "agent_steered":
from strix.telemetry import posthog
posthog.viewer_agent_steered()
self.send_response(HTTPStatus.NO_CONTENT)
self.end_headers()
@ -405,6 +409,11 @@ def _make_handler(state: _ViewerState) -> type[BaseHTTPRequestHandler]:
except auth.RelayError as exc:
self._send_relay_error(exc)
return
# Server-authoritative: fire only after a successful relay (respects
# the telemetry opt-out; no message/email content is sent).
from strix.telemetry import posthog
posthog.viewer_feedback_submitted()
self._send_json(HTTPStatus.OK, {"ok": True})
# Cap on a steering message so a runaway client cannot flood the agent.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -6,8 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark" />
<title>Strix Results</title>
<script type="module" crossorigin src="./assets/index-_2j_QfOq.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-BtBksasm.css">
<script type="module" crossorigin src="./assets/index-BNKUksp9.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-BdiSGmzb.css">
</head>
<body>
<div id="root"></div>

View file

@ -191,6 +191,62 @@ def test_server_event_endpoint_forwards_email_funnel(
httpd.server_close()
def test_server_event_endpoint_forwards_agent_steered(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
run_dir = _make_run(tmp_path, "steerevt", status="running", end_time=None)
_bundle(tmp_path, monkeypatch)
seen: list[bool] = []
monkeypatch.setattr("strix.telemetry.posthog.viewer_agent_steered", lambda: seen.append(True))
httpd, url, _ = serve(run_dir, open_browser=False)
try:
req = urllib.request.Request( # noqa: S310 - localhost test server
f"{url}/api/event",
data=json.dumps({"event": "agent_steered"}).encode(),
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as resp: # noqa: S310
assert resp.status == 204
assert seen == [True]
finally:
httpd.shutdown()
httpd.server_close()
def test_feedback_records_telemetry_on_success(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
run_dir = _make_run(tmp_path, "fbtel", status="running", end_time=None)
_bundle(tmp_path, monkeypatch)
sent: list[bool] = []
monkeypatch.setattr("strix.viewer.auth.feedback_submit", lambda *_a: None)
monkeypatch.setattr(
"strix.telemetry.posthog.viewer_feedback_submitted", lambda: sent.append(True)
)
httpd, url, token = serve(run_dir, open_browser=False)
try:
cookie = _session_cookie(url, token)
# A successful, session-holding submission relays and records telemetry.
status, _ = _post(
url, "/api/feedback", {"email": "a@b.com", "message": "hi"}, cookie=cookie
)
assert status == 200
assert sent == [True]
# A cookie-less caller is rejected and records nothing.
sent.clear()
status, _ = _post(url, "/api/feedback", {"email": "a@b.com", "message": "hi"})
assert status == 403
assert sent == []
finally:
httpd.shutdown()
httpd.server_close()
def _post(
url: str, path: str, payload: Mapping[str, object], *, cookie: str | None = None
) -> tuple[int, bytes]:
@ -395,9 +451,7 @@ def test_report_send_requires_session_cookie(
httpd.server_close()
def test_report_send_rejects_live_run(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
def test_report_send_rejects_live_run(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
# A running scan would only produce a partial report, so the endpoint must
# fail closed even for a verified, session-holding caller.
run_dir = _make_run(tmp_path, "live", status="running", end_time=None)
@ -406,9 +460,7 @@ def test_report_send_rejects_live_run(
httpd, url, token = serve(run_dir, open_browser=False)
try:
status, _ = _post(
url, "/api/report/send", {}, cookie=_session_cookie(url, token)
)
status, _ = _post(url, "/api/report/send", {}, cookie=_session_cookie(url, token))
assert status == 409
finally:
httpd.shutdown()