diff --git a/pyproject.toml b/pyproject.toml index 5c595f07..0aa93ad1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "strix-agent" -version = "1.2.0" +version = "1.3.1" description = "Open-source AI Hackers for your apps" readme = "README.md" license = "Apache-2.0" diff --git a/scripts/install.sh b/scripts/install.sh index d45c325f..2024dd87 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -4,7 +4,7 @@ set -euo pipefail APP=strix REPO="usestrix/strix" -STRIX_IMAGE="ghcr.io/usestrix/strix-sandbox:1.0.0" +STRIX_IMAGE="ghcr.io/usestrix/strix-sandbox:1.1.0" MUTED='\033[0;2m' RED='\033[0;31m' diff --git a/strix/config/settings.py b/strix/config/settings.py index 2ca67eb2..f7a74cb3 100644 --- a/strix/config/settings.py +++ b/strix/config/settings.py @@ -52,7 +52,7 @@ class RuntimeSettings(BaseSettings): model_config = _BASE_CONFIG image: str = Field( - default="ghcr.io/usestrix/strix-sandbox:1.0.0", + default="ghcr.io/usestrix/strix-sandbox:1.1.0", alias="STRIX_IMAGE", ) backend: str = Field(default="docker", alias="STRIX_RUNTIME_BACKEND") diff --git a/strix/telemetry/posthog.py b/strix/telemetry/posthog.py index 48eb9936..79fb95d9 100644 --- a/strix/telemetry/posthog.py +++ b/strix/telemetry/posthog.py @@ -183,6 +183,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) diff --git a/strix/viewer/auth.py b/strix/viewer/auth.py index 1c7c5fef..c25d12b6 100644 --- a/strix/viewer/auth.py +++ b/strix/viewer/auth.py @@ -207,6 +207,26 @@ def otp_verify(email: str, code: str) -> dict[str, Any]: raise RelayError("unavailable") +def feedback_submit(email: str, message: str) -> None: + """Relay a feedback message + email to Strix. No verification is required; + the email is taken as given. Raises RelayError on failure.""" + status, data = _post_json( + "/api/oss/feedback", + {"email": email, "message": message}, + timeout=_OTP_TIMEOUT, + ) + if status == 200: + return + if status == 429: + raise RelayError("rate_limited") + if status == 400: + code = data.get("error") + if code in ("invalid_email", "invalid_message"): + raise RelayError(str(code)) + raise RelayError("invalid_message") + raise RelayError("unavailable") + + def report_send( token: str, pdf_bytes: bytes, @@ -241,6 +261,7 @@ def report_send( __all__ = [ "AUTH_PATH", "RelayError", + "feedback_submit", "forget", "is_verified", "otp_start", diff --git a/strix/viewer/frontend/package-lock.json b/strix/viewer/frontend/package-lock.json index 1c74cd0c..09790806 100644 --- a/strix/viewer/frontend/package-lock.json +++ b/strix/viewer/frontend/package-lock.json @@ -16,6 +16,7 @@ "lucide-react": "^0.563.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-icons": "^5.7.0", "react-markdown": "^10.1.0", "remark-gfm": "^4.0.1", "tailwind-merge": "^3.4.0" @@ -919,9 +920,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -936,9 +934,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -953,9 +948,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -970,9 +962,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -987,9 +976,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1004,9 +990,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1021,9 +1004,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1038,9 +1018,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1055,9 +1032,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1072,9 +1046,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1089,9 +1060,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1106,9 +1074,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1123,9 +1088,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1349,9 +1311,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1369,9 +1328,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1389,9 +1345,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1409,9 +1362,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2543,9 +2493,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2567,9 +2514,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2591,9 +2535,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2615,9 +2556,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3695,6 +3633,15 @@ "react": "^19.2.7" } }, + "node_modules/react-icons": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.7.0.tgz", + "integrity": "sha512-LBLy340Rzqy6+/yVhZKT3B/QpP1BZaesGqasf09HPOBzRarcDIFH0WwXlXQfE7q7ipxK4MSiC5DIBWURCny6fw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-markdown": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", diff --git a/strix/viewer/frontend/package.json b/strix/viewer/frontend/package.json index 37eaff55..e79db76f 100644 --- a/strix/viewer/frontend/package.json +++ b/strix/viewer/frontend/package.json @@ -17,6 +17,7 @@ "lucide-react": "^0.563.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-icons": "^5.7.0", "react-markdown": "^10.1.0", "remark-gfm": "^4.0.1", "tailwind-merge": "^3.4.0" diff --git a/strix/viewer/frontend/src/App.tsx b/strix/viewer/frontend/src/App.tsx index 8afc025c..042d54ac 100644 --- a/strix/viewer/frontend/src/App.tsx +++ b/strix/viewer/frontend/src/App.tsx @@ -2,14 +2,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ArrowLeft, AlertCircle, - Waypoints, + Bot, Mail, ChevronDown, - Wrench, - FileCheck2, - CalendarClock, Radar, - GitPullRequest, Rocket, ArrowUpRight, History, @@ -45,11 +41,10 @@ import PastRunsView from "@/components/PastRunsView"; import EmailReportView from "@/components/EmailReportView"; import { RunDetails } from "@/components/RunDetails"; import { TrustToast } from "@/components/TrustToast"; -import FeatureDetail from "@/components/FeatureDetail"; -import { ProTile, ProInlineCta, type ProItem } from "@/components/ProCta"; -import { FEATURES } from "@/lib/pro-features"; +import FeedbackView from "@/components/FeedbackView"; +import { ProInlineCta } from "@/components/ProCta"; -export type View = "overview" | "issues" | "agents" | "history" | "feature" | "email"; +export type View = "overview" | "issues" | "agents" | "history" | "email" | "feedback"; const TRUST_BANNER = "Your findings stay on your machine. They're rendered here locally in your browser and never uploaded or stored by Strix."; @@ -57,25 +52,12 @@ const TRUST_BANNER = const SEVERITY_ORDER: VulnerabilitySeverity[] = ["critical", "high", "medium", "low"]; const POLL_MS = 500; -// Curated inline CTAs. Continuous-coverage row on Overview (the restyled upsell -// tiles), plus the recommendations pairing. -const RECOMMENDATION_CTAS: ProItem[] = [ - { title: "One-click autofix + open a fix PR", desc: "Fix it for you and open a PR, retested.", slug: "autofix", icon: Wrench }, - { title: "Export SOC 2 / ISO 27001 report", desc: "Share an auditor-ready report with your team.", slug: "compliance", icon: FileCheck2 }, -]; -const COVERAGE_CTAS: ProItem[] = [ - { title: "Scheduled pentesting", desc: "Continuous coverage for your whole org.", slug: "scheduled", icon: CalendarClock }, - { title: "Attack surface monitoring", desc: "Continuous coverage for your whole org.", slug: "asm", icon: Radar }, - { title: "PR reviews", desc: "Pentest every pull request your team opens.", slug: "pr_reviews", icon: GitPullRequest }, -]; - export default function App() { const [activeRun, setActiveRun] = useState(null); const [run, setRun] = useState(null); const [error, setError] = useState(null); const [selectedId, setSelectedId] = useState(null); const [view, setView] = useState("overview"); - const [activeFeature, setActiveFeature] = useState(null); const [auth, setAuth] = useState(null); const [runs, setRuns] = useState(null); const [emailPurpose, setEmailPurpose] = useState<"report" | "verify">("report"); @@ -249,12 +231,6 @@ export default function App() { await refreshRuns(); }, [refreshAuth, refreshRuns]); - const selectFeature = useCallback((slug: string) => { - trackCta(slug, "sidebar_nav"); - setActiveFeature(slug); - userSetView("feature"); - }, [userSetView]); - const onForget = useCallback(async () => { await forgetAuth(); await refreshAuth(); @@ -266,11 +242,13 @@ export default function App() { { + // Clicking a sidebar view always lands on that section's top level, + // so leaving a specific issue's detail view and clicking "Issues" + // returns to the full findings list. + setSelectedId(null); if (v === "history") openHistory(); else userSetView(v); }} - activeFeature={activeFeature} - onSelectFeature={selectFeature} issuesCount={run?.vulnerabilities.length ?? 0} agentCount={agentCount} runCount={runs?.count ?? 0} @@ -285,7 +263,7 @@ export default function App() {
{/* Top bar */}
-
+
Strix
- Local results {run && }
{verified && runs && !runs.locked && runs.runs.length > 0 && ( @@ -322,14 +299,20 @@ export default function App() {
-
- {error && !run && view !== "history" && view !== "email" && view !== "feature" && ( +
+ {error && !run && view !== "history" && view !== "email" && (
)} + {/* Keyed wrapper: re-mounts on every view / finding / run change so the + page-in transition replays. */} +
{view === "email" ? ( setView(dest === "history" ? "history" : "overview")} /> - ) : view === "feature" && activeFeature && FEATURES[activeFeature] ? ( - + ) : view === "feedback" ? ( + setView(dest)} + /> ) : view === "history" ? (
@@ -369,7 +355,7 @@ export default function App() { {/* Tab strip: shown on small screens where the sidebar is hidden. */}
userSetView("overview")}> - Overview + Pentest Overview userSetView("issues")}> Issues{run.vulnerabilities.length > 0 ? ` (${run.vulnerabilities.length})` : ""} @@ -412,6 +398,7 @@ export default function App() { )} ) : null} +
@@ -438,33 +425,37 @@ function RunSwitcher({ {open && (
+
+ Switch pentest +
{runs.runs.map((r) => { const active = r.name === activeRun; return ( ); })} @@ -508,7 +499,7 @@ function SummaryHeader({ summary }: { summary: ParsedRunSummary }) { return (

- {runTitle(summary.targets[0] ?? null, summary.runName ?? summary.runId ?? "Scan results")} + {runTitle(summary.targets[0] ?? null, summary.runName ?? summary.runId ?? "Pentest results")}

{summary.targets.length > 0 && ( @@ -547,7 +538,7 @@ function FindingsList({ return (
- {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…"}
{finished && (
@@ -573,7 +564,7 @@ function FindingsList({
- Email report + Export report to PDF
@@ -673,26 +664,32 @@ function OverviewTab({ return (
- +
+ +
{total > 0 && ( -
+
)} {/* Primary CTA: the one primary on Overview. Hidden until the run is finished, since a live scan would only email a partial report. */} - {finished && } + {finished && ( +
+ +
+ )} {sections.length > 0 ? ( -
+
{sections.map((s) => ( ))}
) : reportMarkdown ? ( -
+
) : ( @@ -701,22 +698,6 @@ function OverviewTab({ ) )} - {/* Near Recommendations: act on the fixes. */} -
- {RECOMMENDATION_CTAS.map((item) => ( - - ))} -
- - {/* Continuous coverage for your org (restyled upsell tiles). */} -
-

Continuous coverage for your org

-
- {COVERAGE_CTAS.map((item) => ( - - ))} -
-
); } @@ -746,8 +727,7 @@ function TabButton({ function AgentsTab({ run, canSteer }: { run: LoadedRun; canSteer: boolean }) { const { agents, events } = run.transcript; const graphAgents = useMemo(() => buildGraphAgents(agents, events), [agents, events]); - // Clicking a graph node opens the agent's transcript in a modal (matching the - // cloud app); no node selected means no modal. + // Clicking a graph node opens the agent's transcript in a modal; no node selected means no modal. const [selectedId, setSelectedId] = useState(null); const selectedAgent = selectedId ? (agents.find((a) => a.id === selectedId) ?? null) : null; @@ -758,7 +738,7 @@ function AgentsTab({ run, canSteer }: { run: LoadedRun; canSteer: boolean }) {
-