mirror of
https://github.com/usestrix/strix.git
synced 2026-08-28 05:25:00 +00:00
Local viewer: prominent scan switcher + rename to "pentest" terminology (#848)
This commit is contained in:
parent
ef07bad945
commit
1f7373714b
13 changed files with 150 additions and 67 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
10
strix/viewer/static/assets/index-BdiSGmzb.css
Normal file
10
strix/viewer/static/assets/index-BdiSGmzb.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue