From 052098ec32e2919035821a8fe9dbd4ad680aa445 Mon Sep 17 00:00:00 2001 From: spetro511 Date: Thu, 3 Sep 2026 00:33:07 -0400 Subject: [PATCH 1/4] fix(app): keep security workbench live Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 3 + README.md | 2 +- packages/app/src/app.tsx | 5 +- packages/app/src/context/workbench.tsx | 254 ++++++++++++++++++ packages/app/src/pages/session.tsx | 33 ++- .../src/pages/session/activity-panel.test.ts | 35 ++- .../app/src/pages/session/activity-panel.tsx | 93 +------ packages/app/src/pages/session/activity.ts | 51 ++++ .../app/src/pages/session/memory-panel.tsx | 27 +- .../app/src/pages/session/mission-panel.tsx | 76 +++--- .../src/pages/session/session-side-panel.tsx | 57 +++- .../app/src/pages/session/terminal-panel.tsx | 22 +- .../app/src/pages/session/topology-panel.tsx | 42 ++- .../pages/session/use-session-commands.tsx | 6 +- .../app/src/pages/session/workbench-bar.tsx | 92 +++++++ packages/cyberstrike/README.md | 2 +- .../prompt/methodology/common-methodology.txt | 11 + packages/cyberstrike/src/event/index.ts | 60 ++++- .../src/server/routes/event-log.ts | 21 +- packages/cyberstrike/src/tool/bash.ts | 32 +++ packages/cyberstrike/test/agent/agent.test.ts | 1 + packages/cyberstrike/test/event/event.test.ts | 174 ++++++++++++ packages/cyberstrike/test/tool/bash.test.ts | 71 +++++ packages/sdk/js/src/v2/gen/sdk.gen.ts | 2 + packages/sdk/js/src/v2/gen/types.gen.ts | 1 + 25 files changed, 1002 insertions(+), 171 deletions(-) create mode 100644 packages/app/src/context/workbench.tsx create mode 100644 packages/app/src/pages/session/workbench-bar.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 18b2a82b02..f6551ad16e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,15 +18,18 @@ Format based on [Keep a Changelog](https://keepachangelog.com/), versions follow - Version-pinned MCP catalog with manual/optional install states and three additional owner-maintained integrations - Safe recovery startup with `--safe` and namespaced extension configuration - Target-selectable source builds and a localhost-only systemd user service template for Kali/Linux deployments +- Always-visible live workbench status with resilient activity streaming, meaningful change badges, and event-driven Mission, Topology, and Memory refresh ### Changed - MCP tool materialization now filters schemas before conversion and enforces real context-budget eviction +- Agent methodology now routes Nmap through the approval-gated `nmap_scan` system of record and explicitly records verified discoveries, findings, and reusable memory for workbench projection ### Fixed - Release and local-binary installs now deploy the bundled HackBrowser worker to the runtime data directory - Web update checks now honor disabled auto-updates, avoiding false upgrade prompts for managed source builds +- Hidden workbench panels no longer miss live changes or depend on visible-only polling; repeated session lifecycle events and reconnect gaps are recovered ## [1.1.16] — 2026-08-08 diff --git a/README.md b/README.md index d7cd88e929..d6e4e5e320 100644 --- a/README.md +++ b/README.md @@ -333,7 +333,7 @@ If user or project configuration prevents startup, run `cyberstrike web --safe` | **Web Context** | Endpoints, roles, credentials, and functions discovered during active sessions | | **Mission** | Methodology phases, coverage, blockers, attack chains, agents, and safe CTAs | | **Topology** | Evidence-linked assets, Nmap hosts/services/routes, scan history/diffs, endpoints, identities, and findings | -| **Activity** | Durable Agent/Tool/MCP/Bolt/Browser/PTY lanes with filtering and JSONL export | +| **Activity** | Always-visible live status plus durable Agent/Tool/MCP/Bolt/Browser/PTY lanes, filtering, reconnect recovery, and JSONL export | | **Memory** | Trust-ranked structured memory, FTS search, redaction, notes, and invalidation | [**app.cyberstrike.io**](https://app.cyberstrike.io) is a hosted static page (no backend, no data storage) for convenience. Or self-host: clone the repo and serve `packages/app/dist/` from your own domain. diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index a942ada714..72fea45a53 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -21,6 +21,7 @@ import { TerminalProvider } from "@/context/terminal" import { PromptProvider } from "@/context/prompt" import { FileProvider } from "@/context/file" import { CommentsProvider } from "@/context/comments" +import { WorkbenchProvider } from "@/context/workbench" import { NotificationProvider } from "@/context/notification" import { ModelsProvider } from "@/context/models" import { DialogProvider } from "@cyberstrike-io/ui/context/dialog" @@ -92,7 +93,9 @@ function SessionProviders(props: ParentProps) { - {props.children} + + {props.children} + diff --git a/packages/app/src/context/workbench.tsx b/packages/app/src/context/workbench.tsx new file mode 100644 index 0000000000..3efb383f2a --- /dev/null +++ b/packages/app/src/context/workbench.tsx @@ -0,0 +1,254 @@ +import { createSimpleContext } from "@cyberstrike-io/ui/context" +import { useParams } from "@solidjs/router" +import { createEffect, createMemo, onCleanup, untrack } from "solid-js" +import { createStore, produce, reconcile } from "solid-js/store" +import { useSDK } from "@/context/sdk" +import { + activityChannels, + activityRefreshChannels, + isActivity, + mergeActivity, + type Activity, + type ActivitySource, + type WorkbenchChannel, +} from "@/pages/session/activity" + +const blank = () => ({ + activity: 0, + mission: 0, + topology: 0, + memory: 0, + mcp: 0, + bolt: 0, + terminal: 0, + vulns: 0, + web: 0, +}) + +export const { use: useWorkbench, provider: WorkbenchProvider } = createSimpleContext({ + name: "Workbench", + init: () => { + const params = useParams() + const sdk = useSDK() + const [events, setEvents] = createStore([]) + const [state, setState] = createStore({ + connected: false, + error: "", + changes: blank(), + revision: blank(), + last: blank(), + }) + + const bump = (channel: WorkbenchChannel, time = Date.now(), changed = true) => { + if (changed) setState("changes", channel, (value) => value + 1) + setState("revision", channel, (value) => value + 1) + setState("last", channel, time) + } + + const marked = new Set() + const mark = (event: Activity) => { + if (marked.has(event.id)) return + marked.delete(event.id) + marked.add(event.id) + while (marked.size > 2_000) marked.delete(marked.values().next().value!) + for (const channel of activityChannels(event)) bump(channel, event.time) + for (const channel of activityRefreshChannels(event)) bump(channel, event.time, false) + } + + const add = (event: Activity) => { + const index = events.findIndex((item) => item.id === event.id) + if (index !== -1) { + setEvents(index, reconcile(event)) + mark(event) + return + } + setEvents( + produce((draft) => { + draft.push(event) + if (draft.length > 2_000) draft.splice(0, draft.length - 2_000) + }), + ) + mark(event) + } + + createEffect(() => { + const sessionID = params.id + sdk.directory + marked.clear() + setEvents(reconcile([])) + setState({ + connected: false, + error: "", + changes: blank(), + revision: blank(), + last: blank(), + }) + if (!sessionID) return + + const abort = new AbortController() + const client = sdk.createClient({ + directory: sdk.directory, + throwOnError: true, + signal: abort.signal, + }) + let connected = false + let hydrated = false + let requested = 0 + let completed = 0 + let syncing: Promise | undefined + let streamError = "" + let historyError = "" + const render = () => setState({ connected, error: historyError || streamError }) + const snapshot = async () => { + const known = new Set(untrack(() => events.map((event) => event.id))) + const pages: Activity[] = [] + let before: number | undefined + let beforeID: string | undefined + while (pages.length < 2_000) { + const limit = Math.min(500, 2_000 - pages.length) + const response = await client.eventLog.list({ sessionID, before, beforeID, limit }) + const page = (response.data ?? []).filter(isActivity) + if (page.length === 0) break + pages.unshift(...page) + if (hydrated && page.some((event) => known.has(event.id))) break + if (page.length < limit) break + const next = page[0]!.time + const nextID = page[0]!.id + if (before === next && beforeID === nextID) break + before = next + beforeID = nextID + } + return { known, incoming: mergeActivity(pages, [], 2_000) } + } + const sync = () => { + if (syncing) return syncing + syncing = (async () => { + while (!abort.signal.aborted) { + const version = requested + try { + const history = await snapshot() + if (abort.signal.aborted) return + const changed = hydrated + setEvents(reconcile(mergeActivity(history.incoming, [...events]))) + if (changed) history.incoming.filter((event) => !history.known.has(event.id)).forEach(mark) + hydrated = true + completed = version + historyError = "" + render() + if (requested <= completed) return + } catch (cause) { + if (abort.signal.aborted) return + historyError = cause instanceof Error ? cause.message : String(cause) + render() + await new Promise((resolve) => setTimeout(resolve, 500)) + } + } + })().finally(() => { + syncing = undefined + }) + return syncing + } + const requestSync = () => { + requested++ + return sync() + } + const unsubs = [ + sdk.event.on("memory.updated", (event) => { + if (!event.properties.sessionID) bump("memory") + }), + sdk.event.on("mcp.tools.changed", () => bump("mcp")), + sdk.event.on("pty.created", () => bump("terminal")), + sdk.event.on("pty.updated", () => bump("terminal")), + sdk.event.on("pty.exited", () => bump("terminal")), + sdk.event.on("pty.deleted", () => bump("terminal")), + ] + + void requestSync() + let timer: ReturnType | undefined + void (async () => { + while (!abort.signal.aborted) { + try { + const response = await client.eventLog.stream( + { sessionID }, + { + onSseError: (cause) => { + if (abort.signal.aborted) return + connected = false + streamError = cause instanceof Error ? cause.message : String(cause) + render() + }, + onSseEvent: () => { + if (timer) clearTimeout(timer) + timer = undefined + const recovered = !connected + connected = true + streamError = "" + render() + if (recovered) void requestSync() + }, + }, + ) + timer = setTimeout(() => { + if (abort.signal.aborted) return + streamError = "Live activity stream did not connect" + render() + }, 5_000) + let next = await response.stream.next() + while (!next.done && !abort.signal.aborted) { + if (isActivity(next.value)) add(next.value) + next = await response.stream.next() + } + if (abort.signal.aborted) return + connected = false + streamError = "Live activity stream disconnected" + render() + } catch (cause) { + if (abort.signal.aborted) return + connected = false + streamError = cause instanceof Error ? cause.message : String(cause) + render() + } finally { + if (timer) clearTimeout(timer) + } + await new Promise((resolve) => setTimeout(resolve, 500)) + } + })() + + onCleanup(() => { + if (timer) clearTimeout(timer) + abort.abort() + unsubs.forEach((unsub) => unsub()) + }) + }) + + const latest = createMemo(() => events.at(-1)) + + return { + get events() { + return events + }, + get connected() { + return state.connected + }, + get error() { + return state.error + }, + latest, + count(source: ActivitySource) { + return events.filter((event) => event.source === source).length + }, + changes(channel: WorkbenchChannel) { + return state.changes[channel] + }, + revision(channel: WorkbenchChannel) { + return state.revision[channel] + }, + last(channel: WorkbenchChannel) { + return state.last[channel] + }, + ack(channel: WorkbenchChannel) { + setState("changes", channel, 0) + }, + } + }, +}) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 3ccf68e238..db91b1f60a 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -59,6 +59,7 @@ import { SessionMobileTabs } from "@/pages/session/session-mobile-tabs" import { SessionSidePanel } from "@/pages/session/session-side-panel" import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll" import { useServer } from "@/context/server" +import { WorkbenchBar } from "@/pages/session/workbench-bar" type HandoffSession = { prompt: string @@ -121,6 +122,7 @@ export default function Page() { const blocked = createMemo(() => !!permRequest() || !!questionRequest()) const [ui, setUi] = createStore({ + activity: false, responding: false, pendingMessage: undefined as string | undefined, scrollGesture: 0, @@ -691,6 +693,7 @@ export default function Page() { setUi("autoCreated", false) return } + if (ui.activity) return if (observer()) return if (!terminal.ready() || terminal.all().length !== 0 || ui.autoCreated) return terminal.new() @@ -701,7 +704,7 @@ export default function Page() { on( () => terminal.all().length, (count, prevCount) => { - if (prevCount !== undefined && prevCount > 0 && count === 0) { + if (prevCount !== undefined && prevCount > 0 && count === 0 && !ui.activity) { if (view().terminal.opened()) { view().terminal.toggle() } @@ -737,6 +740,16 @@ export default function Page() { ) const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? idle) + const openActivity = () => { + setUi("activity", true) + if (!view().terminal.opened()) view().terminal.open() + } + const newTerminal = () => { + setUi("activity", false) + setUi("autoCreated", true) + view().terminal.open() + terminal.new() + } createEffect( on( @@ -745,6 +758,7 @@ export default function Page() { setStore("messageId", undefined) setStore("expanded", {}) setStore("changes", "session") + setUi("activity", false) setUi("autoCreated", false) }, { defer: true }, @@ -958,6 +972,7 @@ export default function Page() { setActiveMessage, addSelectionToContext, focusInput, + newTerminal, }) const openReviewFile = createOpenReviewFile({ @@ -1572,6 +1587,14 @@ export default function Page() { return (
+ openTab("mission-panel")} + onTopology={() => openTab("topology-panel")} + onMemory={() => openTab("memory-panel")} + />
{ + view().terminal.close() + setUi("activity", false) + }} + activity={ui.activity} + setActivity={(value) => setUi("activity", value)} + newTerminal={newTerminal} terminal={terminal} readOnly={observer()} language={language} diff --git a/packages/app/src/pages/session/activity-panel.test.ts b/packages/app/src/pages/session/activity-panel.test.ts index 68e57c026d..a5b64a9b96 100644 --- a/packages/app/src/pages/session/activity-panel.test.ts +++ b/packages/app/src/pages/session/activity-panel.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { isActivity, mergeActivity } from "./activity" +import { activityChannels, activityRefreshChannels, isActivity, mergeActivity } from "./activity" const event = (id: string, time: number, title = id) => ({ id, @@ -26,4 +26,37 @@ describe("activity history", () => { expect(isActivity({})).toBe(false) expect(isActivity(event("one", 1))).toBe(true) }) + + test("routes live changes to affected workbench surfaces", () => { + expect(activityChannels({ ...event("memory", 1), type: "memory.updated", source: "system" })).toEqual([ + "activity", + "memory", + ]) + expect(activityChannels({ ...event("nmap", 2), type: "nmap.scan.updated", source: "tool" })).toEqual([ + "activity", + "topology", + ]) + expect( + activityChannels({ ...event("finding", 3), type: "vulnerability.updated", source: "finding" }), + ).toEqual(["activity", "vulns", "mission", "topology"]) + expect(activityChannels({ ...event("intel", 4), type: "intel.updated", source: "finding" })).toEqual([ + "activity", + "mission", + "topology", + ]) + }) + + test("refreshes derived surfaces without marking unchanged tabs", () => { + const idle = { ...event("idle", 1), type: "session.idle" } + expect(activityChannels(idle)).toEqual(["activity"]) + expect(activityRefreshChannels(idle)).toEqual([ + "mission", + "topology", + "memory", + "mcp", + "bolt", + "vulns", + "web", + ]) + }) }) diff --git a/packages/app/src/pages/session/activity-panel.tsx b/packages/app/src/pages/session/activity-panel.tsx index 6d0c80ec18..d55c708801 100644 --- a/packages/app/src/pages/session/activity-panel.tsx +++ b/packages/app/src/pages/session/activity-panel.tsx @@ -1,9 +1,8 @@ -import { For, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js" -import { createStore, produce, reconcile } from "solid-js/store" +import { For, Show, createEffect, createMemo, createSignal } from "solid-js" import { useParams } from "@solidjs/router" import { Icon } from "@cyberstrike-io/ui/icon" -import { useSDK } from "@/context/sdk" -import { isActivity, mergeActivity, type Activity, type ActivitySource } from "./activity" +import { useWorkbench } from "@/context/workbench" +import { activitySummary, type Activity, type ActivitySource } from "./activity" const sources: Array<{ id: ActivitySource; label: string }> = [ { id: "agent", label: "Agent" }, @@ -24,18 +23,6 @@ const badge = (source: ActivitySource) => { return "bg-surface-base text-text-weak" } -const value = (data: Record, key: string) => - typeof data[key] === "string" || typeof data[key] === "number" ? String(data[key]) : "" - -const summary = (event: Activity) => { - const title = value(event.data, "title") - const tool = value(event.data, "tool") - const status = value(event.data, "status") - const name = value(event.data, "name") - const count = value(event.data, "count") - return [tool || name || event.type, status, title, count ? `${count} items` : ""].filter(Boolean).join(" · ") -} - function ActivityRow(props: { event: Activity }) { return (
@@ -50,7 +37,7 @@ function ActivityRow(props: { event: Activity }) { {props.event.source.toUpperCase()} - {summary(props.event)} + {activitySummary(props.event)}
         {JSON.stringify(
@@ -70,79 +57,23 @@ function ActivityRow(props: { event: Activity }) {
 
 export function ActivityPanel() {
   const params = useParams()
-  const sdk = useSDK()
-  const [events, setEvents] = createStore([])
+  const workbench = useWorkbench()
   const [source, setSource] = createSignal("all")
   const [search, setSearch] = createSignal("")
   const [mode, setMode] = createSignal<"timeline" | "lanes">("timeline")
   const [follow, setFollow] = createSignal(true)
-  const [error, setError] = createSignal("")
   let scroll!: HTMLDivElement
 
-  const add = (event: Activity) => {
-    const index = events.findIndex((item) => item.id === event.id)
-    if (index !== -1) {
-      setEvents(index, reconcile(event))
-      return
-    }
-    setEvents(
-      produce((draft) => {
-        draft.push(event)
-        if (draft.length > 2_000) draft.splice(0, draft.length - 2_000)
-      }),
-    )
-  }
-
-  const merge = (incoming: Activity[]) => {
-    setEvents(reconcile(mergeActivity(incoming, [...events])))
-  }
-
   createEffect(() => {
-    const sessionID = params.id
-    if (!sessionID) {
-      setEvents(reconcile([]))
-      return
-    }
-
-    const abort = new AbortController()
-    const client = sdk.createClient({
-      directory: sdk.directory,
-      throwOnError: true,
-      signal: abort.signal,
-    })
-    setError("")
-    void client.eventLog
-      .list({ sessionID, limit: 500 })
-      .then((response) => merge(response.data ?? []))
-      .catch((cause) => {
-        if (!abort.signal.aborted) setError(cause instanceof Error ? cause.message : String(cause))
-      })
-    void (async () => {
-      try {
-        const response = await client.eventLog.stream(
-          { sessionID },
-          {
-            onSseError: (cause) => {
-              if (!abort.signal.aborted) setError(cause instanceof Error ? cause.message : String(cause))
-            },
-          },
-        )
-        for await (const event of response.stream) {
-          if (isActivity(event)) add(event)
-        }
-      } catch (cause) {
-        if (!abort.signal.aborted) setError(cause instanceof Error ? cause.message : String(cause))
-      }
-    })()
-    onCleanup(() => abort.abort())
+    if (workbench.changes("activity") > 0) workbench.ack("activity")
   })
 
   const filtered = createMemo(() => {
     const query = search().trim().toLowerCase()
-    return events.filter((event) => {
+    return workbench.events.filter((event) => {
       if (source() !== "all" && event.source !== source()) return false
       if (!query) return true
-      return `${event.type} ${summary(event)} ${event.correlationID ?? ""}`.toLowerCase().includes(query)
+      return `${event.type} ${activitySummary(event)} ${event.correlationID ?? ""}`.toLowerCase().includes(query)
     })
   })
 
@@ -174,7 +105,7 @@ export function ActivityPanel() {
           }}
           onClick={() => setSource("all")}
         >
-          All {events.length}
+          All {workbench.events.length}
         
         
           {(item) => (
@@ -226,8 +157,10 @@ export function ActivityPanel() {
           
         
       
- -
{error()}
+ +
+ {workbench.error} +
[event.id, event])) return [...byID.values()].sort((a, b) => a.time - b.time).slice(-limit) } + +const value = (data: Record, key: string) => + typeof data[key] === "string" || typeof data[key] === "number" ? String(data[key]) : "" + +export const activitySummary = (event: Activity) => { + const title = value(event.data, "title") + const tool = value(event.data, "tool") + const status = value(event.data, "status") + const name = value(event.data, "name") + const count = value(event.data, "count") + return [tool || name || event.type, status, title, count ? `${count} items` : ""].filter(Boolean).join(" · ") +} + +export const activityChannels = (event: Activity): WorkbenchChannel[] => { + const channels: WorkbenchChannel[] = ["activity"] + if (event.source === "mcp") channels.push("mcp") + if (event.source === "bolt") channels.push("bolt") + if (event.source === "pty") channels.push("terminal") + if (event.type.startsWith("memory.")) channels.push("memory") + if ( + event.type.startsWith("methodology.") || + event.type.startsWith("intel.") || + event.type.startsWith("coverage.") + ) + channels.push("mission") + if (event.type.startsWith("intel.")) channels.push("topology") + if (event.type.startsWith("vulnerability.")) channels.push("vulns", "mission", "topology") + if (event.type.startsWith("nmap.") || event.type.startsWith("dossier.note.")) channels.push("topology") + if ( + event.type.startsWith("request.") || + event.type.startsWith("web.") || + event.type.startsWith("web_") || + event.type.startsWith("hackbrowser.") || + event.type.startsWith("observation.") + ) + channels.push("web", "topology") + return [...new Set(channels)] +} + +export const activityRefreshChannels = (event: Activity): WorkbenchChannel[] => + event.type === "session.idle" ? ["mission", "topology", "memory", "mcp", "bolt", "vulns", "web"] : [] diff --git a/packages/app/src/pages/session/memory-panel.tsx b/packages/app/src/pages/session/memory-panel.tsx index dbacbdb7f1..a2165320bd 100644 --- a/packages/app/src/pages/session/memory-panel.tsx +++ b/packages/app/src/pages/session/memory-panel.tsx @@ -1,9 +1,10 @@ -import { For, Show, createEffect, createSignal, onCleanup } from "solid-js" +import { For, Show, createEffect, createSignal, on, onCleanup } from "solid-js" import { createStore, reconcile } from "solid-js/store" import { useParams } from "@solidjs/router" import type { MemoryListResponse } from "@cyberstrike-io/sdk/v2/client" import { Icon } from "@cyberstrike-io/ui/icon" import { useSDK } from "@/context/sdk" +import { useWorkbench } from "@/context/workbench" type Kind = MemoryListResponse[number]["kind"] @@ -24,6 +25,7 @@ const trust = (value: MemoryListResponse[number]["trust"]) => { export function MemoryPanel() { const params = useParams() const sdk = useSDK() + const workbench = useWorkbench() const [items, setItems] = createStore([]) const [query, setQuery] = createSignal("") const [kind, setKind] = createSignal("all") @@ -68,6 +70,8 @@ export function MemoryPanel() { onCleanup(() => clearTimeout(timer)) }) + createEffect(on(() => workbench.revision("memory"), () => void load(), { defer: true })) + const add = async () => { const title = form.title.trim() const content = form.content.trim() @@ -111,13 +115,20 @@ export function MemoryPanel() {
Persistent memory - +
+ 0}> + + Live · {new Date(workbench.last("memory")).toLocaleTimeString()} + + + +
diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index c4df5b9794..3e93778950 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -44,6 +44,8 @@ import { useServer } from "@/context/server" import { MissionPanel } from "@/pages/session/mission-panel" import { TopologyPanel } from "@/pages/session/topology-panel" import { MemoryPanel } from "@/pages/session/memory-panel" +import { useWorkbench } from "@/context/workbench" +import type { WorkbenchChannel } from "@/pages/session/activity" const statusDot = (status: string) => { if (status === "connected") return "bg-icon-success-base" @@ -53,6 +55,26 @@ const statusDot = (status: string) => { return "bg-surface-inset-base" } +const panelChannels: Record = { + "mcp-panel": "mcp", + "mission-panel": "mission", + "bolt-panel": "bolt", + "vulns-panel": "vulns", + "web-panel": "web", + "topology-panel": "topology", + "memory-panel": "memory", +} + +function ChangeCount(props: { value: number }) { + return ( + 0}> + + {Math.min(props.value, 99)} + + + ) +} + type McpCatalogEntry = { id: string name: string @@ -1196,6 +1218,13 @@ export function SessionSidePanel(props: { }) { const openedTabs = createMemo(() => props.openedTabs()) const server = useServer() + const workbench = useWorkbench() + + createEffect(() => { + if (!props.open || !props.reviewOpen) return + const channel = panelChannels[props.activeTab()] + if (channel && workbench.changes(channel) > 0) workbench.ack(channel) + }) return ( @@ -1263,26 +1292,40 @@ export function SessionSidePanel(props: { -
MCP
+
+ MCP +
-
Mission
+
+ Mission +
-
Bolt
+
+ Bolt +
-
Vulns
+
+ Vulns +
-
Web
+
+ Web +
-
Topology
+
+ Topology +
-
Memory
+
+ Memory +
diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx index 160fd684ec..41d1ea7bc8 100644 --- a/packages/app/src/pages/session/terminal-panel.tsx +++ b/packages/app/src/pages/session/terminal-panel.tsx @@ -1,4 +1,4 @@ -import { For, Show, createMemo, createSignal } from "solid-js" +import { For, Show, createMemo } from "solid-js" import { Tabs } from "@cyberstrike-io/ui/tabs" import { ResizeHandle } from "@cyberstrike-io/ui/resize-handle" import { IconButton } from "@cyberstrike-io/ui/icon-button" @@ -19,6 +19,9 @@ export function TerminalPanel(props: { height: number resize: (value: number) => void close: () => void + activity: boolean + setActivity: (value: boolean) => void + newTerminal: () => void terminal: ReturnType readOnly: boolean language: ReturnType @@ -33,8 +36,6 @@ export function TerminalPanel(props: { const all = createMemo(() => props.terminal.all()) const ids = createMemo(() => all().map((pty) => pty.id)) const byId = createMemo(() => new Map(all().map((pty) => [pty.id, pty]))) - const [activity, setActivity] = createSignal(false) - return (
{ if (id === "activity") { - setActivity(true) + props.setActivity(true) return } - setActivity(false) + props.setActivity(false) props.terminal.open(id) }} class="!h-auto !flex-none" @@ -130,10 +131,7 @@ export function TerminalPanel(props: { icon="plus-small" variant="ghost" iconSize="large" - onClick={() => { - setActivity(false) - props.terminal.new() - }} + onClick={props.newTerminal} aria-label={props.language.t("command.terminal.new")} /> @@ -142,7 +140,7 @@ export function TerminalPanel(props: {
- +
@@ -155,7 +153,7 @@ export function TerminalPanel(props: { class="absolute inset-0" style={{ display: props.terminal.active() === pty.id ? "block" : "none", - visibility: activity() ? "hidden" : "visible", + visibility: props.activity ? "hidden" : "visible", }} > diff --git a/packages/app/src/pages/session/topology-panel.tsx b/packages/app/src/pages/session/topology-panel.tsx index 91f94c1cec..27cc3a87f7 100644 --- a/packages/app/src/pages/session/topology-panel.tsx +++ b/packages/app/src/pages/session/topology-panel.tsx @@ -1,4 +1,4 @@ -import { For, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js" +import { For, Show, createEffect, createMemo, createSignal, on, onCleanup } from "solid-js" import { createStore, reconcile } from "solid-js/store" import { useParams } from "@solidjs/router" import type { @@ -11,6 +11,7 @@ import { Icon } from "@cyberstrike-io/ui/icon" import { useSDK } from "@/context/sdk" import { useServer } from "@/context/server" import { usePrompt } from "@/context/prompt" +import { useWorkbench } from "@/context/workbench" type Node = TopologyGetResponse["nodes"][number] type Kind = Node["kind"] @@ -44,6 +45,7 @@ export function TopologyPanel() { const sdk = useSDK() const server = useServer() const prompt = usePrompt() + const workbench = useWorkbench() const [graph, setGraph] = createStore({ sessionID: "", nodes: [], @@ -68,6 +70,9 @@ export function TopologyPanel() { }) let fileInput!: HTMLInputElement let generation = 0 + let pending = false + let loading: Promise | undefined + let refreshTimer: ReturnType | undefined const load = async () => { const sessionID = params.id @@ -95,6 +100,19 @@ export function TopologyPanel() { return false } } + const refresh = () => { + if (loading) { + pending = true + return loading + } + loading = load().finally(() => { + loading = undefined + if (!pending) return + pending = false + void refresh() + }) + return loading + } createEffect(() => { params.id @@ -102,10 +120,10 @@ export function TopologyPanel() { setTo("") setDiff(undefined) let alive = true - void load() + void refresh() const timer = setInterval(() => { - if (alive) void load() - }, 10_000) + if (alive) void refresh() + }, 30_000) onCleanup(() => { alive = false generation++ @@ -113,6 +131,20 @@ export function TopologyPanel() { }) }) + createEffect( + on( + () => workbench.revision("topology"), + () => { + if (refreshTimer) clearTimeout(refreshTimer) + refreshTimer = setTimeout(() => void refresh(), 250) + }, + { defer: true }, + ), + ) + onCleanup(() => { + if (refreshTimer) clearTimeout(refreshTimer) + }) + createEffect(() => { const sessionID = params.id const baseline = from() @@ -157,7 +189,7 @@ export function TopologyPanel() { name: file.name.replace(/\.xml$/i, ""), xml: await file.text(), }) - await load() + await refresh() setError("") } catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)) diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx index 8ffb2dbb98..3cecd47df0 100644 --- a/packages/app/src/pages/session/use-session-commands.tsx +++ b/packages/app/src/pages/session/use-session-commands.tsx @@ -53,6 +53,7 @@ export type SessionCommandContext = { setActiveMessage: (message: UserMessage | undefined) => void addSelectionToContext: (path: string, selection: FileSelection) => void focusInput: () => void + newTerminal: () => void } const withCategory = (category: string) => { @@ -184,10 +185,7 @@ export const useSessionCommands = (input: SessionCommandContext) => { title: input.language.t("command.terminal.new"), description: input.language.t("command.terminal.new.description"), keybind: "ctrl+alt+t", - onSelect: () => { - if (input.terminal.all().length > 0) input.terminal.new() - input.view().terminal.open() - }, + onSelect: input.newTerminal, }), viewCommand({ id: "steps.toggle", diff --git a/packages/app/src/pages/session/workbench-bar.tsx b/packages/app/src/pages/session/workbench-bar.tsx new file mode 100644 index 0000000000..04a6b5504b --- /dev/null +++ b/packages/app/src/pages/session/workbench-bar.tsx @@ -0,0 +1,92 @@ +import { Show, createMemo } from "solid-js" +import { useParams } from "@solidjs/router" +import { useWorkbench } from "@/context/workbench" +import { activitySummary } from "@/pages/session/activity" + +function Count(props: { value: number }) { + return ( + 0}> + + {Math.min(props.value, 99)} + + + ) +} + +export function WorkbenchBar(props: { + busy: boolean + observer: boolean + onActivity: () => void + onMission: () => void + onTopology: () => void + onMemory: () => void +}) { + const params = useParams() + const workbench = useWorkbench() + const latest = createMemo(() => { + const event = workbench.latest() + return event ? activitySummary(event) : "Waiting for session activity" + }) + + return ( + +
+ + +
+ {latest()} +
+ + + + + +
+
+ ) +} diff --git a/packages/cyberstrike/README.md b/packages/cyberstrike/README.md index 17093e20e9..893debf778 100644 --- a/packages/cyberstrike/README.md +++ b/packages/cyberstrike/README.md @@ -180,7 +180,7 @@ Each proxy tester follows a structured methodology: intercept traffic, identify ### Web UI & Remote Access -Run `cyberstrike web` and control your agents, MCP servers, Bolt connections, activity lanes, mission posture, Nmap scan history/diffs and topology, structured memory, and vulnerability findings from any browser. Access from anywhere with Cloudflare Tunnel — zero open ports, end-to-end encryption, password-protected API. An optional `observer` credential is restricted server-side to redacted read-only routes. Your data stays on your machine. +Run `cyberstrike web` and control your agents, MCP servers, Bolt connections, always-visible live activity, event-driven mission posture, Nmap scan history/diffs and topology, structured memory, and vulnerability findings from any browser. Access from anywhere with Cloudflare Tunnel — zero open ports, end-to-end encryption, password-protected API. An optional `observer` credential is restricted server-side to redacted read-only routes. Your data stays on your machine. ```bash export CYBERSTRIKE_SERVER_PASSWORD=your-secure-password diff --git a/packages/cyberstrike/src/agent/prompt/methodology/common-methodology.txt b/packages/cyberstrike/src/agent/prompt/methodology/common-methodology.txt index 455b589830..8130a7da34 100644 --- a/packages/cyberstrike/src/agent/prompt/methodology/common-methodology.txt +++ b/packages/cyberstrike/src/agent/prompt/methodology/common-methodology.txt @@ -16,6 +16,17 @@ Log ALL discoveries via `add_intel` immediately upon finding them: Use appropriate severity: critical, high, medium, low, informational. Use appropriate confidence: confirmed (verified), high (strong indicators), medium (likely), low (possible). +Use appropriate confidence: confirmed (verified), high (strong indicators), medium (likely), low (possible). +### Workbench Synchronization + +Treat the UI-backed stores as the engagement system of record, not the final chat response: +- Use `nmap_scan` for active Nmap discovery instead of running `nmap` through `bash`. It enforces approval, stores XML, updates scan history, and projects hosts/services/routes into Topology. +- Record verified target discoveries with `add_intel` as they are found so Mission, coverage, attack paths, and Topology update during execution. +- Record validated vulnerabilities with `report_vulnerability`; do not leave findings only in prose. +- Use `memory_write` for reusable project facts, meaningful engagement outcomes, and failed approaches worth avoiding. Do not store transient chatter or unverified guesses. +- Check `methodology_status` before the final engagement summary so remaining gaps and blockers are visible. + +Do not fabricate records merely to make a panel change. A panel should update only when its backing evidence changes. ### Coverage Tracking diff --git a/packages/cyberstrike/src/event/index.ts b/packages/cyberstrike/src/event/index.ts index ec9785cd17..4dccc8df8b 100644 --- a/packages/cyberstrike/src/event/index.ts +++ b/packages/cyberstrike/src/event/index.ts @@ -1,5 +1,5 @@ import z from "zod" -import { and, desc, eq, lt } from "drizzle-orm" +import { and, desc, eq, lt, or } from "drizzle-orm" import { Bus } from "../bus" import { Database } from "../storage/db" import { Identifier } from "../id/id" @@ -10,6 +10,17 @@ import { Log } from "../util/log" export namespace EngagementEvent { const log = Log.create({ service: "engagement-event" }) const MAX_SEEN = 2_000 + const repeatable = new Set([ + "endpoint_template.updated", + "request.updated", + "vulnerability.updated", + "web_credential.updated", + "web_function.updated", + "web_object.updated", + "web_object_value.updated", + "web_retest.updated", + "web_role.updated", + ]) export const Info = z.object({ id: Identifier.schema("engagement_event"), @@ -73,6 +84,7 @@ export namespace EngagementEvent { const part = record(props.part) const info = record(props.info) const state = record(part?.state) + const status = record(props.status) const tool = record(props.tool) const time = record(state?.time) const list = Array.isArray(props.vulnerabilities) @@ -92,6 +104,9 @@ export namespace EngagementEvent { text(props.callID) ?? text(props.permissionID) ?? text(props.requestID) ?? + text(props.entryID) ?? + text(props.scanID) ?? + text(props.entityID) ?? text(props.id) ?? text(info?.id) const parentID = text(part?.messageID) ?? text(props.messageID) ?? text(info?.parentID) @@ -139,14 +154,20 @@ export namespace EngagementEvent { return { id: text(props.id), + entryID: text(props.entryID), + entityID: text(props.entityID), name: text(props.name), - status: text(props.status), + action: text(props.action), + status: text(props.status) ?? text(status?.type), exitCode: number(props.exitCode), directory: text(props.directory), permission: text(props.permission), tool: text(tool?.tool) ?? text(props.tool), scanID: text(props.scanID), + server: text(props.server) ?? text(props.mcpName), hosts: number(props.hosts), + count: number(props.count), + entryCount: number(props.entryCount), patternCount: Array.isArray(props.patterns) ? props.patterns.length : undefined, } })() @@ -165,12 +186,14 @@ export namespace EngagementEvent { () => { const seen = new Map() const listeners = new Set<(event: Info) => void>() + const closures = new Set<() => void>() const unsub = Bus.subscribeAll((event) => { const next = normalize(event) if (!next) return - const key = `${next.type}:${next.correlationID ?? next.parentID ?? next.sessionID ?? "global"}` + const key = `${next.sessionID ?? "global"}:${next.type}:${next.correlationID ?? next.parentID ?? "global"}` const signature = JSON.stringify(next.data) - if (seen.get(key) === signature) return + const preserve = next.type === "session.idle" || next.type === "session.status" || repeatable.has(next.type) + if (!preserve && seen.get(key) === signature) return seen.delete(key) seen.set(key, signature) while (seen.size > MAX_SEEN) seen.delete(seen.keys().next().value!) @@ -208,9 +231,12 @@ export namespace EngagementEvent { log.error("failed to persist event", { type: next.type, error }) } }) - return { listeners, unsub } + return { listeners, closures, unsub } + }, + async (entry) => { + for (const close of entry.closures) close() + entry.unsub() }, - async (entry) => entry.unsub(), ) export function init() { @@ -223,7 +249,13 @@ export namespace EngagementEvent { return () => current.listeners.delete(listener) } - export function list(input: { sessionID: string; before?: number; limit?: number }) { + export function onDispose(close: () => void) { + const current = state() + current.closures.add(close) + return () => current.closures.delete(close) + } + + export function list(input: { sessionID: string; before?: number; beforeID?: string; limit?: number }) { const limit = Math.max(1, Math.min(input.limit ?? 200, 500)) const rows = Database.use((db) => db @@ -233,10 +265,20 @@ export namespace EngagementEvent { and( eq(EngagementEventTable.project_id, Instance.project.id), eq(EngagementEventTable.session_id, input.sessionID), - input.before ? lt(EngagementEventTable.time_created, input.before) : undefined, + input.before + ? input.beforeID + ? or( + lt(EngagementEventTable.time_created, input.before), + and( + eq(EngagementEventTable.time_created, input.before), + lt(EngagementEventTable.id, input.beforeID), + ), + ) + : lt(EngagementEventTable.time_created, input.before) + : undefined, ), ) - .orderBy(desc(EngagementEventTable.time_created)) + .orderBy(desc(EngagementEventTable.time_created), desc(EngagementEventTable.id)) .limit(limit) .all(), ) diff --git a/packages/cyberstrike/src/server/routes/event-log.ts b/packages/cyberstrike/src/server/routes/event-log.ts index 80b305c28f..07d6510432 100644 --- a/packages/cyberstrike/src/server/routes/event-log.ts +++ b/packages/cyberstrike/src/server/routes/event-log.ts @@ -29,6 +29,7 @@ export const EventLogRoutes = lazy(() => "query", z.object({ before: z.coerce.number().int().positive().optional(), + beforeID: z.string().optional(), limit: z.coerce.number().int().min(1).max(500).optional(), }), ), @@ -62,6 +63,11 @@ export const EventLogRoutes = lazy(() => c.header("X-Accel-Buffering", "no") c.header("Connection", "keep-alive") return streamSSE(c, async (stream) => { + const closed = new Promise((resolve) => stream.onAbort(resolve)) + let off = () => {} + const disposed = new Promise((resolve) => { + off = EngagementEvent.onDispose(resolve) + }) const unsub = EngagementEvent.subscribe((event) => { if (event.sessionID !== sessionID) return void stream.writeSSE({ id: event.id, data: JSON.stringify(event) }) @@ -69,13 +75,14 @@ export const EventLogRoutes = lazy(() => const heartbeat = setInterval(() => { void stream.write(": heartbeat\n\n") }, 30_000) - await new Promise((resolve) => { - stream.onAbort(() => { - clearInterval(heartbeat) - unsub() - resolve() - }) - }) + try { + await stream.write(": connected\n\n") + await Promise.race([closed, disposed]) + } finally { + clearInterval(heartbeat) + off() + unsub() + } }) }, ), diff --git a/packages/cyberstrike/src/tool/bash.ts b/packages/cyberstrike/src/tool/bash.ts index 9f6286be5e..a44228f0f2 100644 --- a/packages/cyberstrike/src/tool/bash.ts +++ b/packages/cyberstrike/src/tool/bash.ts @@ -19,6 +19,8 @@ import { Truncate } from "./truncation" import { Plugin } from "@/plugin" const MAX_METADATA_LENGTH = 30_000 +const NMAP_SAFE_REFERENCES = new Set(["apt", "apt-get", "brew", "echo", "grep", "printf", "rg", "type", "whereis", "which"]) +const NMAP_REFERENCE = /(?:^|[\s"';&|()<>\\/])nmap(?:\.exe)?(?=$|[\s"';&|()<>])/i // Detect binary content in a buffer by checking for high density of // non-printable bytes. Printable = ASCII 0x20-0x7E, tab, newline, CR, ESC @@ -37,6 +39,14 @@ function isBinaryBuffer(buf: Buffer): boolean { return nonPrintable / buf.length > 0.3 } const DEFAULT_TIMEOUT = Flag.CYBERSTRIKE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000 +const executable = (value: string) => + value + .replace(/^['"]|['"]$/g, "") + .trim() + .split(/[\\/]/) + .at(-1)! + .toLowerCase() + .replace(/\.exe$/, "") export const log = Log.create({ service: "bash-tool" }) @@ -102,6 +112,28 @@ export const BashTool = Tool.define("bash", async () => { if (!tree) { throw new Error("Failed to parse command") } + const invokesNmap = (root: typeof tree): boolean => { + for (const node of root.rootNode.descendantsOfType("command")) { + if (!node) continue + const command = Array.from({ length: node.childCount }, (_, index) => node.child(index)) + .filter( + (child) => + !!child && + ["command_name", "word", "string", "raw_string", "concatenation"].includes(child.type), + ) + .map((child) => child!.text) + const name = executable(command[0] ?? "") + if (name === "nmap") return true + if (NMAP_SAFE_REFERENCES.has(name)) continue + if (NMAP_REFERENCE.test(node.text)) return true + } + return false + } + if (invokesNmap(tree)) { + throw new Error( + "Raw Nmap execution is disabled. Use nmap_scan so approval, XML evidence, scan history, and topology stay synchronized.", + ) + } const directories = new Set() if (!Instance.containsPath(cwd)) directories.add(cwd) const patterns = new Set() diff --git a/packages/cyberstrike/test/agent/agent.test.ts b/packages/cyberstrike/test/agent/agent.test.ts index eda3555318..abaff12ef3 100644 --- a/packages/cyberstrike/test/agent/agent.test.ts +++ b/packages/cyberstrike/test/agent/agent.test.ts @@ -39,6 +39,7 @@ test("cyberstrike agent has correct default properties", async () => { expect(cs?.native).toBe(true) expect(evalPerm(cs, "edit")).toBe("allow") expect(evalPerm(cs, "bash")).toBe("allow") + expect(PermissionNext.evaluate("nmap_scan", "*", cs!.permission).action).toBe("ask") }, }) }) diff --git a/packages/cyberstrike/test/event/event.test.ts b/packages/cyberstrike/test/event/event.test.ts index e8fb85310c..aa6800b4ad 100644 --- a/packages/cyberstrike/test/event/event.test.ts +++ b/packages/cyberstrike/test/event/event.test.ts @@ -5,6 +5,7 @@ import { Bus } from "../../src/bus" import { BusEvent } from "../../src/bus/bus-event" import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" +import { Server } from "../../src/server/server" describe("engagement event normalization", () => { test("records tool lifecycle without arguments or output", () => { @@ -107,6 +108,36 @@ describe("engagement event normalization", () => { ).toBe("tool") }) + test("summarizes structured session status", () => { + expect( + EngagementEvent.normalize({ + type: "session.status", + properties: { sessionID: "ses_test", status: { type: "busy" } }, + })?.data, + ).toEqual({ status: "busy" }) + }) + + test("preserves counters and identifiers used by live panels", () => { + expect( + EngagementEvent.normalize({ + type: "intel.updated", + properties: { sessionID: "ses_test", entryCount: 2 }, + }), + ).toMatchObject({ + correlationID: undefined, + data: { entryCount: 2 }, + }) + expect( + EngagementEvent.normalize({ + type: "memory.updated", + properties: { sessionID: "ses_test", entryID: "mem_test", action: "created" }, + }), + ).toMatchObject({ + correlationID: "mem_test", + data: { entryID: "mem_test", action: "created" }, + }) + }) + test("persists and lists session events", async () => { await using tmp = await tmpdir() const sessionID = `ses_event_${Date.now()}` @@ -137,4 +168,147 @@ describe("engagement event normalization", () => { }, }) }) + + test("preserves repeated session lifecycle events", async () => { + await using tmp = await tmpdir() + const sessionID = `ses_lifecycle_${Date.now()}` + const event = BusEvent.define("session.idle", z.object({ sessionID: z.string() })) + + await Instance.provide({ + directory: tmp.path, + init: () => { + EngagementEvent.init() + return Promise.resolve() + }, + fn: async () => { + await Bus.publish(event, { sessionID }) + await Bus.publish(event, { sessionID }) + expect(EngagementEvent.list({ sessionID })).toHaveLength(2) + await Instance.dispose() + }, + }) + }) + + test("deduplicates correlated events within each session", async () => { + await using tmp = await tmpdir() + const event = BusEvent.define( + "dossier.note.updated", + z.object({ sessionID: z.string(), entityID: z.string(), count: z.number() }), + ) + + await Instance.provide({ + directory: tmp.path, + init: () => { + EngagementEvent.init() + return Promise.resolve() + }, + fn: async () => { + await Bus.publish(event, { sessionID: "ses_one", entityID: "host:example", count: 1 }) + await Bus.publish(event, { sessionID: "ses_two", entityID: "host:example", count: 1 }) + expect(EngagementEvent.list({ sessionID: "ses_one" })).toHaveLength(1) + expect(EngagementEvent.list({ sessionID: "ses_two" })).toHaveLength(1) + await Instance.dispose() + }, + }) + }) + + test("pages events sharing the same timestamp without gaps", async () => { + await using tmp = await tmpdir() + const sessionID = `ses_cursor_${Date.now()}` + const event = BusEvent.define("test.cursor", z.object({ sessionID: z.string(), id: z.string() })) + const original = Date.now + Date.now = () => 1_788_000_000_000 + + try { + await Instance.provide({ + directory: tmp.path, + init: () => { + EngagementEvent.init() + return Promise.resolve() + }, + fn: async () => { + await Bus.publish(event, { sessionID, id: "one" }) + await Bus.publish(event, { sessionID, id: "two" }) + await Bus.publish(event, { sessionID, id: "three" }) + const latest = EngagementEvent.list({ sessionID, limit: 1 }) + const older = EngagementEvent.list({ + sessionID, + before: latest[0]!.time, + beforeID: latest[0]!.id, + limit: 2, + }) + expect(new Set([...older, ...latest].map((item) => item.correlationID))).toEqual( + new Set(["one", "two", "three"]), + ) + await Instance.dispose() + }, + }) + } finally { + Date.now = original + } + }) + + test("preserves lossy aggregate updates", async () => { + await using tmp = await tmpdir() + const sessionID = `ses_aggregate_${Date.now()}` + const event = BusEvent.define( + "vulnerability.updated", + z.object({ + sessionID: z.string(), + vulnerabilities: z.array(z.object({ id: z.string(), severity: z.string() })), + }), + ) + + await Instance.provide({ + directory: tmp.path, + init: () => { + EngagementEvent.init() + return Promise.resolve() + }, + fn: async () => { + await Bus.publish(event, { sessionID, vulnerabilities: [{ id: "vul_test", severity: "low" }] }) + await Bus.publish(event, { sessionID, vulnerabilities: [{ id: "vul_test", severity: "critical" }] }) + expect(EngagementEvent.list({ sessionID })).toHaveLength(2) + await Instance.dispose() + }, + }) + }) + + test("signals that the live event stream is connected", async () => { + await using tmp = await tmpdir() + const abort = new AbortController() + const response = await Server.App().request( + `/event-log/session/ses_stream/stream?directory=${encodeURIComponent(tmp.path)}`, + { signal: abort.signal }, + ) + expect(response.status).toBe(200) + const reader = response.body!.getReader() + const first = await Promise.race([ + reader.read(), + Bun.sleep(1_000).then(() => { + throw new Error("event stream connection signal timed out") + }), + ]) + expect(new TextDecoder().decode(first.value)).toContain(": connected") + abort.abort() + await reader.cancel() + await Instance.disposeAll() + }) + + test("closes the live event stream when its instance is disposed", async () => { + await using tmp = await tmpdir() + const response = await Server.App().request( + `/event-log/session/ses_stream/stream?directory=${encodeURIComponent(tmp.path)}`, + ) + const reader = response.body!.getReader() + await reader.read() + await Instance.provide({ directory: tmp.path, fn: () => Instance.dispose() }) + const result = await Promise.race([ + reader.read(), + Bun.sleep(1_000).then(() => { + throw new Error("event stream did not close during instance disposal") + }), + ]) + expect(result.done).toBe(true) + }) }) diff --git a/packages/cyberstrike/test/tool/bash.test.ts b/packages/cyberstrike/test/tool/bash.test.ts index fd03b7f980..19a8f80603 100644 --- a/packages/cyberstrike/test/tool/bash.test.ts +++ b/packages/cyberstrike/test/tool/bash.test.ts @@ -40,6 +40,77 @@ describe("tool.bash", () => { }) describe("tool.bash permissions", () => { + test("routes direct and wrapped Nmap commands to nmap_scan", async () => { + await using tmp = await tmpdir({ git: true }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const bash = await BashTool.init() + const testCtx = { + ...ctx, + ask: async () => { + throw new Error("Bash permission reached") + }, + } + for (const command of [ + "nmap -sV example.test", + "/usr/bin/nmap -sV example.test", + "sudo -n /usr/bin/nmap -sV example.test", + "env NMAP_PRIVILEGED=1 nmap -sV example.test", + "bash -c \"nmap -sV example.test\"", + "sh -c \"echo ready; /usr/bin/nmap -sV example.test\"", + "bash -c \"sudo -n /usr/bin/nmap -sV example.test\"", + "bash -c \"env MODE=test /usr/bin/nmap -sV example.test\"", + "stdbuf -oL nmap -sV example.test", + "nmap "resolved") + .catch((cause) => (cause instanceof Error ? cause.message : String(cause))) + if (!result.includes("Use nmap_scan")) throw new Error(`Nmap routing bypass for ${command}: ${result}`) + } + }, + }) + }) + + test("allows commands that only reference Nmap as data", async () => { + await using tmp = await tmpdir({ git: true }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const bash = await BashTool.init() + const testCtx = { + ...ctx, + ask: async () => { + throw new Error("permission reached") + }, + } + for (const command of ["echo nmap", "grep nmap README.md", "apt-get install nmap"]) { + await expect( + bash.execute( + { + command, + description: "Reference Nmap command", + }, + testCtx, + ), + ).rejects.toThrow("permission reached") + } + }, + }) + }) + test("asks for bash permission with correct pattern", async () => { await using tmp = await tmpdir({ git: true }) await Instance.provide({ diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 14c2718329..043f786c79 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -4376,6 +4376,7 @@ export class EventLog extends HeyApiClient { sessionID: string directory?: string before?: number + beforeID?: string limit?: number }, options?: Options, @@ -4388,6 +4389,7 @@ export class EventLog extends HeyApiClient { { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "before" }, + { in: "query", key: "beforeID" }, { in: "query", key: "limit" }, ], }, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index c354738a1a..c756d584f3 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -6758,6 +6758,7 @@ export type EventLogListData = { query?: { directory?: string before?: number + beforeID?: string limit?: number } url: "/event-log/session/{sessionID}" From 270eab33a571b3c97f472d7b7600e5450c1f527c Mon Sep 17 00:00:00 2001 From: spetro511 Date: Thu, 3 Sep 2026 00:35:28 -0400 Subject: [PATCH 2/4] test(topology): cover multi-host scans Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- packages/cyberstrike/src/topology/index.ts | 94 +++++++++---------- .../test/topology/topology.test.ts | 42 +++++++++ 2 files changed, 89 insertions(+), 47 deletions(-) diff --git a/packages/cyberstrike/src/topology/index.ts b/packages/cyberstrike/src/topology/index.ts index 5f582382d9..ce4ce209d9 100644 --- a/packages/cyberstrike/src/topology/index.ts +++ b/packages/cyberstrike/src/topology/index.ts @@ -186,59 +186,59 @@ export namespace Topology { for (const observation of latest.values()) { const scan = observation.scan const host = observation.host - const current = node({ - id: id("host", host.id), + const current = node({ + id: id("host", host.id), + kind: "host", + label: host.hostnames[0] ?? host.id, + source: "nmap", + status: host.status, + confidence: host.os[0] ? `${host.os[0].accuracy}%` : undefined, + data: { + addresses: host.addresses, + os: host.os, + scanID: scan.id, + scanName: scan.name, + scannedAt: scan.time, + }, + }) + for (const port of host.ports) { + const service = node({ + id: id("service", `${host.id}:${port.protocol}:${port.port}`), + kind: "service", + label: `${port.port}/${port.protocol} ${port.service.name ?? port.service.product ?? "unknown"}`, + source: "nmap", + status: port.state, + data: { + host: host.id, + port: port.port, + protocol: port.protocol, + service: port.service, + scripts: port.scripts, + scanID: scan.id, + }, + }) + edge(current, service, "exposes") + } + let prior: string | undefined + const target = new Set(host.addresses.map((address) => address.address)) + for (const hop of host.trace.toSorted((a, b) => a.ttl - b.ttl)) { + if (target.has(hop.address)) continue + const hopNode = node({ + id: id("host", hop.address), kind: "host", - label: host.hostnames[0] ?? host.id, + label: hop.host ?? hop.address, source: "nmap", - status: host.status, - confidence: host.os[0] ? `${host.os[0].accuracy}%` : undefined, data: { - addresses: host.addresses, - os: host.os, + address: hop.address, + ttl: hop.ttl, + rtt: hop.rtt, scanID: scan.id, - scanName: scan.name, - scannedAt: scan.time, }, }) - for (const port of host.ports) { - const service = node({ - id: id("service", `${host.id}:${port.protocol}:${port.port}`), - kind: "service", - label: `${port.port}/${port.protocol} ${port.service.name ?? port.service.product ?? "unknown"}`, - source: "nmap", - status: port.state, - data: { - host: host.id, - port: port.port, - protocol: port.protocol, - service: port.service, - scripts: port.scripts, - scanID: scan.id, - }, - }) - edge(current, service, "exposes") - } - let prior: string | undefined - const target = new Set(host.addresses.map((address) => address.address)) - for (const hop of host.trace.toSorted((a, b) => a.ttl - b.ttl)) { - if (target.has(hop.address)) continue - const hopNode = node({ - id: id("host", hop.address), - kind: "host", - label: hop.host ?? hop.address, - source: "nmap", - data: { - address: hop.address, - ttl: hop.ttl, - rtt: hop.rtt, - scanID: scan.id, - }, - }) - if (prior) edge(prior, hopNode, "routes_to") - prior = hopNode - } - if (prior) edge(prior, current, "routes_to") + if (prior) edge(prior, hopNode, "routes_to") + prior = hopNode + } + if (prior) edge(prior, current, "routes_to") } return { diff --git a/packages/cyberstrike/test/topology/topology.test.ts b/packages/cyberstrike/test/topology/topology.test.ts index 5ec9a6c3e9..b948f0fda4 100644 --- a/packages/cyberstrike/test/topology/topology.test.ts +++ b/packages/cyberstrike/test/topology/topology.test.ts @@ -212,6 +212,48 @@ describe("topology projection", () => { expect(graph.nodes.some((node) => node.label === "443/tcp https")).toBe(true) }) + test("projects every host in a multi-host Nmap scan", () => { + const host = (address: string, port: number) => ({ + id: address, + status: "up", + addresses: [{ address, type: "ipv4" }], + hostnames: [`host-${port}.example.test`], + ports: [ + { + protocol: "tcp", + port, + state: "open", + service: { name: port === 22 ? "ssh" : "https", cpe: [] }, + scripts: [], + }, + ], + os: [], + trace: [], + }) + const graph = Topology.project({ + sessionID: "ses_test", + intel: [], + requests: [], + vulnerabilities: [], + scans: [ + { + id: "nms_multi", + sessionID: "ses_test", + name: "Multi-host", + source: "nmap_scan", + xmlHash: "multi", + time: 1, + summary: { scanner: "nmap", up: 2, down: 0, total: 2 }, + hosts: [host("192.0.2.10", 22), host("192.0.2.11", 443)], + }, + ], + }) + + expect(graph.nodes.filter((node) => node.kind === "host")).toHaveLength(2) + expect(graph.nodes.some((node) => node.label === "22/tcp ssh")).toBe(true) + expect(graph.nodes.some((node) => node.label === "443/tcp https")).toBe(true) + }) + test("does not create traceroute self-loops or overwrite the host label", () => { const graph = Topology.project({ sessionID: "ses_test", From 70455b08e2f6a0788ac2fa4b5868423058a57ac8 Mon Sep 17 00:00:00 2001 From: spetro511 Date: Thu, 3 Sep 2026 02:12:01 -0400 Subject: [PATCH 3/4] fix(nmap): require managed topology scans Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 1 + README.md | 228 +++++++----------- .../src/pages/session/session-prompt-dock.tsx | 25 +- .../app/src/pages/session/topology-panel.tsx | 16 +- packages/cyberstrike/README.md | 35 +-- packages/cyberstrike/src/tool/nmap-scan.ts | 52 +++- .../cyberstrike/test/tool/nmap-scan.test.ts | 34 +++ 7 files changed, 214 insertions(+), 177 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6551ad16e..ed22c3a951 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Format based on [Keep a Changelog](https://keepachangelog.com/), versions follow - Release and local-binary installs now deploy the bundled HackBrowser worker to the runtime data directory - Web update checks now honor disabled auto-updates, avoiding false upgrade prompts for managed source builds - Hidden workbench panels no longer miss live changes or depend on visible-only polling; repeated session lifecycle events and reconnect gaps are recovered +- Nmap permissions now identify active/elevated execution, privileged profiles use non-interactive sudo when required, and Topology explains why generic shell output is not graphable ## [1.1.16] — 2026-08-08 diff --git a/README.md b/README.md index d6e4e5e320..1843a33519 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ That's it. CyberStrike launches a TUI in your terminal, asks for your LLM provid > **Already have a Claude Code or OpenAI subscription?** CyberStrike's intelligence layer sits on top of your existing AI subscription. No separate API costs — your current plan powers an entire pentest toolkit. -Explore the full documentation at [**docs.cyberstrike.io**](https://docs.cyberstrike.io) or visit [**cyberstrike.io**](https://cyberstrike.io) for demos and guides. +Explore the full documentation at **[docs.cyberstrike.io](https://docs.cyberstrike.io)** or visit **[cyberstrike.io](https://cyberstrike.io)** for demos and guides. --- @@ -150,31 +150,31 @@ CyberStrike isn't just a wrapper around an LLM. It's an intelligence layer that CyberStrike integrates with the entire AI ecosystem through 23 bundled SDK providers and 150+ providers via the [models.dev](https://models.dev) catalog. Here are the core integrations: -| Provider | Models | Notes | -| --- | --- | --- | -| **Anthropic** | Claude 4.5, Claude 4 | Best performance with extended thinking | -| **OpenAI** | GPT-5, GPT-4.1, o3, o4 | Full tool-use + reasoning support | -| **Google** | Gemini 2.5 Pro/Flash | Long context for large codebases | -| **Amazon Bedrock** | All Bedrock models | IAM auth, no API keys needed | -| **Azure OpenAI** | All Azure-hosted models | Enterprise deployments | -| **Google Vertex AI** | Gemini + Claude on GCP | Regional endpoints (EU/US) | -| **GitHub Copilot** | GPT-5, Claude, Gemini | Use your existing Copilot subscription | -| **xAI** | Grok 3, Grok 3 Mini | Real-time data access | -| **Groq** | LLaMA, Mixtral | Ultra-fast inference | -| **Mistral** | Mistral Large, Codestral | European data residency | -| **DeepSeek** | DeepSeek V3, R1 | Cost-effective alternative | -| **Cerebras** | LLaMA on Cerebras | Fastest inference available | -| **Cohere** | Command R+ | RAG-optimized models | -| **OpenRouter** | 300+ models | Single API, any model | -| **Together AI** | Open-source models | Fine-tuning support | -| **DeepInfra** | Open-source models | Pay-per-token, no GPU needed | -| **Perplexity** | Sonar models | Search-augmented generation | -| **Alibaba Cloud** | Qwen, Kimi, DashScope | Chinese model ecosystem | -| **Cloudflare AI Gateway** | Any provider via gateway | Caching, rate limiting, analytics | -| **Ollama** | Any GGUF model | Fully offline, local-only | -| **LM Studio** | Any local model | Desktop GUI + API server | -| **vLLM** | Any HuggingFace model | Self-hosted, GPU-optimized | -| **Any OpenAI-compatible** | — | Custom endpoints welcome | +| Provider | Models | Notes | +| ------------------------- | ------------------------ | --------------------------------------- | +| **Anthropic** | Claude 4.5, Claude 4 | Best performance with extended thinking | +| **OpenAI** | GPT-5, GPT-4.1, o3, o4 | Full tool-use + reasoning support | +| **Google** | Gemini 2.5 Pro/Flash | Long context for large codebases | +| **Amazon Bedrock** | All Bedrock models | IAM auth, no API keys needed | +| **Azure OpenAI** | All Azure-hosted models | Enterprise deployments | +| **Google Vertex AI** | Gemini + Claude on GCP | Regional endpoints (EU/US) | +| **GitHub Copilot** | GPT-5, Claude, Gemini | Use your existing Copilot subscription | +| **xAI** | Grok 3, Grok 3 Mini | Real-time data access | +| **Groq** | LLaMA, Mixtral | Ultra-fast inference | +| **Mistral** | Mistral Large, Codestral | European data residency | +| **DeepSeek** | DeepSeek V3, R1 | Cost-effective alternative | +| **Cerebras** | LLaMA on Cerebras | Fastest inference available | +| **Cohere** | Command R+ | RAG-optimized models | +| **OpenRouter** | 300+ models | Single API, any model | +| **Together AI** | Open-source models | Fine-tuning support | +| **DeepInfra** | Open-source models | Pay-per-token, no GPU needed | +| **Perplexity** | Sonar models | Search-augmented generation | +| **Alibaba Cloud** | Qwen, Kimi, DashScope | Chinese model ecosystem | +| **Cloudflare AI Gateway** | Any provider via gateway | Caching, rate limiting, analytics | +| **Ollama** | Any GGUF model | Fully offline, local-only | +| **LM Studio** | Any local model | Desktop GUI + API server | +| **vLLM** | Any HuggingFace model | Self-hosted, GPU-optimized | +| **Any OpenAI-compatible** | — | Custom endpoints welcome | > **Air-gapped environments?** Run CyberStrike entirely offline with Ollama or LM Studio. No data leaves your machine — ever. @@ -223,26 +223,26 @@ Your security tools don't have to run on your laptop. Deploy Bolt on one or many Switch between agents with `Tab`. Each one is a domain specialist. -| Agent | Focus | What It Does | -| --- | --- | --- | -| **cyberstrike** | General | Full-access primary agent — reconnaissance, exploitation, reporting | -| **web-application** | Web | OWASP Top 10, WSTG methodology, API security, session testing | -| **mobile-application** | Mobile | Android/iOS, Frida/Objection, MASTG/MASVS compliance | -| **cloud-security** | Cloud | AWS, Azure, GCP — IAM misconfigs, CIS benchmarks, exposed resources | -| **internal-network** | Network | Active Directory, Kerberos attacks, lateral movement, pivoting | +| Agent | Focus | What It Does | +| ---------------------- | ------- | ------------------------------------------------------------------- | +| **cyberstrike** | General | Full-access primary agent — reconnaissance, exploitation, reporting | +| **web-application** | Web | OWASP Top 10, WSTG methodology, API security, session testing | +| **mobile-application** | Mobile | Android/iOS, Frida/Objection, MASTG/MASVS compliance | +| **cloud-security** | Cloud | AWS, Azure, GCP — IAM misconfigs, CIS benchmarks, exposed resources | +| **internal-network** | Network | Active Directory, Kerberos attacks, lateral movement, pivoting | Plus **8 specialized proxy testers** that run automatically on intercepted traffic: -| Tester | What It Tests | -| --- | --- | -| **IDOR** | Object-level access control — can user A reach user B's resources? | +| Tester | What It Tests | +| ------------------------ | ---------------------------------------------------------------------------- | +| **IDOR** | Object-level access control — can user A reach user B's resources? | | **Authorization Bypass** | Vertical privilege escalation — can low-privilege users hit admin endpoints? | -| **Mass Assignment** | Unexpected writable fields — role, price, balance, userId in request bodies | -| **Injection** | SQL, command, LDAP, template injection across all input vectors | -| **Authentication** | Token validation, session fixation, credential exposure | -| **Business Logic** | Price manipulation, coupon reuse, race conditions, workflow bypass | -| **SSRF** | Internal host access via user-controlled URLs or redirect parameters | -| **File Attacks** | Path traversal, unrestricted upload, dangerous file types | +| **Mass Assignment** | Unexpected writable fields — role, price, balance, userId in request bodies | +| **Injection** | SQL, command, LDAP, template injection across all input vectors | +| **Authentication** | Token validation, session fixation, credential exposure | +| **Business Logic** | Price manipulation, coupon reuse, race conditions, workflow bypass | +| **SSRF** | Internal host access via user-controlled URLs or redirect parameters | +| **File Attacks** | Path traversal, unrestricted upload, dangerous file types | Each tester uses a **3-gate confirmation protocol**: execute a baseline request, execute the attack, compare responses. A finding is only reported when there is a measurable, reproducible difference — not on speculation. Duplicate findings (same endpoint + attack vector) are automatically suppressed across the session. @@ -254,12 +254,12 @@ CyberStrike ships with **7,600+ security skill files** — structured, Ed25519-s **Skill categories:** -| Category | Skills | What They Cover | -| --- | --- | --- | -| **Attack Methodologies** | 19 | JWT attacks, SSRF, SSTI, race conditions, request smuggling, cache poisoning, CORS, GraphQL, prototype pollution, XXE, WebSocket, subdomain takeover, host header injection, open redirect | -| **Post-Exploitation** | 5 | AWS, Azure, Kubernetes, Windows, macOS privilege escalation and persistence | -| **Compliance Frameworks** | 3 | CIS Benchmarks (AWS/Azure/GCP/K8s), NIST Framework, MITRE ATT&CK (Enterprise, Mobile, ICS) | -| **Domain Knowledge** | 8+ | Active Directory security, web security patterns, recon methodology, CI/CD attacks, Kerberos attacks, eBPF techniques | +| Category | Skills | What They Cover | +| ------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **Attack Methodologies** | 19 | JWT attacks, SSRF, SSTI, race conditions, request smuggling, cache poisoning, CORS, GraphQL, prototype pollution, XXE, WebSocket, subdomain takeover, host header injection, open redirect | +| **Post-Exploitation** | 5 | AWS, Azure, Kubernetes, Windows, macOS privilege escalation and persistence | +| **Compliance Frameworks** | 3 | CIS Benchmarks (AWS/Azure/GCP/K8s), NIST Framework, MITRE ATT&CK (Enterprise, Mobile, ICS) | +| **Domain Knowledge** | 8+ | Active Directory security, web security patterns, recon methodology, CI/CD attacks, Kerberos attacks, eBPF techniques | Each skill includes testing procedures, payloads, tool commands, and CWE mappings. Skills are tagged with OWASP WSTG IDs, CIS control IDs, and chain relationships — so agents know which skills to combine for multi-step attack chains. @@ -267,7 +267,7 @@ Each skill includes testing procedures, payloads, tool commands, and CWE mapping ### HackBrowser -> Full documentation: [**docs.cyberstrike.io/docs/tools/hacker-browser**](https://docs.cyberstrike.io/docs/tools/hacker-browser/) +> Full documentation: **[docs.cyberstrike.io/docs/tools/hacker-browser](https://docs.cyberstrike.io/docs/tools/hacker-browser/)** HackBrowser is CyberStrike's built-in Chromium browser. Start it from the TUI with `/hackbrowser`. As you browse, every HTTP request is captured and routed through the proxy-agent pipeline — no manual export, no Burp project files. @@ -305,38 +305,29 @@ Browser ──HTTPS──▶ Cloudflare Tunnel ──encrypted──▶ cloudfla ```bash export CYBERSTRIKE_SERVER_PASSWORD=your-secure-password -# Optional API/viewer credential with a strict read-only route allowlist: -export CYBERSTRIKE_OBSERVER_PASSWORD=your-observer-password cyberstrike web # In another terminal: cloudflared tunnel --url http://localhost:4096 run your-tunnel ``` -If user or project configuration prevents startup, run `cyberstrike web --safe` to start recovery mode without those config sources. Managed administrator policy is still enforced. - **Why this is secure:** - **Zero open ports** — CyberStrike binds to `localhost:4096`. `cloudflared` makes an outbound-only connection to Cloudflare's edge. No firewall rules, no port forwarding needed. - **End-to-end encryption** — Browser to Cloudflare edge is TLS. Cloudflare edge to your machine is an encrypted tunnel. No plaintext leaves your network. - **Password-protected API** — Every API request requires Basic Auth. Local requests on `localhost` bypass auth for convenience; remote requests via CF tunnel always require credentials (detects `X-Forwarded-For` / `CF-Connecting-IP`). -- **Read-only observers** — The optional `observer` account can read redacted activity, mission posture, topology, findings, and status, but cannot access configuration, secrets, raw events, PTYs, WebSockets, or mutation routes. - **Your data stays local** — LLM inference runs on your hardware. CyberStrike processes everything locally. The tunnel is just a secure pipe. **What's in the Web UI:** -| Tab | What It Does | -| --- | --- | -| **Chat** | Full conversation with all 13+ security agents | -| **MCP** | Live MCP server status, health, and tool counts | -| **Bolt** | Bolt remote server connection monitoring | -| **Vulnerabilities** | Discovered vulns with severity, PoC, and impact | -| **Web Context** | Endpoints, roles, credentials, and functions discovered during active sessions | -| **Mission** | Methodology phases, coverage, blockers, attack chains, agents, and safe CTAs | -| **Topology** | Evidence-linked assets, Nmap hosts/services/routes, scan history/diffs, endpoints, identities, and findings | -| **Activity** | Always-visible live status plus durable Agent/Tool/MCP/Bolt/Browser/PTY lanes, filtering, reconnect recovery, and JSONL export | -| **Memory** | Trust-ranked structured memory, FTS search, redaction, notes, and invalidation | - -[**app.cyberstrike.io**](https://app.cyberstrike.io) is a hosted static page (no backend, no data storage) for convenience. Or self-host: clone the repo and serve `packages/app/dist/` from your own domain. +| Tab | What It Does | +| ------------------- | ------------------------------------------------------------------------------ | +| **Chat** | Full conversation with all 13+ security agents | +| **MCP** | Live MCP server status, health, and tool counts | +| **Bolt** | Bolt remote server connection monitoring | +| **Vulnerabilities** | Discovered vulns with severity, PoC, and impact | +| **Web Context** | Endpoints, roles, credentials, and functions discovered during active sessions | + +**[app.cyberstrike.io](https://app.cyberstrike.io)** is a hosted static page (no backend, no data storage) for convenience. Or self-host: clone the repo and serve `packages/app/dist/` from your own domain. --- @@ -371,23 +362,16 @@ Bolt is CyberStrike's remote tool server. Deploy it on any VPS, cloud instance, ### MCP Ecosystem -CyberStrike includes a curated MCP catalog with roughly **724 direct/composite security tools** across 11 default entries: +CyberStrike connects to specialized MCP servers that extend its capabilities — **176+ security tools** across 5 domains: -| Server | Tools | What It Adds | -| --- | --- | --- | -| [github-security-mcp](https://github.com/badchars/github-security-mcp) | 39 | GitHub org, repo, Actions, secrets, supply chain, and access posture | -| [cve-mcp](https://github.com/badchars/cve-mcp) | 41 | CVE intelligence across 11 vulnerability and exploitability sources | -| [osint-mcp-server](https://github.com/badchars/osint-mcp-server) | 37 | Shodan, VirusTotal, Censys, DNS, WHOIS, certificates, BGP, and archives | -| [cloud-audit-mcp](https://github.com/badchars/cloud-audit-mcp) | 38 | AWS, Azure, and GCP security audits with 60+ checks | -| [hackbrowser-mcp](https://github.com/badchars/hackbrowser-mcp) | 39 | Firefox security browser, isolated roles, traffic replay, active tests | -| [darknet-mcp-server](https://github.com/badchars/darknet-mcp-server) | 66 | Breach, ransomware, Tor, malware, blockchain, and exploit intelligence | -| [dns-security-mcp](https://github.com/badchars/dns-security-mcp) | 103 | DNSSEC, email, hijacking, tunneling, typosquatting, and certificates | -| [supply-chain-mcp-server](https://github.com/badchars/supply-chain-mcp-server) | 7/90 | 7 composite tools orchestrating 90 package and provenance techniques | -| [mcp-security-scanner](https://github.com/badchars/mcp-security-scanner) | 55 | Runtime, source, config, dependency, and OWASP MCP security analysis | -| [steganography-mcp](https://github.com/badchars/steganography-mcp) | 128 | Offline image, audio, video, document, and covert-channel analysis | -| [satellite-mcp](https://github.com/badchars/satellite-mcp) | 171 | Satellite, aviation, maritime, conflict, infrastructure, and GEOINT | +| Server | Tools | What It Adds | +| ---------------------------------------------------------------------- | ----- | -------------------------------------------------------------------- | +| [cloud-audit-mcp](https://github.com/badchars/cloud-audit-mcp) | 38 | Cloud security audits — 60+ checks across AWS, Azure, GCP | +| [github-security-mcp](https://github.com/badchars/github-security-mcp) | 39 | GitHub security posture — repo, org, actions, secrets, supply chain | +| [cve-mcp](https://github.com/badchars/cve-mcp) | 23 | CVE intelligence — NVD, EPSS, CISA KEV, GitHub Advisory, OSV | +| [osint-mcp](https://github.com/badchars/osint-mcp) | 37 | OSINT recon — Shodan, VirusTotal, SecurityTrails, Censys, DNS, WHOIS | -Runnable npm entries are version-pinned. `cloud-audit-mcp` and `hackbrowser-mcp` currently require manual installation from their repositories. The catalog also offers optional wireless-security, LOLBin, and fingerprinting servers. +All open source. All installable with `npx`. Plug them into CyberStrike or use them standalone with any MCP-compatible client. --- @@ -395,16 +379,16 @@ Runnable npm entries are version-pinned. `cloud-audit-mcp` and `hackbrowser-mcp` CyberStrike agents have direct access to **56+ tools** without any external dependencies: -| Category | Tools | -| --- | --- | -| **Execution** | Shell, typed host readiness, file read/write/edit/patch, directory listing, batch operations | -| **Discovery** | Web fetch, web search, code search, glob, grep, intel gathering | -| **Offensive** | Approval-gated Nmap profiles, HackBrowser, attack scripts, vulnerability reporting & triage | -| **Post-Exploitation** | AWS hook, Azure hook, Kubernetes hook, Windows hook, macOS hook, CI/CD pipe, eBPF | -| **Web Context** | Session context, endpoint/role/credential/function discovery and management | -| **Proxy** | HTTP/HTTPS interception, request replay, session context sharing across sub-testers | -| **Reporting** | Professional report generation, coverage notes, methodology tracking, VRT checks | -| **Integration** | MCP servers, Bolt remote tools, custom plugins, LSP | +| Category | Tools | +| --------------------- | ----------------------------------------------------------------------------------- | +| **Execution** | Shell (bash), file read/write/edit/patch, directory listing, batch operations | +| **Discovery** | Web fetch, web search, code search, glob, grep, intel gathering | +| **Offensive** | HackBrowser, attack script execution, vulnerability reporting & triage | +| **Post-Exploitation** | AWS hook, Azure hook, Kubernetes hook, Windows hook, macOS hook, CI/CD pipe, eBPF | +| **Web Context** | Session context, endpoint/role/credential/function discovery and management | +| **Proxy** | HTTP/HTTPS interception, request replay, session context sharing across sub-testers | +| **Reporting** | Professional report generation, coverage notes, methodology tracking, VRT checks | +| **Integration** | MCP servers, Bolt remote tools, custom plugins, LSP | Plus a **plugin SDK** with 15+ hook types (tool interception, message transformation, permission prompts, shell environment) — build your own agents and tools, register them at runtime. @@ -414,15 +398,15 @@ Plus a **plugin SDK** with 15+ hook types (tool interception, message transforma CyberStrike includes built-in post-exploitation capabilities across multiple platforms — no external tools required. -| Platform | Capabilities | -| --- | --- | -| **macOS** | Chrome credential extraction, Keychain dumping, keylogging, TCC bypass, GateKeeper bypass, XProtect checks, SSH key extraction, DTrace system tracing | -| **Windows** | Post-exploitation hooks for privilege escalation and persistence | +| Platform | Capabilities | +| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **macOS** | Chrome credential extraction, Keychain dumping, keylogging, TCC bypass, GateKeeper bypass, XProtect checks, SSH key extraction, DTrace system tracing | +| **Windows** | Post-exploitation hooks for privilege escalation and persistence | | **Linux/eBPF** | 29 kernel-level scripts — process execution monitoring, SSL/TLS sniffing, keystroke logging, namespace manipulation detection, rootkit detection, process/file/connection hiding | -| **AWS** | IAM enumeration, S3 exposure, Lambda backdoors, CloudTrail evasion | -| **Azure** | Identity enumeration, storage exposure, function exploitation | -| **Kubernetes** | Pod escape, service account abuse, secret extraction, RBAC exploitation | -| **CI/CD** | Pipeline injection, secret extraction, build artifact manipulation | +| **AWS** | IAM enumeration, S3 exposure, Lambda backdoors, CloudTrail evasion | +| **Azure** | Identity enumeration, storage exposure, function exploitation | +| **Kubernetes** | Pod escape, service account abuse, secret extraction, RBAC exploitation | +| **CI/CD** | Pipeline injection, secret extraction, build artifact manipulation | All post-exploitation tools are agent-driven — they execute based on context and findings, not as fixed scripts. @@ -447,36 +431,6 @@ scoop install cyberstrike curl -fsSL https://cyberstrike.io/install.sh | bash ``` -#### Build and deploy on Kali/Linux from source - -Source deployments require the compiled binary, the matching HackBrowser worker, and the web bundle. Use the repository-pinned Bun version: - -```bash -bun install --frozen-lockfile -bun run --cwd packages/app build -CYBERSTRIKE_BUILD_TARGET=linux-x64 bun run --cwd packages/cyberstrike script/build.ts - -# Installs the binary and its sibling HackBrowser worker. -./install --binary packages/cyberstrike/dist/cyberstrike-linux-x64/bin/cyberstrike - -# Install the locally built Web UI. -install -d "${XDG_DATA_HOME:-$HOME/.local/share}/cyberstrike/web" -cp -R packages/app/dist/. "${XDG_DATA_HOME:-$HOME/.local/share}/cyberstrike/web/" - -CYBERSTRIKE_SERVER_PASSWORD=change-me cyberstrike web --hostname 127.0.0.1 -``` - -Use `linux-x64-baseline` on x64 CPUs without AVX2, or the corresponding `*-musl` target on musl-based distributions. Back up the installed binary, configuration, and data directory before replacing a production deployment. - -For a persistent localhost-only deployment, install `contrib/systemd/cyberstrike-web.service` under `~/.config/systemd/user/`, create a mode `0600` `~/.config/cyberstrike/web.env` containing `CYBERSTRIKE_SERVER_PASSWORD`, then run: - -```bash -systemctl --user daemon-reload -systemctl --user enable --now cyberstrike-web.service -``` - -Use an SSH or authenticated Cloudflare tunnel for remote access rather than exposing port 4096 directly. - --- ### Who Is This For? @@ -540,13 +494,13 @@ This personal workstream is based on the upstream [CyberStrike](https://github.c CyberStrike is the core platform. These MCP servers extend its capabilities: -| Project | Domain | Tools | -| --- | --- | --- | -| **CyberStrike** | **Autonomous offensive security agent** | **13+ agents, 56+ tools, 7,600+ skills, 150+ AI providers** | -| [cloud-audit-mcp](https://github.com/badchars/cloud-audit-mcp) | Cloud security (AWS/Azure/GCP) | 38 tools, 60+ checks | -| [github-security-mcp](https://github.com/badchars/github-security-mcp) | GitHub security posture | 39 tools, 45 checks | -| [cve-mcp](https://github.com/badchars/cve-mcp) | Vulnerability intelligence | 23 tools, 5 sources | -| [osint-mcp](https://github.com/badchars/osint-mcp-server) | OSINT & reconnaissance | 37 tools, 12 sources | +| Project | Domain | Tools | +| ---------------------------------------------------------------------- | --------------------------------------- | ----------------------------------------------------------- | +| **CyberStrike** | **Autonomous offensive security agent** | **13+ agents, 56+ tools, 7,600+ skills, 150+ AI providers** | +| [cloud-audit-mcp](https://github.com/badchars/cloud-audit-mcp) | Cloud security (AWS/Azure/GCP) | 38 tools, 60+ checks | +| [github-security-mcp](https://github.com/badchars/github-security-mcp) | GitHub security posture | 39 tools, 45 checks | +| [cve-mcp](https://github.com/badchars/cve-mcp) | Vulnerability intelligence | 23 tools, 5 sources | +| [osint-mcp](https://github.com/badchars/osint-mcp-server) | OSINT & reconnaissance | 37 tools, 12 sources | --- diff --git a/packages/app/src/pages/session/session-prompt-dock.tsx b/packages/app/src/pages/session/session-prompt-dock.tsx index be66cc138c..2b58009aea 100644 --- a/packages/app/src/pages/session/session-prompt-dock.tsx +++ b/packages/app/src/pages/session/session-prompt-dock.tsx @@ -9,7 +9,9 @@ import { questionSubtitle } from "@/pages/session/session-prompt-helpers" export function SessionPromptDock(props: { centered: boolean questionRequest: () => QuestionRequest | undefined - permissionRequest: () => { patterns: string[]; permission: string } | undefined + permissionRequest: () => + | { patterns: string[]; permission: string; metadata?: Record } + | undefined blocked: boolean promptReady: boolean handoffPrompt?: string @@ -68,7 +70,7 @@ export function SessionPromptDock(props: { : perm.permission, }} > - 0}> + 0 && perm.permission !== "nmap_scan"}>
{(pattern) => {pattern}} @@ -80,6 +82,25 @@ export function SessionPromptDock(props: { {props.t("settings.permissions.tool.doom_loop.description")}
+ +
+
Active network scan
+
+ Target: {String(perm.metadata?.target ?? perm.patterns[0] ?? "unknown")} ·{" "} + Profile: {String(perm.metadata?.profile ?? "service")} + {perm.metadata?.ports ? ` · Ports: ${String(perm.metadata.ports)}` : ""} + {perm.metadata?.privileged ? " · Elevated execution" : ""} +
+ + + {String(perm.metadata?.command)} + + +
+ Approval saves canonical XML and updates scan history and Topology. +
+
+
diff --git a/packages/app/src/pages/session/topology-panel.tsx b/packages/app/src/pages/session/topology-panel.tsx index 27cc3a87f7..560bfd7bc2 100644 --- a/packages/app/src/pages/session/topology-panel.tsx +++ b/packages/app/src/pages/session/topology-panel.tsx @@ -63,6 +63,7 @@ export function TopologyPanel() { const [from, setFrom] = createSignal("") const [to, setTo] = createSignal("") const [importing, setImporting] = createSignal(false) + const [historySession, setHistorySession] = createSignal("") const [draft, setDraft] = createStore({ content: "", link: "", saving: false }) const [scan, setScan] = createStore({ target: "", @@ -88,6 +89,7 @@ export function TopologyPanel() { setGraph(reconcile(topology.data)) setNotes(reconcile(noteList.data ?? [])) setScans(reconcile(history.data ?? [])) + setHistorySession(sessionID) const available = history.data ?? [] if (available.length >= 2 && (!available.some((scan) => scan.id === from()) || !available.some((scan) => scan.id === to()))) { setFrom(available.at(-2)!.id) @@ -115,7 +117,11 @@ export function TopologyPanel() { } createEffect(() => { - params.id + const sessionID = params.id + setGraph(reconcile({ sessionID: sessionID ?? "", nodes: [], edges: [], time: 0 })) + setNotes(reconcile([])) + setScans(reconcile([])) + setHistorySession("") setFrom("") setTo("") setDiff(undefined) @@ -170,7 +176,7 @@ export function TopologyPanel() { const prepareScan = () => { const target = scan.target.trim() if (!target) return - const value = `Run the built-in nmap_scan tool against the explicitly authorized target ${target} with the ${scan.profile} profile. Preview the exact command (${command()}), confirm scope and expected impact, and wait for my approval before starting. Persist the XML result into topology and compare it with prior scans.` + const value = `Run the built-in nmap_scan tool against the explicitly authorized target ${target} with the ${scan.profile} profile. Preview the planned Nmap arguments (${command()}); the approval card must show the resolved executor and exact command, including sudo when elevated. Confirm scope and expected impact, and wait for my approval before starting. Persist the XML result into topology and compare it with prior scans.` prompt.set([{ type: "text", content: value, start: 0, end: value.length }], value.length) } @@ -422,6 +428,12 @@ export function TopologyPanel() {
+ +
+ No managed Nmap evidence yet. Generic shell output cannot populate this graph; use Prepare scan or + Import XML so canonical results are saved to Topology. +
+
diff --git a/packages/cyberstrike/README.md b/packages/cyberstrike/README.md index 893debf778..54a9622b0d 100644 --- a/packages/cyberstrike/README.md +++ b/packages/cyberstrike/README.md @@ -180,16 +180,13 @@ Each proxy tester follows a structured methodology: intercept traffic, identify ### Web UI & Remote Access -Run `cyberstrike web` and control your agents, MCP servers, Bolt connections, always-visible live activity, event-driven mission posture, Nmap scan history/diffs and topology, structured memory, and vulnerability findings from any browser. Access from anywhere with Cloudflare Tunnel — zero open ports, end-to-end encryption, password-protected API. An optional `observer` credential is restricted server-side to redacted read-only routes. Your data stays on your machine. +Run `cyberstrike web` and control your agents, MCP servers, Bolt connections, and vulnerability findings from any browser. Access from anywhere with Cloudflare Tunnel — zero open ports, end-to-end encryption, password-protected API. Your data stays on your machine. ```bash export CYBERSTRIKE_SERVER_PASSWORD=your-secure-password -export CYBERSTRIKE_OBSERVER_PASSWORD=your-observer-password # optional read-only API/viewer role cyberstrike web ``` -If user or project configuration prevents startup, run `cyberstrike web --safe` to start recovery mode without those config sources. Managed administrator policy is still enforced. - Use **[app.cyberstrike.io](https://app.cyberstrike.io)** (static page, no backend) or self-host from `packages/app/dist/`. See the [full README](https://github.com/CyberStrikeus/CyberStrike#web-ui--remote-access) for the complete security model. @@ -227,9 +224,16 @@ Bolt is CyberStrike's remote tool server. Deploy it on any VPS, cloud instance, ### MCP Ecosystem -CyberStrike includes a curated MCP catalog with roughly **724 direct/composite security tools** across 11 default entries. Runnable npm entries are version-pinned; `cloud-audit-mcp` and `hackbrowser-mcp` currently require manual installation from their repositories. +CyberStrike connects to specialized MCP servers that extend its capabilities — **176+ security tools** across 5 domains: + +| Server | Tools | What It Adds | +| ---------------------------------------------------------------------- | ----- | -------------------------------------------------------------------- | +| [cloud-audit-mcp](https://github.com/badchars/cloud-audit-mcp) | 38 | Cloud security audits — 60+ checks across AWS, Azure, GCP | +| [github-security-mcp](https://github.com/badchars/github-security-mcp) | 39 | GitHub security posture — repo, org, actions, secrets, supply chain | +| [cve-mcp](https://github.com/badchars/cve-mcp) | 23 | CVE intelligence — NVD, EPSS, CISA KEV, GitHub Advisory, OSV | +| [osint-mcp](https://github.com/badchars/osint-mcp) | 37 | OSINT recon — Shodan, VirusTotal, SecurityTrails, Censys, DNS, WHOIS | -The catalog covers GitHub posture (39), CVE intelligence (41), OSINT (37), cloud audit (38), HackBrowser (39), darknet intelligence (66), DNS security (103), supply-chain analysis (7 composite tools / 90 techniques), MCP security scanning (55), steganography (128), and satellite/GEOINT (171). Optional owner-maintained entries add wireless security, LOLBin intelligence, and fingerprinting. +All open source. All installable with `npx`. Plug them into CyberStrike or use them standalone with any MCP-compatible client. --- @@ -268,25 +272,6 @@ scoop install cyberstrike curl -fsSL https://cyberstrike.io/install.sh | bash ``` -#### Build and deploy on Kali/Linux from source - -Build the matching Linux binary and web bundle with the repository-pinned Bun version: - -```bash -bun install --frozen-lockfile -bun run --cwd packages/app build -CYBERSTRIKE_BUILD_TARGET=linux-x64 bun run --cwd packages/cyberstrike script/build.ts -./install --binary packages/cyberstrike/dist/cyberstrike-linux-x64/bin/cyberstrike - -install -d "${XDG_DATA_HOME:-$HOME/.local/share}/cyberstrike/web" -cp -R packages/app/dist/. "${XDG_DATA_HOME:-$HOME/.local/share}/cyberstrike/web/" -CYBERSTRIKE_SERVER_PASSWORD=change-me cyberstrike web --hostname 127.0.0.1 -``` - -The installer also copies the sibling HackBrowser worker. Use `linux-x64-baseline` for x64 CPUs without AVX2 and a `*-musl` target for musl-based distributions. - -For a persistent localhost-only deployment, install [`contrib/systemd/cyberstrike-web.service`](../../contrib/systemd/cyberstrike-web.service), set `CYBERSTRIKE_SERVER_PASSWORD` in a mode `0600` `~/.config/cyberstrike/web.env`, and enable the unit with `systemctl --user enable --now cyberstrike-web.service`. - --- ### Who Is This For? diff --git a/packages/cyberstrike/src/tool/nmap-scan.ts b/packages/cyberstrike/src/tool/nmap-scan.ts index a81440a43c..e3f90c3abd 100644 --- a/packages/cyberstrike/src/tool/nmap-scan.ts +++ b/packages/cyberstrike/src/tool/nmap-scan.ts @@ -25,6 +25,23 @@ const profiles: Record, string[]> = { comprehensive: ["-T4", "-sV", "-O", "-sC"], } +const invocation = (input: { + binary: string + profile: z.infer + sudo?: string + platform?: NodeJS.Platform + uid?: number +}) => { + const privileged = input.profile === "os" || input.profile === "comprehensive" + if (!privileged || input.platform === "win32" || input.uid === 0) { + return { argv: [input.binary], privileged } + } + if (!input.sudo) throw new Error("This Nmap profile requires root or passwordless sudo on this execution plane") + return { argv: [input.sudo, "-n", input.binary], privileged } +} + +const scope = (target: string, privileged: boolean) => `${privileged ? "elevated" : "standard"}:${target}` + export const NmapScanTool = Tool.define("nmap_scan", { description: "Run an authorized Nmap profile, stream progress, persist canonical XML, and update topology. This performs active network testing and always requires explicit target approval.", @@ -38,16 +55,13 @@ export const NmapScanTool = Tool.define("nmap_scan", { async execute(params, ctx) { const binary = Bun.which("nmap") if (!binary) throw new Error("Nmap is not installed on this execution plane") - await ctx.ask({ - permission: "nmap_scan", - patterns: [params.target], - always: [params.target], - metadata: { - profile: params.profile, - ports: params.ports, - }, + const exec = invocation({ + binary, + profile: params.profile, + sudo: Bun.which("sudo") ?? undefined, + platform: process.platform, + uid: typeof process.getuid === "function" ? process.getuid() : undefined, }) - const args = [ ...profiles[params.profile], ...(params.ports ? ["-p", params.ports] : []), @@ -57,8 +71,22 @@ export const NmapScanTool = Tool.define("nmap_scan", { "-", params.target, ] - const command = [binary, ...args].join(" ") - const proc = Bun.spawn([binary, ...args], { + const command = [...exec.argv, ...args].join(" ") + await ctx.ask({ + permission: "nmap_scan", + patterns: [scope(params.target, exec.privileged)], + always: [scope(params.target, exec.privileged)], + metadata: { + target: params.target, + profile: params.profile, + ports: params.ports, + privileged: exec.privileged, + executor: exec.argv[0], + command, + }, + }) + + const proc = Bun.spawn([...exec.argv, ...args], { stdout: "pipe", stderr: "pipe", env: process.env, @@ -132,4 +160,6 @@ export const NmapScanParameters = { Target, Ports, Profile, + Invocation: invocation, + Scope: scope, } diff --git a/packages/cyberstrike/test/tool/nmap-scan.test.ts b/packages/cyberstrike/test/tool/nmap-scan.test.ts index c5898180b4..e9ba2fa8a7 100644 --- a/packages/cyberstrike/test/tool/nmap-scan.test.ts +++ b/packages/cyberstrike/test/tool/nmap-scan.test.ts @@ -18,4 +18,38 @@ describe("nmap_scan parameters", () => { expect(NmapScanParameters.Ports.parse("22,80,443,8000-8100")).toBe("22,80,443,8000-8100") expect(NmapScanParameters.Ports.safeParse("http,https").success).toBe(false) }) + + test("uses passwordless sudo only for privileged Unix profiles", () => { + expect( + NmapScanParameters.Invocation({ + binary: "/usr/bin/nmap", + profile: "service", + sudo: "/usr/bin/sudo", + platform: "linux", + uid: 1000, + }), + ).toEqual({ argv: ["/usr/bin/nmap"], privileged: false }) + expect( + NmapScanParameters.Invocation({ + binary: "/usr/bin/nmap", + profile: "os", + sudo: "/usr/bin/sudo", + platform: "linux", + uid: 1000, + }), + ).toEqual({ argv: ["/usr/bin/sudo", "-n", "/usr/bin/nmap"], privileged: true }) + expect(() => + NmapScanParameters.Invocation({ + binary: "/usr/bin/nmap", + profile: "comprehensive", + platform: "linux", + uid: 1000, + }), + ).toThrow("passwordless sudo") + }) + + test("separates standard and elevated approval scopes", () => { + expect(NmapScanParameters.Scope("192.0.2.10", false)).toBe("standard:192.0.2.10") + expect(NmapScanParameters.Scope("192.0.2.10", true)).toBe("elevated:192.0.2.10") + }) }) From 58b97d0b829e1ede1a3478974110f12b39e65862 Mon Sep 17 00:00:00 2001 From: spetro511 Date: Thu, 3 Sep 2026 01:44:07 -0400 Subject: [PATCH 4/4] docs: clarify personal fork attribution Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 544 ++++++++------------------------- packages/cyberstrike/README.md | 35 ++- 2 files changed, 160 insertions(+), 419 deletions(-) diff --git a/README.md b/README.md index 1843a33519..76eab5e19e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@

- English | + English (personal fork) | + Upstream translations: 简体中文 | 繁體中文 | 한국어 | @@ -28,488 +29,213 @@ - CyberStrike by Suren Sahaydachny — open-source AI agent for offensive security + CyberStrike open-source offensive security agent

-

CyberStrike

- -

The AI-first era of offensive security is already here.

- -

- A personal open-source project maintained by Suren Sahaydachny. -

+

Personal fork: an enhanced CyberStrike distribution and operational workbench.

- The old model made the security professional the middleware between dozens of disconnected tools.
- CyberStrike replaces that fragmentation with one intelligent, inspectable, open-source orchestration layer.
- Bring Claude, GPT, Gemini, Copilot, or a local model. Leave with an autonomous red team. + Maintained by Suren Sahayachny (spetro511)
+ and built on the original open-source project from + CyberStrikeus/CyberStrike + and its contributors.

- 150+ AI providers5,300+ models56+ built-in tools176+ MCP tools + About this fork • + Workbench • + Installation • + Upstream • + Ethical use • + License

- Built by Suren Sahaydachny - Email Suren Sahaydachny - Connect with Suren Sahaydachny on LinkedIn -

- -

- Suren Sahaydachny on GitHub - npm - Downloads - Build - License -

- -

- Why I Built This • - Quick Start • - Intelligence Layer • - What Makes It Different • - Agents • - Skills • - Web UI • - Bolt • - MCP Ecosystem • - Post-Exploitation • - Installation • - Docs • - Website + Personal fork + Upstream project + License

--- -## A Personal Note from Suren Sahaydachny - -> We do not notice revolutions when they start quietly. - -Offensive security is still fragmented across terminals, browsers, scanners, notes, scripts, dashboards, and tribal knowledge. The tools are powerful. The system around them is not. We ask talented people to copy, paste, context-switch, remember every finding, and manually conduct an orchestra that was never designed to play together. - -That model got us here. It will not define what comes next. - -CyberStrike is my personal open-source workstream for the next era of security: an AI-first orchestration layer that turns models, agents, methodologies, browsers, remote infrastructure, and specialist tools into one coordinated system. These are not disconnected utilities wearing an AI badge. They are parts of a security platform that can reason about the objective, choose the right instrument, preserve context, validate results, and keep a human in control. - -The future will not belong to the team with the most dashboards. It will belong to the team that can embed intelligence into the flow of work so naturally that the complexity recedes and the outcome takes center stage. +### About This Fork -I am open-sourcing this work because security infrastructure should be inspectable. Methodology should be shareable. Intelligence should not be trapped behind one provider, one model, or one company. The strongest systems will be built in the open, pressure-tested by the people who use them, and improved by a community bold enough to challenge the old assumptions. +> [!IMPORTANT] +> CyberStrike originated at +> [**`CyberStrikeus/CyberStrike`**](https://github.com/CyberStrikeus/CyberStrike). This repository is Suren +> Sahayachny's personal fork of that AGPL-licensed open-source project. Suren did **not** create the original +> CyberStrike project; its original authors and contributors retain full credit through the upstream repository and +> Git history. -I have been waiting my whole life for this moment—the point where AI moves from science fiction into our everyday reality. Now I get to build it. - -**— Suren Sahaydachny**
-Builder and maintainer of this personal CyberStrike workstream
-[surenpeter511@gmail.com](mailto:surenpeter511@gmail.com) · [LinkedIn](https://www.linkedin.com/in/suren-sahaydachny) · [GitHub](https://github.com/spetro511) - -| The project at a glance | | -| --- | --- | -| **Maintainer** | **Suren Sahaydachny** | -| **Mission** | Make serious offensive security automation open, adaptable, and model-agnostic | -| **Philosophy** | Do not build another tool. Build a system that knows how to use the tools. | -| **Operating model** | Human judgment + agentic execution + reproducible evidence | -| **License** | AGPL-3.0-only | -| **Contact** | [surenpeter511@gmail.com](mailto:surenpeter511@gmail.com) | - -> **Authorized security testing only.** CyberStrike is built for systems you own or have explicit permission to assess. Capability without judgment is noise; capability with accountability is leverage. - ---- - -### Quick Start - -```bash -npm i -g @cyberstrike-io/cyberstrike@latest && cyberstrike -``` - -That's it. CyberStrike launches a TUI in your terminal, asks for your LLM provider and API key on first run, and you're ready to go. Tell it what to test — it handles reconnaissance, vulnerability discovery, exploitation, and reporting autonomously. - -> **Already have a Claude Code or OpenAI subscription?** CyberStrike's intelligence layer sits on top of your existing AI subscription. No separate API costs — your current plan powers an entire pentest toolkit. - -Explore the full documentation at **[docs.cyberstrike.io](https://docs.cyberstrike.io)** or visit **[cyberstrike.io](https://cyberstrike.io)** for demos and guides. - ---- +Suren took the upstream project and substantially enhanced it as an operational distribution for day-to-day, +authorized security work. The main contribution of this fork is an expanded Web UI that acts as a centralized hub +for observing an engagement, understanding its posture, managing evidence, and operating a managed CyberStrike +deployment. -### Intelligence Layer - -CyberStrike isn't just a wrapper around an LLM. It's an intelligence layer that transforms any AI model into an offensive security specialist. - -**How it works:** When you connect your LLM provider, CyberStrike injects domain-specific context — OWASP testing methodology, vulnerability patterns, attack chain reasoning, and tool orchestration logic — into every interaction. The model doesn't need to know security; CyberStrike teaches it. - -> **My design principle:** The AI should not be the star of the show. It should be the conductor behind the curtain — coordinating every instrument, preserving context, and making the hard parts feel inevitable. -> -> **— Suren Sahaydachny** - -**What the intelligence layer provides:** - -- **Schema normalization** — Structured output from any provider, regardless of response format differences -- **Context guard** — Prevents prompt leakage and keeps the agent focused on the current test phase -- **Provider auto-detection** — Automatically identifies your LLM endpoint and configures the optimal transport -- **Tool orchestration** — Chains security tools intelligently based on findings, not fixed scripts - -**150+ AI providers and 5,300+ models supported out of the box:** - -CyberStrike integrates with the entire AI ecosystem through 23 bundled SDK providers and 150+ providers via the [models.dev](https://models.dev) catalog. Here are the core integrations: - -| Provider | Models | Notes | -| ------------------------- | ------------------------ | --------------------------------------- | -| **Anthropic** | Claude 4.5, Claude 4 | Best performance with extended thinking | -| **OpenAI** | GPT-5, GPT-4.1, o3, o4 | Full tool-use + reasoning support | -| **Google** | Gemini 2.5 Pro/Flash | Long context for large codebases | -| **Amazon Bedrock** | All Bedrock models | IAM auth, no API keys needed | -| **Azure OpenAI** | All Azure-hosted models | Enterprise deployments | -| **Google Vertex AI** | Gemini + Claude on GCP | Regional endpoints (EU/US) | -| **GitHub Copilot** | GPT-5, Claude, Gemini | Use your existing Copilot subscription | -| **xAI** | Grok 3, Grok 3 Mini | Real-time data access | -| **Groq** | LLaMA, Mixtral | Ultra-fast inference | -| **Mistral** | Mistral Large, Codestral | European data residency | -| **DeepSeek** | DeepSeek V3, R1 | Cost-effective alternative | -| **Cerebras** | LLaMA on Cerebras | Fastest inference available | -| **Cohere** | Command R+ | RAG-optimized models | -| **OpenRouter** | 300+ models | Single API, any model | -| **Together AI** | Open-source models | Fine-tuning support | -| **DeepInfra** | Open-source models | Pay-per-token, no GPU needed | -| **Perplexity** | Sonar models | Search-augmented generation | -| **Alibaba Cloud** | Qwen, Kimi, DashScope | Chinese model ecosystem | -| **Cloudflare AI Gateway** | Any provider via gateway | Caching, rate limiting, analytics | -| **Ollama** | Any GGUF model | Fully offline, local-only | -| **LM Studio** | Any local model | Desktop GUI + API server | -| **vLLM** | Any HuggingFace model | Self-hosted, GPU-optimized | -| **Any OpenAI-compatible** | — | Custom endpoints welcome | - -> **Air-gapped environments?** Run CyberStrike entirely offline with Ollama or LM Studio. No data leaves your machine — ever. +The upstream package, website, documentation, releases, and community remain upstream resources. Features documented +as fork enhancements below may not be present in an upstream release. --- -### What Makes It Different +### Operational Workbench - - - - - - - - - -
+The fork connects the existing agent, tool, browser, MCP, Bolt, and finding data into one live browser workbench. -**Specialized Security Agents, Not Generic Chat** - -CyberStrike ships with 13+ agents purpose-built for security domains. Each agent carries domain-specific methodology, tool knowledge, and testing patterns. The web-application agent follows OWASP WSTG. The cloud-security agent knows CIS benchmarks. The mobile agent uses Frida and follows MASTG/MASVS. They don't guess — they follow proven offensive security frameworks. - - - -**Intelligence Layer, Not Just an LLM Wrapper** - -Most AI security tools are thin wrappers that send your prompt to an API. CyberStrike's intelligence layer normalizes outputs across 150+ providers and 5,300+ models, guards context between test phases, auto-detects your provider configuration, and orchestrates multi-step attack chains. The result: consistent, methodology-driven pentesting regardless of which model you use. - -
- -**150+ Providers, Zero Lock-in** - -Anthropic, OpenAI, Google, Amazon Bedrock, Azure, Groq, Mistral, xAI, DeepSeek, Cerebras, Cohere, OpenRouter, Together AI, GitHub Copilot — or run fully offline with Ollama and LM Studio. 150+ providers, 5,300+ models. You choose the model. You own the results. As AI models get better and cheaper, CyberStrike gets better with them. Switch providers in seconds without reconfiguring anything. - - - -**Remote Tool Execution with Bolt** - -Your security tools don't have to run on your laptop. Deploy Bolt on one or many remote servers, pair with Ed25519 keys, and control everything from your local terminal. One CyberStrike instance can orchestrate dozens of Bolt servers — each with its own toolkit, network position, and attack surface access. - -
- ---- - -### Agents - -Switch between agents with `Tab`. Each one is a domain specialist. - -| Agent | Focus | What It Does | -| ---------------------- | ------- | ------------------------------------------------------------------- | -| **cyberstrike** | General | Full-access primary agent — reconnaissance, exploitation, reporting | -| **web-application** | Web | OWASP Top 10, WSTG methodology, API security, session testing | -| **mobile-application** | Mobile | Android/iOS, Frida/Objection, MASTG/MASVS compliance | -| **cloud-security** | Cloud | AWS, Azure, GCP — IAM misconfigs, CIS benchmarks, exposed resources | -| **internal-network** | Network | Active Directory, Kerberos attacks, lateral movement, pivoting | - -Plus **8 specialized proxy testers** that run automatically on intercepted traffic: - -| Tester | What It Tests | -| ------------------------ | ---------------------------------------------------------------------------- | -| **IDOR** | Object-level access control — can user A reach user B's resources? | -| **Authorization Bypass** | Vertical privilege escalation — can low-privilege users hit admin endpoints? | -| **Mass Assignment** | Unexpected writable fields — role, price, balance, userId in request bodies | -| **Injection** | SQL, command, LDAP, template injection across all input vectors | -| **Authentication** | Token validation, session fixation, credential exposure | -| **Business Logic** | Price manipulation, coupon reuse, race conditions, workflow bypass | -| **SSRF** | Internal host access via user-controlled URLs or redirect parameters | -| **File Attacks** | Path traversal, unrestricted upload, dangerous file types | - -Each tester uses a **3-gate confirmation protocol**: execute a baseline request, execute the attack, compare responses. A finding is only reported when there is a measurable, reproducible difference — not on speculation. Duplicate findings (same endpoint + attack vector) are automatically suppressed across the session. - ---- - -### Security Skills - -CyberStrike ships with **7,600+ security skill files** — structured, Ed25519-signed methodology documents that give agents deep domain knowledge at runtime. Skills are lazy-loaded (one at a time, on demand) and statically injected into agent prompts. - -**Skill categories:** - -| Category | Skills | What They Cover | -| ------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **Attack Methodologies** | 19 | JWT attacks, SSRF, SSTI, race conditions, request smuggling, cache poisoning, CORS, GraphQL, prototype pollution, XXE, WebSocket, subdomain takeover, host header injection, open redirect | -| **Post-Exploitation** | 5 | AWS, Azure, Kubernetes, Windows, macOS privilege escalation and persistence | -| **Compliance Frameworks** | 3 | CIS Benchmarks (AWS/Azure/GCP/K8s), NIST Framework, MITRE ATT&CK (Enterprise, Mobile, ICS) | -| **Domain Knowledge** | 8+ | Active Directory security, web security patterns, recon methodology, CI/CD attacks, Kerberos attacks, eBPF techniques | - -Each skill includes testing procedures, payloads, tool commands, and CWE mappings. Skills are tagged with OWASP WSTG IDs, CIS control IDs, and chain relationships — so agents know which skills to combine for multi-step attack chains. - ---- +| Area | Fork enhancement | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Central Web UI** | A session workbench that keeps agent interaction, findings, execution status, and engagement context together instead of treating the browser as a static companion view. | +| **Live activity** | Durable, redacted engagement events with live updates, correlation data, source filters, timeline and lane views, reconnect recovery, and JSONL export. | +| **Mission posture** | Methodology phase state, per-asset coverage, blockers, warnings, attack-chain candidates, agent performance, and approval-aware next actions. | +| **Topology** | Evidence-linked assets, hosts, services, routes, endpoints, identities, findings, and relationships in a searchable engagement graph. | +| **Nmap workflows** | Approval-gated scan profiles, exact command previews, canonical XML ingestion, saved scan history, topology projection, and comparisons between scans. | +| **Target notes** | Operator-authored notes attached to topology entities, with links and human-confirmed provenance. | +| **Structured memory** | Project and session memory with working, episodic, semantic, and procedural categories; search, provenance, trust, confidence, redaction, and invalidation. | +| **Observer access** | A server-enforced read-only role for redacted activity, mission, topology, findings, and status data without mutation, secret, raw-event, PTY, or configuration access. | +| **Managed Kali deployment** | Target-selectable source builds, installation of the matching HackBrowser worker and Web UI, and a localhost-only `systemd` user service template. | -### HackBrowser +The implementation is visible in the +[session workbench UI](./packages/app/src/pages/session/), +[server routes](./packages/cyberstrike/src/server/routes/), +[topology and Nmap model](./packages/cyberstrike/src/topology/), and +[structured memory store](./packages/cyberstrike/src/memory/). -> Full documentation: **[docs.cyberstrike.io/docs/tools/hacker-browser](https://docs.cyberstrike.io/docs/tools/hacker-browser/)** +#### Operational flow -HackBrowser is CyberStrike's built-in Chromium browser. Start it from the TUI with `/hackbrowser`. As you browse, every HTTP request is captured and routed through the proxy-agent pipeline — no manual export, no Burp project files. - -**Two capture modes:** - -- **Manual** — Browse the target yourself. Log in as different users, navigate features, trigger actions. HackBrowser captures the real API traffic behind every click. -- **Autonomous** — Provide credentials for multiple accounts, set a scope, and let HackBrowser crawl automatically. It logs in as each user, maps reachable pages, and captures the traffic difference between roles. - -**Role & credential discovery:** - -As you browse with multiple accounts, CyberStrike builds a session context — a live map of discovered credentials, inferred role hierarchy, and which endpoints each role can reach. The 8 proxy sub-testers use this context directly: they know which token to use for a high-privilege baseline and which lower-privilege credentials to test with, without any manual setup. +```text +Authorized engagement + | + v +CyberStrike agent and tools -----> durable, redacted activity + | | + +--> Mission posture +--> Web UI timeline and lanes + +--> Nmap evidence --> Topology + +--> Findings and target notes + +--> Structured project/session memory +Operator: full authenticated control +Observer: redacted read-only projection ``` -Browser traffic → Proxy intercept → Orchestrator → 8 sub-testers (parallel) - ↓ - Session context (credentials, roles, - endpoints, functions) shared across all testers -``` - -**Scope control:** - -Use `--scope` to limit testing to specific domains. CyberStrike automatically derives the registered domain (e.g. `--scope api.example.com` covers `api.example.com` but not `other.com`). Pass multiple `--scope` flags for multi-domain targets. --- -### Web UI & Remote Access +### Using This Fork -CyberStrike includes a full web interface. Run `cyberstrike web` and control your agents, MCP servers, Bolt connections, and vulnerability findings from any browser. +#### Upstream release -**Access from anywhere with Cloudflare Tunnel:** - -``` -Browser ──HTTPS──▶ Cloudflare Tunnel ──encrypted──▶ cloudflared (localhost) ──▶ CyberStrike Server -``` +The published package is maintained by the upstream project: ```bash -export CYBERSTRIKE_SERVER_PASSWORD=your-secure-password -cyberstrike web -# In another terminal: -cloudflared tunnel --url http://localhost:4096 run your-tunnel +npm i -g @cyberstrike-io/cyberstrike@latest +cyberstrike ``` -**Why this is secure:** - -- **Zero open ports** — CyberStrike binds to `localhost:4096`. `cloudflared` makes an outbound-only connection to Cloudflare's edge. No firewall rules, no port forwarding needed. -- **End-to-end encryption** — Browser to Cloudflare edge is TLS. Cloudflare edge to your machine is an encrypted tunnel. No plaintext leaves your network. -- **Password-protected API** — Every API request requires Basic Auth. Local requests on `localhost` bypass auth for convenience; remote requests via CF tunnel always require credentials (detects `X-Forwarded-For` / `CF-Connecting-IP`). -- **Your data stays local** — LLM inference runs on your hardware. CyberStrike processes everything locally. The tunnel is just a secure pipe. - -**What's in the Web UI:** - -| Tab | What It Does | -| ------------------- | ------------------------------------------------------------------------------ | -| **Chat** | Full conversation with all 13+ security agents | -| **MCP** | Live MCP server status, health, and tool counts | -| **Bolt** | Bolt remote server connection monitoring | -| **Vulnerabilities** | Discovered vulns with severity, PoC, and impact | -| **Web Context** | Endpoints, roles, credentials, and functions discovered during active sessions | +See the [upstream documentation](https://docs.cyberstrike.io) for its supported release workflow. The npm package +does not necessarily include enhancements that exist only on this fork. -**[app.cyberstrike.io](https://app.cyberstrike.io)** is a hosted static page (no backend, no data storage) for convenience. Or self-host: clone the repo and serve `packages/app/dist/` from your own domain. +#### Build and deploy the fork on Kali/Linux ---- +Source deployments require the compiled binary, matching HackBrowser worker, and Web UI bundle. Use the +repository-pinned Bun version: -### Bolt — Remote Tool Execution +```bash +bun install --frozen-lockfile +bun run --cwd packages/app build +CYBERSTRIKE_BUILD_TARGET=linux-x64 bun run --cwd packages/cyberstrike script/build.ts -Bolt is CyberStrike's remote tool server. Deploy it on any VPS, cloud instance, or Docker container — then control it from your local terminal over MCP protocol with Ed25519 authentication. +# Installs the binary and its sibling HackBrowser worker. +./install --binary packages/cyberstrike/dist/cyberstrike-linux-x64/bin/cyberstrike -**One CyberStrike, many Bolt servers:** +# Installs the locally built Web UI. +install -d "${XDG_DATA_HOME:-$HOME/.local/share}/cyberstrike/web" +cp -R packages/app/dist/. "${XDG_DATA_HOME:-$HOME/.local/share}/cyberstrike/web/" +CYBERSTRIKE_SERVER_PASSWORD=change-me cyberstrike web --hostname 127.0.0.1 ``` - ┌─────────────────────┐ - ┌───►│ Bolt Server #1 │ - │ │ nmap, nuclei, ffuf │ -┌──────────────────┐ MCP + Ed25519 │ └─────────────────────┘ -│ Your Terminal │ over HTTPS │ ┌─────────────────────┐ -│ CyberStrike TUI │ ◄─────────────►├───►│ Bolt Server #2 │ -│ │ Tool Results │ │ sqlmap, burp, zap │ -└──────────────────┘ │ └─────────────────────┘ - │ ┌─────────────────────┐ - └───►│ Bolt Server #3 │ - │ Custom toolkit │ - └─────────────────────┘ -``` - -- **Deploy anywhere** — VPS, Docker, Kubernetes, or bare metal with pre-built Kali images -- **Ed25519 key pairing** — No passwords, no shared secrets, no attack surface -- **Real-time streaming** — Results flow back to your TUI as they happen -- **Manage from TUI** — Add, remove, and monitor Bolt servers without leaving CyberStrike -- **Scale horizontally** — Run heavy scans from servers with better bandwidth while you work locally - ---- - -### MCP Ecosystem - -CyberStrike connects to specialized MCP servers that extend its capabilities — **176+ security tools** across 5 domains: - -| Server | Tools | What It Adds | -| ---------------------------------------------------------------------- | ----- | -------------------------------------------------------------------- | -| [cloud-audit-mcp](https://github.com/badchars/cloud-audit-mcp) | 38 | Cloud security audits — 60+ checks across AWS, Azure, GCP | -| [github-security-mcp](https://github.com/badchars/github-security-mcp) | 39 | GitHub security posture — repo, org, actions, secrets, supply chain | -| [cve-mcp](https://github.com/badchars/cve-mcp) | 23 | CVE intelligence — NVD, EPSS, CISA KEV, GitHub Advisory, OSV | -| [osint-mcp](https://github.com/badchars/osint-mcp) | 37 | OSINT recon — Shodan, VirusTotal, SecurityTrails, Censys, DNS, WHOIS | - -All open source. All installable with `npx`. Plug them into CyberStrike or use them standalone with any MCP-compatible client. - ---- - -### Built-in Tools - -CyberStrike agents have direct access to **56+ tools** without any external dependencies: -| Category | Tools | -| --------------------- | ----------------------------------------------------------------------------------- | -| **Execution** | Shell (bash), file read/write/edit/patch, directory listing, batch operations | -| **Discovery** | Web fetch, web search, code search, glob, grep, intel gathering | -| **Offensive** | HackBrowser, attack script execution, vulnerability reporting & triage | -| **Post-Exploitation** | AWS hook, Azure hook, Kubernetes hook, Windows hook, macOS hook, CI/CD pipe, eBPF | -| **Web Context** | Session context, endpoint/role/credential/function discovery and management | -| **Proxy** | HTTP/HTTPS interception, request replay, session context sharing across sub-testers | -| **Reporting** | Professional report generation, coverage notes, methodology tracking, VRT checks | -| **Integration** | MCP servers, Bolt remote tools, custom plugins, LSP | +Use `linux-x64-baseline` on x64 CPUs without AVX2, or the corresponding `*-musl` target on musl-based +distributions. Back up the installed binary, configuration, and data directory before replacing a managed +deployment. -Plus a **plugin SDK** with 15+ hook types (tool interception, message transformation, permission prompts, shell environment) — build your own agents and tools, register them at runtime. - ---- - -### Post-Exploitation - -CyberStrike includes built-in post-exploitation capabilities across multiple platforms — no external tools required. - -| Platform | Capabilities | -| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **macOS** | Chrome credential extraction, Keychain dumping, keylogging, TCC bypass, GateKeeper bypass, XProtect checks, SSH key extraction, DTrace system tracing | -| **Windows** | Post-exploitation hooks for privilege escalation and persistence | -| **Linux/eBPF** | 29 kernel-level scripts — process execution monitoring, SSL/TLS sniffing, keystroke logging, namespace manipulation detection, rootkit detection, process/file/connection hiding | -| **AWS** | IAM enumeration, S3 exposure, Lambda backdoors, CloudTrail evasion | -| **Azure** | Identity enumeration, storage exposure, function exploitation | -| **Kubernetes** | Pod escape, service account abuse, secret extraction, RBAC exploitation | -| **CI/CD** | Pipeline injection, secret extraction, build artifact manipulation | - -All post-exploitation tools are agent-driven — they execute based on context and findings, not as fixed scripts. - ---- - -### Installation +For a persistent localhost-only service, install +[`contrib/systemd/cyberstrike-web.service`](./contrib/systemd/cyberstrike-web.service) under +`~/.config/systemd/user/`. Create a mode `0600` file at `~/.config/cyberstrike/web.env` containing +`CYBERSTRIKE_SERVER_PASSWORD`, then run: ```bash -# npm (recommended) -npm i -g @cyberstrike-io/cyberstrike@latest - -# bun / pnpm / yarn -bun add -g @cyberstrike-io/cyberstrike@latest +systemctl --user daemon-reload +systemctl --user enable --now cyberstrike-web.service +``` -# macOS (Homebrew) -brew install CyberStrikeus/tap/cyberstrike +#### Remote access and observers -# Windows (Scoop) -scoop install cyberstrike +Keep the service bound to localhost and use an authenticated SSH or Cloudflare tunnel rather than exposing port +`4096` directly. -# Linux / macOS (curl) -curl -fsSL https://cyberstrike.io/install.sh | bash +```bash +export CYBERSTRIKE_SERVER_PASSWORD=your-operator-password +export CYBERSTRIKE_OBSERVER_PASSWORD=your-read-only-password +cyberstrike web --hostname 127.0.0.1 ``` ---- - -### Who Is This For? - -- **Pentesters** — Automate the repetitive parts. Let agents handle recon and initial testing while you focus on the creative attack chains that need human intuition. -- **Bug Bounty Hunters** — Faster reconnaissance, wider coverage, consistent methodology across programs. CyberStrike doesn't get tired at 3am. -- **Security Teams** — Run structured OWASP assessments with reproducible methodology. Get reports that map to standards your compliance team understands. -- **Security Researchers** — Extend CyberStrike with custom agents and MCP servers. The plugin system and MCP protocol make it a platform, not just a tool. +The optional `observer` credential is restricted by server policy. It is suitable for monitoring redacted engagement +state, not for controlling agents or accessing secrets. If user or project configuration prevents startup, +`cyberstrike web --safe` starts recovery mode without those configuration sources; managed administrator policy is +still enforced. --- -### Contributing +### Upstream Project and Contributor Credit -CyberStrike is a personal project with community-sized ambition. I am opening the doors because the future of security should not be designed in a closed room. If you believe agents can do more than chat, tools can do more than sit in silos, and open systems can outperform locked ecosystems, there is a place for your work here. +This fork exists because of the original CyberStrike project and the work of its maintainers and community. -We welcome contributions across: +| Resource | Link | +| ------------------------- | ---------------------------------------------------------------------------------------- | +| **Original repository** | [CyberStrikeus/CyberStrike](https://github.com/CyberStrikeus/CyberStrike) | +| **Upstream contributors** | [Contributor history](https://github.com/CyberStrikeus/CyberStrike/graphs/contributors) | +| **Documentation** | [docs.cyberstrike.io](https://docs.cyberstrike.io) | +| **Website** | [cyberstrike.io](https://cyberstrike.io) | +| **Published package** | [@cyberstrike-io/cyberstrike](https://www.npmjs.com/package/@cyberstrike-io/cyberstrike) | +| **Releases** | [Upstream releases](https://github.com/CyberStrikeus/CyberStrike/releases) | +| **Issues and roadmap** | [Upstream issues](https://github.com/CyberStrikeus/CyberStrike/issues) | +| **Community** | [Discord](https://discord.gg/snunAaHf6U) | -- **Security agents and skills** — New attack methodologies, testing patterns, vulnerability detection -- **MCP servers** — Connect new security tools and data sources -- **Knowledge base** — WSTG, MASTG, PTES, CIS methodology guides -- **Core improvements** — Performance, UX, provider integrations, bug fixes - -Read the [Contributing Guide](./CONTRIBUTING.md) before submitting a PR. All contributions must follow the project's [ethical use policy](./CODE_OF_CONDUCT.md) — CyberStrike is for authorized security testing only. +Git history is intentionally preserved so upstream and fork contributors remain attributed for their work. For +changes intended for the original project, read the [Contributing Guide](./CONTRIBUTING.md) and submit them to the +upstream repository. Fork-specific work should be proposed to +[`spetro511/CyberStrike`](https://github.com/spetro511/CyberStrike) against the appropriate personal-fork branch. --- -### Maintainer & Contact - - - - - - -
- Suren
Sahaydachny
-
- -**Suren Sahaydachny** maintains this public CyberStrike workstream as a personal open-source project focused on AI-first systems, multi-agent orchestration, security automation, and the future of human-machine collaboration. - -- **Email:** [surenpeter511@gmail.com](mailto:surenpeter511@gmail.com) -- **LinkedIn:** [linkedin.com/in/suren-sahaydachny](https://www.linkedin.com/in/suren-sahaydachny) -- **GitHub:** [github.com/spetro511](https://github.com/spetro511) - -
+### Ethical Use -If you are building at the intersection of AI, orchestration, open source, and security—or if you simply see the same future I do—reach out. You never know what a brief conversation can lead to, especially these days. +CyberStrike is intended only for systems you own or are explicitly authorized to test. Users are responsible for +scope, approvals, data handling, tool execution, and compliance with applicable laws and engagement rules. -#### Provenance - -This personal workstream is based on the upstream [CyberStrike](https://github.com/CyberStrikeus/CyberStrike) project. Its Git history, contributors, license, and attribution remain preserved. Personal stewardship by Suren Sahaydachny is additive, not a claim over the work of the broader CyberStrike community. +The agent is **not a security sandbox**. Review commands and active-test previews before approving them, protect +credentials, and do not expose the Web UI directly to untrusted networks. Read the +[Code of Conduct and ethical-use policy](./CODE_OF_CONDUCT.md) and [Security Policy](./SECURITY.md) before operating +the software. --- ### License -[AGPL-3.0-only](./LICENSE) — Free for personal and open-source use. Commercial licensing available via [contact@cyberstrike.io](mailto:contact@cyberstrike.io). - ---- - -### MCP Security Suite +The upstream project and this fork are distributed under the +[GNU Affero General Public License v3.0 only](./LICENSE) (`AGPL-3.0-only`). Fork modifications remain under the same +license and do not replace or weaken upstream copyright or contributor attribution. -CyberStrike is the core platform. These MCP servers extend its capabilities: - -| Project | Domain | Tools | -| ---------------------------------------------------------------------- | --------------------------------------- | ----------------------------------------------------------- | -| **CyberStrike** | **Autonomous offensive security agent** | **13+ agents, 56+ tools, 7,600+ skills, 150+ AI providers** | -| [cloud-audit-mcp](https://github.com/badchars/cloud-audit-mcp) | Cloud security (AWS/Azure/GCP) | 38 tools, 60+ checks | -| [github-security-mcp](https://github.com/badchars/github-security-mcp) | GitHub security posture | 39 tools, 45 checks | -| [cve-mcp](https://github.com/badchars/cve-mcp) | Vulnerability intelligence | 23 tools, 5 sources | -| [osint-mcp](https://github.com/badchars/osint-mcp-server) | OSINT & reconnaissance | 37 tools, 12 sources | +The upstream project also advertises commercial licensing through +[contact@cyberstrike.io](mailto:contact@cyberstrike.io). ---

- Suren Sahaydachny · Email · GitHub · CyberStrike · Docs · Discord -

-

- A personal open-source workstream maintained by Suren Sahaydachny — for security professionals who got tired of being the middleware between their tools. + Original CyberStrike project · + Suren's personal fork · + Upstream docs · + Upstream website

- The next era will not be defined by more software. It will be defined by better orchestration. + Upstream CyberStrike and its contributors are the foundation of this enhanced personal distribution.

diff --git a/packages/cyberstrike/README.md b/packages/cyberstrike/README.md index 54a9622b0d..17093e20e9 100644 --- a/packages/cyberstrike/README.md +++ b/packages/cyberstrike/README.md @@ -180,13 +180,16 @@ Each proxy tester follows a structured methodology: intercept traffic, identify ### Web UI & Remote Access -Run `cyberstrike web` and control your agents, MCP servers, Bolt connections, and vulnerability findings from any browser. Access from anywhere with Cloudflare Tunnel — zero open ports, end-to-end encryption, password-protected API. Your data stays on your machine. +Run `cyberstrike web` and control your agents, MCP servers, Bolt connections, activity lanes, mission posture, Nmap scan history/diffs and topology, structured memory, and vulnerability findings from any browser. Access from anywhere with Cloudflare Tunnel — zero open ports, end-to-end encryption, password-protected API. An optional `observer` credential is restricted server-side to redacted read-only routes. Your data stays on your machine. ```bash export CYBERSTRIKE_SERVER_PASSWORD=your-secure-password +export CYBERSTRIKE_OBSERVER_PASSWORD=your-observer-password # optional read-only API/viewer role cyberstrike web ``` +If user or project configuration prevents startup, run `cyberstrike web --safe` to start recovery mode without those config sources. Managed administrator policy is still enforced. + Use **[app.cyberstrike.io](https://app.cyberstrike.io)** (static page, no backend) or self-host from `packages/app/dist/`. See the [full README](https://github.com/CyberStrikeus/CyberStrike#web-ui--remote-access) for the complete security model. @@ -224,16 +227,9 @@ Bolt is CyberStrike's remote tool server. Deploy it on any VPS, cloud instance, ### MCP Ecosystem -CyberStrike connects to specialized MCP servers that extend its capabilities — **176+ security tools** across 5 domains: - -| Server | Tools | What It Adds | -| ---------------------------------------------------------------------- | ----- | -------------------------------------------------------------------- | -| [cloud-audit-mcp](https://github.com/badchars/cloud-audit-mcp) | 38 | Cloud security audits — 60+ checks across AWS, Azure, GCP | -| [github-security-mcp](https://github.com/badchars/github-security-mcp) | 39 | GitHub security posture — repo, org, actions, secrets, supply chain | -| [cve-mcp](https://github.com/badchars/cve-mcp) | 23 | CVE intelligence — NVD, EPSS, CISA KEV, GitHub Advisory, OSV | -| [osint-mcp](https://github.com/badchars/osint-mcp) | 37 | OSINT recon — Shodan, VirusTotal, SecurityTrails, Censys, DNS, WHOIS | +CyberStrike includes a curated MCP catalog with roughly **724 direct/composite security tools** across 11 default entries. Runnable npm entries are version-pinned; `cloud-audit-mcp` and `hackbrowser-mcp` currently require manual installation from their repositories. -All open source. All installable with `npx`. Plug them into CyberStrike or use them standalone with any MCP-compatible client. +The catalog covers GitHub posture (39), CVE intelligence (41), OSINT (37), cloud audit (38), HackBrowser (39), darknet intelligence (66), DNS security (103), supply-chain analysis (7 composite tools / 90 techniques), MCP security scanning (55), steganography (128), and satellite/GEOINT (171). Optional owner-maintained entries add wireless security, LOLBin intelligence, and fingerprinting. --- @@ -272,6 +268,25 @@ scoop install cyberstrike curl -fsSL https://cyberstrike.io/install.sh | bash ``` +#### Build and deploy on Kali/Linux from source + +Build the matching Linux binary and web bundle with the repository-pinned Bun version: + +```bash +bun install --frozen-lockfile +bun run --cwd packages/app build +CYBERSTRIKE_BUILD_TARGET=linux-x64 bun run --cwd packages/cyberstrike script/build.ts +./install --binary packages/cyberstrike/dist/cyberstrike-linux-x64/bin/cyberstrike + +install -d "${XDG_DATA_HOME:-$HOME/.local/share}/cyberstrike/web" +cp -R packages/app/dist/. "${XDG_DATA_HOME:-$HOME/.local/share}/cyberstrike/web/" +CYBERSTRIKE_SERVER_PASSWORD=change-me cyberstrike web --hostname 127.0.0.1 +``` + +The installer also copies the sibling HackBrowser worker. Use `linux-x64-baseline` for x64 CPUs without AVX2 and a `*-musl` target for musl-based distributions. + +For a persistent localhost-only deployment, install [`contrib/systemd/cyberstrike-web.service`](../../contrib/systemd/cyberstrike-web.service), set `CYBERSTRIKE_SERVER_PASSWORD` in a mode `0600` `~/.config/cyberstrike/web.env`, and enable the unit with `systemctl --user enable --now cyberstrike-web.service`. + --- ### Who Is This For?