diff --git a/README.md b/README.md index 06296f94..2f28f928 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,14 @@ CyberStrikeAI is an **AI-native security testing platform** built in Go. It inte - 📱 **Chatbot**: DingTalk and Lark (Feishu) long-lived connections so you can talk to CyberStrikeAI from mobile (see [Robot / Chatbot guide](docs/robot_en.md) for setup and commands) - 🐚 **WebShell management**: Add and manage WebShell connections (e.g. IceSword/AntSword compatible), use a virtual terminal for command execution, a built-in file manager for file operations, and an AI assistant tab that orchestrates tests and keeps per-connection conversation history; supports PHP, ASP, ASPX, JSP and custom shell types with configurable request method and command parameter. +## Plugins + +CyberStrikeAI includes optional integrations under `plugins/`. + +- **Burp Suite extension**: `plugins/burp-suite/cyberstrikeai-burp-extension/` + Build output: `plugins/burp-suite/cyberstrikeai-burp-extension/dist/cyberstrikeai-burp-extension.jar` + Docs: `plugins/burp-suite/cyberstrikeai-burp-extension/README.md` + ## Tool Overview CyberStrikeAI ships with 100+ curated tools covering the whole kill chain: diff --git a/README_CN.md b/README_CN.md index eeb0fc41..36bd1dfe 100644 --- a/README_CN.md +++ b/README_CN.md @@ -96,6 +96,14 @@ CyberStrikeAI 是一款 **AI 原生安全测试平台**,基于 Go 构建,集 - 📱 **机器人**:支持钉钉、飞书长连接,在手机端与 CyberStrikeAI 对话(配置与命令详见 [机器人使用说明](docs/robot.md)) - 🐚 **WebShell 管理**:添加与管理 WebShell 连接(兼容冰蝎/蚁剑等),通过虚拟终端执行命令、内置文件管理进行文件操作,并提供按连接维度保存历史的 AI 助手标签页;支持 PHP/ASP/ASPX/JSP 及自定义类型,可配置请求方法与命令参数。 +## 插件(Plugins) + +可选集成在 `plugins/` 目录下。 + +- **Burp Suite 插件**:`plugins/burp-suite/cyberstrikeai-burp-extension/` + 构建产物:`plugins/burp-suite/cyberstrikeai-burp-extension/dist/cyberstrikeai-burp-extension.jar` + 说明文档:`plugins/burp-suite/cyberstrikeai-burp-extension/README.zh-CN.md` + ## 工具概览 系统预置 100+ 渗透/攻防工具,覆盖完整攻击链: diff --git a/plugins/README.md b/plugins/README.md new file mode 100644 index 00000000..a33e6f2a --- /dev/null +++ b/plugins/README.md @@ -0,0 +1,12 @@ +## Plugins + +This directory contains optional plugins/extensions that integrate CyberStrikeAI with other tools. + +- `burp-suite/`: Burp Suite extensions + +### Burp Suite Extension + +- **Path**: `plugins/burp-suite/cyberstrikeai-burp-extension/` +- **Build output**: `plugins/burp-suite/cyberstrikeai-burp-extension/dist/cyberstrikeai-burp-extension.jar` +- **Docs**: see the plugin folder `README.md` / `README.zh-CN.md` + diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/README.md b/plugins/burp-suite/cyberstrikeai-burp-extension/README.md new file mode 100644 index 00000000..321fd637 --- /dev/null +++ b/plugins/burp-suite/cyberstrikeai-burp-extension/README.md @@ -0,0 +1,68 @@ +## CyberStrikeAI Burp Suite Extension + +中文说明见:`README.zh-CN.md` + +### What it does + +- Configure **Host / Port / Password** and choose **Single-Agent** or **Multi-Agent** +- Click **Validate** to login (`POST /api/auth/login`) and verify token (`GET /api/auth/validate`) +- Right-click any HTTP message in Burp and send it to CyberStrikeAI for **streaming web pentest** +- Keep a **test history sidebar** (searchable) so you can revisit previous runs +- Output is split into **collapsible Progress** + **Final Response** (Markdown rendering supported) +- View captured **Request / Response** for each run +- **Stop** a running task (calls `/api/agent-loop/cancel` once `conversationId` is available) + +### Build + +Requirements: + +- JDK 11+ +- Maven (recommended) OR Burp Extender API jar (offline mode) + +#### Option A (recommended): Maven build (no need to locate Burp) + +```bash +cd plugins/burp-suite/cyberstrikeai-burp-extension +./build-mvn.sh +``` + +Output: + +- `dist/cyberstrikeai-burp-extension.jar` + +#### Option B: Offline build with `build.sh` (needs Burp API jar) + +1) Create `lib/` and copy Burp's API jar into it: + +```bash +mkdir -p lib +# copy from your Burp installation, for example: +# cp "/path/to/burp-extender-api.jar" lib/ +``` + +2) Build: + +```bash +cd plugins/burp-suite/cyberstrikeai-burp-extension +./build.sh +``` + +Output: + +- `dist/cyberstrikeai-burp-extension.jar` + +#### Option C: Gradle (optional) + +If you already have Gradle available, you can still use `build.gradle` to build. + +### Load in Burp Suite + +- Burp Suite → **Extensions** → **Installed** → **Add** +- Extension type: **Java** +- Select the jar above + +### Notes + +- This extension connects to your CyberStrikeAI server (default is `http://127.0.0.1:8080`). +- It uses **Bearer Token** authentication obtained from the configured password. + diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/README.zh-CN.md b/plugins/burp-suite/cyberstrikeai-burp-extension/README.zh-CN.md new file mode 100644 index 00000000..d50a96fe --- /dev/null +++ b/plugins/burp-suite/cyberstrikeai-burp-extension/README.zh-CN.md @@ -0,0 +1,108 @@ +## CyberStrikeAI Burp Suite 插件(中文说明) + +### 功能概述 + +- 在 Burp 的 `CyberStrikeAI` 标签页中配置 **Host、端口、密码、单/多 Agent** +- 点击 **Validate(验证)**: + - 调用 `POST /api/auth/login` 用密码换取 Token + - 调用 `GET /api/auth/validate` 校验 Token + - 验证通过后 Token 会保存在插件内存中(本次 Burp 会话有效) +- 右键任意 HTTP 请求包 → **Send to CyberStrikeAI (stream test)**: + - 将该 HTTP 请求(含 headers/body;若存在响应则附带截断片段)发送到 CyberStrikeAI + - 以 **SSE 流式**接收返回内容,并在标签页中实时展示 + - 单 Agent:`POST /api/agent-loop/stream` + - 多 Agent:`POST /api/multi-agent/stream`(需要服务端启用 `multi_agent.enabled: true`) +- **测试历史侧边栏(可搜索)**:每次发送都会新增一条记录,方便回看与对比 +- **Output 分区**:`Progress`(可折叠)+ `Final Response`(主区域) +- **Markdown 渲染**:最终输出可在 Output 主区域渲染为富文本(可开关) +- **Request / Response 回看**:右侧 Tab 可直接查看该次捕获到的原始请求/响应 +- **Stop 取消**:任务创建会话后可调用 `/api/agent-loop/cancel` 停止当前会话任务 + +### 编译(不依赖 Gradle/Maven,推荐) + +> 给普通用户:你们应当直接发 **编译好的 jar**,用户在 Burp 里加载即可,**不需要编译**。 + +#### 方式 A(推荐,通用):用 Maven 编译(不需要知道 Burp 在哪) + +适合:开发者/CI 打包一次,发布给所有用户使用。 + +环境要求: + +- JDK 11+ +- Maven(会从 Maven Central 下载 `burp-extender-api` 依赖) + +编译打包: + +```bash +cd plugins/burp-suite/cyberstrikeai-burp-extension +./build-mvn.sh +``` + +产物: + +- `dist/cyberstrikeai-burp-extension.jar` + +#### 方式 B(离线):纯 JDK 编译(需要 Burp 的 API jar) + +- JDK 11+ +- Burp Extender API 的 jar(来自你的 Burp 安装目录) + +#### 步骤 + +1) 在插件目录创建 `lib/`,并把 `burp-extender-api.jar` 复制进去: + +```bash +cd plugins/burp-suite/cyberstrikeai-burp-extension +mkdir -p lib +# 复制 Burp 自带的 API jar 到这里,例如: +# cp "/path/to/burp-extender-api.jar" lib/ +``` + +2) 一键编译打包: + +```bash +cd plugins/burp-suite/cyberstrikeai-burp-extension +./build.sh +``` + +产物: + +- `dist/cyberstrikeai-burp-extension.jar` + +### 在 Burp Suite 中加载 + +- Burp Suite → **Extensions** → **Installed** → **Add** +- Extension type:**Java** +- 选择 `dist/cyberstrikeai-burp-extension.jar` + +### 使用方法 + +1) 打开 Burp 顶部标签页 `CyberStrikeAI` +2) 填写: + - **Host**:例如 `127.0.0.1` + - **Port**:例如 `8080` + - **Password**:你的 CyberStrikeAI 登录密码(对应服务端 `config.yaml` 的 `auth.password`) + - **Agent mode**:选择 `Single Agent` 或 `Multi Agent` +3) 点击 **Validate** + - 成功:状态显示 `OK (token saved)` + - 失败:状态会显示错误原因(例如密码错误、服务不可达、401/403 等) +4) 在 Burp 的 Proxy/HTTP history/Repeater 等列表中选中一条 HTTP 包 +5) 右键 → **Send to CyberStrikeAI (stream test)** +6) 每次发送后会在 `CyberStrikeAI` 标签页左侧显示一个“测试记录”(请求标题 + 单/多 Agent + 状态);点击对应记录即可在右侧查看该次的流式输出结果 + +### 常见问题(排错) + +- **Validate 失败 / 401** + - 确认密码是否正确(服务端 `auth.password`) + - 确认 IP/端口是否能访问(例如浏览器能打开 `http://IP:PORT/`) + - 若服务器启用了反向代理/HTTPS,需要把插件里 baseUrl 改成对应协议与端口(当前插件默认使用 `http://`) + +- **选择 Multi Agent 后提示“多代理未启用”** + - 服务端需要开启:`config.yaml` 中 `multi_agent.enabled: true` + - 并重启服务(或按你们项目的动态 apply 配置流程启用) + +- **右键发送后无流式输出** + - 先确认已 Validate(拿到 Token) + - 确认 Burp 能访问到 CyberStrikeAI(网络/代理/防火墙) + - 服务端的流式端点为 SSE,插件会解析 `data: {json}` 行;如果中间件缓冲可能影响实时性 + diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/build-mvn.sh b/plugins/burp-suite/cyberstrikeai-burp-extension/build-mvn.sh new file mode 100644 index 00000000..075d8895 --- /dev/null +++ b/plugins/burp-suite/cyberstrikeai-burp-extension/build-mvn.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DIST_DIR="$ROOT_DIR/dist" + +MVN_BIN="" +if command -v mvn >/dev/null 2>&1; then + MVN_BIN="mvn" +else + # Auto-provision Maven for developer convenience. + # This is only used to build the jar once in CI/dev; Burp users don't need to run this. + MAVEN_VERSION="3.9.6" + BASE_DIR="${HOME}/.cache/cyberstrikeai-burp-extension" + MAVEN_DIR="$BASE_DIR/apache-maven-$MAVEN_VERSION" + MAVEN_TGZ="$BASE_DIR/apache-maven-$MAVEN_VERSION-bin.tar.gz" + MAVEN_URL="https://archive.apache.org/dist/maven/maven-3/$MAVEN_VERSION/binaries/apache-maven-$MAVEN_VERSION-bin.tar.gz" + + if [[ -x "$MAVEN_DIR/bin/mvn" ]]; then + MVN_BIN="$MAVEN_DIR/bin/mvn" + else + echo "[*] Maven not found. Downloading Maven $MAVEN_VERSION ..." + mkdir -p "$BASE_DIR" + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$MAVEN_URL" -o "$MAVEN_TGZ" + elif command -v wget >/dev/null 2>&1; then + wget -q "$MAVEN_URL" -O "$MAVEN_TGZ" + else + echo "Missing: curl/wget (needed to download Maven)." + exit 1 + fi + tar -xzf "$MAVEN_TGZ" -C "$BASE_DIR" + MVN_BIN="$MAVEN_DIR/bin/mvn" + fi +fi + +rm -rf "$DIST_DIR" +mkdir -p "$DIST_DIR" + +echo "[*] Building with Maven (downloads Burp API from Maven Central)..." +(cd "$ROOT_DIR" && "$MVN_BIN" -q -DskipTests package) + +cp "$ROOT_DIR/target/cyberstrikeai-burp-extension-1.0.0.jar" "$DIST_DIR/cyberstrikeai-burp-extension.jar" +echo "[+] Done: $DIST_DIR/cyberstrikeai-burp-extension.jar" + diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/build.gradle b/plugins/burp-suite/cyberstrikeai-burp-extension/build.gradle new file mode 100644 index 00000000..4bd43d59 --- /dev/null +++ b/plugins/burp-suite/cyberstrikeai-burp-extension/build.gradle @@ -0,0 +1,45 @@ +plugins { + id 'java' + id 'com.github.johnrengelman.shadow' version '8.1.1' +} + +group = 'ai.cyberstrike' +version = '1.0.0' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(11) + } +} + +repositories { + mavenCentral() +} + +dependencies { + // Burp Extender API (legacy). Burp will provide the interfaces at runtime, but we compile against it. + implementation 'net.portswigger.burp.extender:burp-extender-api:2.3' + + // JSON parsing for SSE payloads. + implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.2' +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + options.release = 11 +} + +jar { + manifest { + attributes( + 'Main-Class': 'burp.BurpExtender' + ) + } +} + +shadowJar { + archiveBaseName.set('cyberstrikeai-burp-extension') + archiveClassifier.set('all') + archiveVersion.set('') +} + diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/build.sh b/plugins/burp-suite/cyberstrikeai-burp-extension/build.sh new file mode 100644 index 00000000..a3e298b5 --- /dev/null +++ b/plugins/burp-suite/cyberstrikeai-burp-extension/build.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LIB_DIR="$ROOT_DIR/lib" +DIST_DIR="$ROOT_DIR/dist" +BUILD_DIR="$ROOT_DIR/.build" + +API_JAR="$LIB_DIR/burp-extender-api.jar" + +if [[ ! -f "$API_JAR" ]]; then + echo "Missing: $API_JAR" + echo "Please copy Burp's burp-extender-api.jar into plugins/burp-suite/cyberstrikeai-burp-extension/lib/" + exit 1 +fi + +rm -rf "$BUILD_DIR" "$DIST_DIR" +mkdir -p "$BUILD_DIR" "$DIST_DIR" + +SRC_FILES=$(find "$ROOT_DIR/src/main/java" -name "*.java") + +echo "[*] Compiling..." +javac \ + -encoding UTF-8 \ + --release 11 \ + -cp "$API_JAR" \ + -d "$BUILD_DIR" \ + $SRC_FILES + +echo "[*] Packaging..." +JAR_OUT="$DIST_DIR/cyberstrikeai-burp-extension.jar" +jar --create --file "$JAR_OUT" --main-class burp.BurpExtender -C "$BUILD_DIR" . + +echo "[+] Done: $JAR_OUT" + diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/dist/cyberstrikeai-burp-extension.jar b/plugins/burp-suite/cyberstrikeai-burp-extension/dist/cyberstrikeai-burp-extension.jar new file mode 100644 index 00000000..ae3e6ead Binary files /dev/null and b/plugins/burp-suite/cyberstrikeai-burp-extension/dist/cyberstrikeai-burp-extension.jar differ diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/pom.xml b/plugins/burp-suite/cyberstrikeai-burp-extension/pom.xml new file mode 100644 index 00000000..d54d5264 --- /dev/null +++ b/plugins/burp-suite/cyberstrikeai-burp-extension/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + ai.cyberstrike + cyberstrikeai-burp-extension + 1.0.0 + CyberStrikeAI Burp Suite Extension + + + 11 + UTF-8 + + + + + + net.portswigger.burp.extender + burp-extender-api + 2.3 + provided + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + + burp.BurpExtender + + + + + + + + diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/settings.gradle b/plugins/burp-suite/cyberstrikeai-burp-extension/settings.gradle new file mode 100644 index 00000000..6a56ea57 --- /dev/null +++ b/plugins/burp-suite/cyberstrikeai-burp-extension/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = "cyberstrikeai-burp-extension" + diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/src/main/java/burp/BurpExtender.java b/plugins/burp-suite/cyberstrikeai-burp-extension/src/main/java/burp/BurpExtender.java new file mode 100644 index 00000000..7ef116e6 --- /dev/null +++ b/plugins/burp-suite/cyberstrikeai-burp-extension/src/main/java/burp/BurpExtender.java @@ -0,0 +1,138 @@ +package burp; + +import javax.swing.*; +import java.util.ArrayList; +import java.util.List; + +public class BurpExtender implements IBurpExtender, IContextMenuFactory { + private IBurpExtenderCallbacks callbacks; + private IExtensionHelpers helpers; + + private CyberStrikeAITab tab; + private final CyberStrikeAIClient client = new CyberStrikeAIClient(); + + @Override + public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks) { + this.callbacks = callbacks; + this.helpers = callbacks.getHelpers(); + + callbacks.setExtensionName("CyberStrikeAI Extension"); + + this.tab = new CyberStrikeAITab(); + callbacks.addSuiteTab(tab); + + callbacks.registerContextMenuFactory(this); + + callbacks.printOutput("CyberStrikeAI extension loaded."); + } + + @Override + public List createMenuItems(IContextMenuInvocation invocation) { + List items = new ArrayList<>(); + + JMenuItem sendItem = new JMenuItem("Send to CyberStrikeAI (stream test)"); + sendItem.addActionListener(e -> { + IHttpRequestResponse[] selected = invocation.getSelectedMessages(); + if (selected == null || selected.length == 0) { + return; + } + + CyberStrikeAIClient.Config cfg = tab.currentConfig(); + String token = tab.getToken(); + if (token == null || token.trim().isEmpty()) { + JOptionPane.showMessageDialog(tab.getUiComponent(), + "Please click Validate first to obtain a token.", + "CyberStrikeAI", JOptionPane.WARNING_MESSAGE); + return; + } + + String prompt = HttpMessageFormatter.toPrompt(helpers, selected[0]); + String title = HttpMessageFormatter.getRequestTitle(helpers, selected[0]); + String agentModeStr = (cfg.agentMode == CyberStrikeAIClient.AgentMode.MULTI) ? "Multi Agent" : "Single Agent"; + String runId = tab.startNewRun(title, agentModeStr, selected[0]); + tab.appendProgressToRun(runId, "\n[server] " + cfg.baseUrl + "\n\n"); + + client.streamTest(cfg, token, prompt, new CyberStrikeAIClient.StreamListener() { + @Override + public void onEvent(String type, String message, String rawJson) { + if (type == null) type = ""; + switch (type) { + case "response_delta": + case "eino_agent_reply_stream_delta": + // delta chunk (content only) + tab.appendFinalToRun(runId, message); + break; + case "response": + // final response (full) + tab.appendFinalToRun(runId, "\n\n--- Final Response ---\n"); + tab.appendFinalToRun(runId, message); + tab.setFinalResponse(runId, message); + break; + case "progress": + tab.appendProgressToRun(runId, "\n[progress] " + message + "\n"); + tab.setRunStatus(runId, "running"); + break; + case "cancelled": + tab.appendProgressToRun(runId, "\n[cancelled] " + message + "\n"); + tab.setRunStatus(runId, "cancelled"); + break; + case "error": + tab.appendProgressToRun(runId, "\n[error] " + message + "\n"); + tab.setRunStatus(runId, "error"); + break; + case "thinking_stream_start": + case "thinking_stream_delta": + case "tool_call": + case "tool_result": + case "tool_result_delta": + // debug; hide by default + if (tab.isShowDebugEvents() && message != null && !message.isEmpty()) { + tab.appendProgressToRun(runId, "\n[" + type + "] " + message + "\n"); + } + break; + case "conversation": + // Capture conversationId for stop/cancel. + if (rawJson != null) { + String convId = SimpleJson.extractStringField(rawJson, "conversationId"); + if (convId != null && !convId.trim().isEmpty()) { + tab.setRunConversationId(runId, convId); + } + } + if (tab.isShowDebugEvents() && message != null && !message.isEmpty()) { + tab.appendProgressToRun(runId, "\n[" + type + "] " + message + "\n"); + } + break; + case "done": + // handled in onDone too + break; + default: + if (tab.isShowDebugEvents() && message != null && !message.isEmpty()) { + tab.appendProgressToRun(runId, "\n[" + type + "] " + message + "\n"); + } + break; + } + } + + @Override + public void onError(String message, Exception e) { + tab.appendProgressToRun(runId, "\n[error] " + message + "\n"); + tab.setRunStatus(runId, "error"); + callbacks.printError("CyberStrikeAI stream error: " + message); + if (e != null) { + callbacks.printError(e.toString()); + } + } + + @Override + public void onDone() { + tab.appendProgressToRun(runId, "\n\n[done]\n"); + tab.setRunStatus(runId, "done"); + } + }); + }); + + items.add(sendItem); + return items; + } +} + diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/src/main/java/burp/CyberStrikeAIClient.java b/plugins/burp-suite/cyberstrikeai-burp-extension/src/main/java/burp/CyberStrikeAIClient.java new file mode 100644 index 00000000..c7713f58 --- /dev/null +++ b/plugins/burp-suite/cyberstrikeai-burp-extension/src/main/java/burp/CyberStrikeAIClient.java @@ -0,0 +1,234 @@ +package burp; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +final class CyberStrikeAIClient { + + static final class Config { + final String baseUrl; // e.g. http://127.0.0.1:8080 + final String password; + final AgentMode agentMode; + + Config(String baseUrl, String password, AgentMode agentMode) { + this.baseUrl = baseUrl; + this.password = password; + this.agentMode = agentMode; + } + } + + enum AgentMode { + SINGLE, + MULTI + } + + interface StreamListener { + void onEvent(String type, String message, String rawJson); + void onError(String message, Exception e); + void onDone(); + } + + String loginAndValidate(Config cfg) throws IOException { + String token = login(cfg.baseUrl, cfg.password); + validate(cfg.baseUrl, token); + return token; + } + + private String login(String baseUrl, String password) throws IOException { + URL url = new URL(baseUrl + "/api/auth/login"); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setRequestProperty("Accept", "application/json"); + String body = "{\"password\":\"" + escapeJson(password) + "\"}"; + try (OutputStream os = conn.getOutputStream()) { + os.write(body.getBytes(StandardCharsets.UTF_8)); + } + int code = conn.getResponseCode(); + String contentType = conn.getHeaderField("Content-Type"); + String resp = readAll(code >= 200 && code < 300 ? conn.getInputStream() : conn.getErrorStream()); + + // Friendly diagnosis: HTML usually means wrong host/port (e.g., hit Burp UI/proxy page). + if (looksLikeHtml(resp) || (contentType != null && contentType.toLowerCase().contains("text/html"))) { + throw new IOException("Login failed: server returned HTML, not API JSON. Check IP/Port and ensure you point to CyberStrikeAI backend."); + } + + String serverError = SimpleJson.extractStringField(resp, "error"); + if (code < 200 || code >= 300) { + if (!serverError.isEmpty()) { + throw new IOException("Login failed (" + code + "): " + serverError); + } + throw new IOException("Login failed (" + code + ")."); + } + + if (!serverError.isEmpty()) { + throw new IOException("Login failed: " + serverError); + } + + String token = SimpleJson.extractStringField(resp, "token"); + if (token.isEmpty()) { + throw new IOException("Login response missing token. Check backend address and credentials."); + } + return token; + } + + private void validate(String baseUrl, String token) throws IOException { + URL url = new URL(baseUrl + "/api/auth/validate"); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("Authorization", "Bearer " + token); + int code = conn.getResponseCode(); + String resp = readAll(code >= 200 && code < 300 ? conn.getInputStream() : conn.getErrorStream()); + if (code < 200 || code >= 300) { + throw new IOException("Validate failed (" + code + "): " + resp); + } + } + + void streamTest(Config cfg, String token, String message, StreamListener listener) { + String path = (cfg.agentMode == AgentMode.MULTI) ? "/api/multi-agent/stream" : "/api/agent-loop/stream"; + String urlStr = cfg.baseUrl + path; + + Map payload = new HashMap<>(); + payload.put("message", message); + payload.put("conversationId", ""); + payload.put("role", ""); + + new Thread(() -> { + HttpURLConnection conn = null; + try { + URL url = new URL(urlStr); + conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setRequestProperty("Accept", "text/event-stream"); + conn.setRequestProperty("Authorization", "Bearer " + token); + + String body = toJson(payload); + try (OutputStream os = conn.getOutputStream()) { + os.write(body.getBytes(StandardCharsets.UTF_8)); + } + + int code = conn.getResponseCode(); + InputStream is = (code >= 200 && code < 300) ? conn.getInputStream() : conn.getErrorStream(); + if (is == null) { + throw new IOException("No response body (HTTP " + code + ")"); + } + + try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + // SSE format: "data: {json}" + if (line.startsWith("data:")) { + String json = line.substring("data:".length()).trim(); + if (!json.isEmpty()) { + String type = SimpleJson.extractStringField(json, "type"); + String msg = SimpleJson.extractStringField(json, "message"); + listener.onEvent(type, msg, json); + if ("done".equals(type)) { + break; + } + } + } + } + } + listener.onDone(); + } catch (Exception e) { + listener.onError(e.getMessage(), e); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + }, "CyberStrikeAI-Stream").start(); + } + + void cancelByConversationId(String baseUrl, String token, String conversationId) throws IOException { + if (conversationId == null || conversationId.trim().isEmpty()) { + throw new IOException("Missing conversationId."); + } + URL url = new URL(baseUrl + "/api/agent-loop/cancel"); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setRequestProperty("Accept", "application/json"); + conn.setRequestProperty("Authorization", "Bearer " + token); + + String body = "{\"conversationId\":\"" + escapeJson(conversationId.trim()) + "\"}"; + try (OutputStream os = conn.getOutputStream()) { + os.write(body.getBytes(StandardCharsets.UTF_8)); + } + + int code = conn.getResponseCode(); + String resp = readAll(code >= 200 && code < 300 ? conn.getInputStream() : conn.getErrorStream()); + if (code < 200 || code >= 300) { + String serverError = SimpleJson.extractStringField(resp, "error"); + if (!serverError.isEmpty()) { + throw new IOException("Cancel failed (" + code + "): " + serverError); + } + throw new IOException("Cancel failed (" + code + ")."); + } + } + + private static String toJson(Map payload) { + String message = payload.get("message") != null ? String.valueOf(payload.get("message")) : ""; + String conversationId = payload.get("conversationId") != null ? String.valueOf(payload.get("conversationId")) : ""; + String role = payload.get("role") != null ? String.valueOf(payload.get("role")) : ""; + return "{" + + "\"message\":\"" + escapeJson(message) + "\"," + + "\"conversationId\":\"" + escapeJson(conversationId) + "\"," + + "\"role\":\"" + escapeJson(role) + "\"" + + "}"; + } + + private static String escapeJson(String s) { + if (s == null) return ""; + StringBuilder sb = new StringBuilder(s.length() + 16); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '\\': sb.append("\\\\"); break; + case '"': sb.append("\\\""); break; + case '\n': sb.append("\\n"); break; + case '\r': sb.append("\\r"); break; + case '\t': sb.append("\\t"); break; + default: + if (c < 0x20) { + sb.append(String.format("\\u%04x", (int) c)); + } else { + sb.append(c); + } + } + } + return sb.toString(); + } + + private static String readAll(InputStream is) throws IOException { + if (is == null) return ""; + try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + sb.append(line).append('\n'); + } + return sb.toString().trim(); + } + } + + private static boolean looksLikeHtml(String s) { + if (s == null) return false; + String t = s.trim().toLowerCase(); + return t.startsWith("") || t.contains(" agentModeBox = new JComboBox<>(new String[]{"Single Agent", "Multi Agent"}); + private final JButton validateButton = new JButton("Validate"); + private final JButton clearButton = new JButton("Clear Output"); + private final JButton stopButton = new JButton("Stop"); + private final JButton copyButton = new JButton("Copy"); + private final JButton clearAllButton = new JButton("Clear All"); + private final JLabel statusLabel = new JLabel("Not validated"); + private final JCheckBox showDebugEventsBox = new JCheckBox("Show debug events", false); + private final JCheckBox renderMarkdownBox = new JCheckBox("Render Markdown", true); + + private final JTextArea progressArea = new JTextArea(); + private final JTextArea finalRawArea = new JTextArea(); // raw final stream / final response + private final JEditorPane markdownPane = new JEditorPane("text/html", ""); + private final CardLayout outputCardsLayout = new CardLayout(); + private final JPanel outputCards = new JPanel(outputCardsLayout); + private final JPanel outputRoot = new JPanel(new BorderLayout()); + private final JPanel progressContainer = new JPanel(new CardLayout()); + private final JToggleButton progressToggle = new JToggleButton("Progress ▾", true); + private final JTextArea requestArea = new JTextArea(); + private final JTextArea responseArea = new JTextArea(); + private final JTabbedPane rightTabs = new JTabbedPane(); + + private final CyberStrikeAIClient client = new CyberStrikeAIClient(); + private final AtomicReference tokenRef = new AtomicReference<>(""); + + private final DefaultListModel testListModel = new DefaultListModel<>(); + private final JList testList = new JList<>(testListModel); + private final DefaultListModel filteredListModel = new DefaultListModel<>(); + private final JList filteredList = new JList<>(filteredListModel); + private final JTextField searchField = new JTextField(); + private final Map runs = new HashMap<>(); + private final Map runIdToIndex = new HashMap<>(); + private final AtomicInteger runSeq = new AtomicInteger(1); + private String selectedRunId = null; + + private static final class TestRun { + final String id; + final String title; + final String agentMode; + final StringBuilder buffer = new StringBuilder(); + final StringBuilder progressBuffer = new StringBuilder(); + final StringBuilder finalBuffer = new StringBuilder(); + String status; + String conversationId; + String requestRaw; + String responseRaw; + String finalResponse; + + TestRun(String id, String title, String agentMode) { + this.id = id; + this.title = title; + this.agentMode = agentMode; + this.status = "running"; + this.conversationId = ""; + this.requestRaw = ""; + this.responseRaw = ""; + this.finalResponse = ""; + } + + @Override + public String toString() { + return id; + } + } + + CyberStrikeAITab() { + root.add(buildConfigPanel(), BorderLayout.NORTH); + root.add(buildMainPane(), BorderLayout.CENTER); + wireActions(); + } + + private JComponent buildConfigPanel() { + // Best-practice toolbar layout: + // Row 1 = connection settings + // Row 2 = run controls + view options + JPanel rootPanel = new JPanel(); + rootPanel.setLayout(new BoxLayout(rootPanel, BoxLayout.Y_AXIS)); + rootPanel.setBorder(BorderFactory.createEmptyBorder(4, 6, 4, 6)); + + hostField.setColumns(14); + portField.setColumns(6); + passwordField.setColumns(12); + agentModeBox.setPreferredSize(new Dimension(160, agentModeBox.getPreferredSize().height)); + + JPanel row1 = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 2)); + row1.add(new JLabel("Host")); + row1.add(hostField); + row1.add(new JLabel("Port")); + row1.add(portField); + row1.add(new JLabel("Password")); + row1.add(passwordField); + row1.add(validateButton); + row1.add(statusLabel); + + JPanel row2 = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 2)); + row2.add(new JLabel("Agent")); + row2.add(agentModeBox); + row2.add(stopButton); + row2.add(copyButton); + row2.add(clearButton); + row2.add(showDebugEventsBox); + row2.add(renderMarkdownBox); + + rootPanel.add(row1); + rootPanel.add(row2); + return rootPanel; + } + + private JComponent buildMainPane() { + JPanel sidebarPanel = buildSidebarPanel(); + JComponent right = buildRightPanel(); + + JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, sidebarPanel, right); + split.setResizeWeight(0.25); + split.setBorder(null); + return split; + } + + private JPanel buildSidebarPanel() { + JPanel p = new JPanel(new BorderLayout()); + filteredList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + filteredList.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); + filteredList.setCellRenderer(new TestRunCellRenderer()); + filteredList.addListSelectionListener(e -> { + if (!e.getValueIsAdjusting()) { + String id = getSelectedRunIdFromList(); + if (id != null) { + setLogAreaToRun(id); + } + } + }); + + JLabel title = new JLabel("Test History"); + title.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8)); + + JPanel top = new JPanel(new BorderLayout(8, 6)); + top.setBorder(BorderFactory.createEmptyBorder(0, 8, 0, 8)); + top.add(title, BorderLayout.NORTH); + searchField.setToolTipText("Search runs (title)"); + top.add(searchField, BorderLayout.SOUTH); + + JScrollPane sp = new JScrollPane(filteredList); + sp.setBorder(BorderFactory.createTitledBorder("Runs")); + + clearAllButton.addActionListener(e -> clearAllRuns()); + JPanel bottom = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 6)); + bottom.add(clearAllButton); + + p.add(top, BorderLayout.NORTH); + p.add(sp, BorderLayout.CENTER); + p.add(bottom, BorderLayout.SOUTH); + p.setPreferredSize(new Dimension(320, 200)); + return p; + } + + private JComponent buildRightPanel() { + configureTextArea(progressArea, true); + configureTextArea(finalRawArea, true); + markdownPane.setEditable(false); + markdownPane.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE); + markdownPane.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); + markdownPane.setOpaque(true); + markdownPane.setBackground(Color.WHITE); + + configureTextArea(requestArea, false); + configureTextArea(responseArea, false); + + outputCards.add(new JScrollPane(finalRawArea), "raw"); + outputCards.add(new JScrollPane(markdownPane), "md"); + + outputRoot.add(buildOutputHeader(), BorderLayout.NORTH); + outputRoot.add(buildOutputBody(), BorderLayout.CENTER); + + rightTabs.addTab("Output", outputRoot); + rightTabs.addTab("Request", new JScrollPane(requestArea)); + rightTabs.addTab("Response", new JScrollPane(responseArea)); + return rightTabs; + } + + private JComponent buildOutputHeader() { + JPanel header = new JPanel(new BorderLayout(8, 0)); + header.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8)); + + JPanel left = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); + left.add(progressToggle); + header.add(left, BorderLayout.WEST); + + return header; + } + + private JComponent buildOutputBody() { + JScrollPane progressScroll = new JScrollPane(progressArea); + progressScroll.setBorder(BorderFactory.createTitledBorder("Progress")); + progressScroll.getVerticalScrollBar().setUnitIncrement(16); + + JPanel empty = new JPanel(); + progressContainer.add(progressScroll, "show"); + progressContainer.add(empty, "hide"); + ((CardLayout) progressContainer.getLayout()).show(progressContainer, "show"); + + JPanel finalPanel = new JPanel(new BorderLayout()); + finalPanel.add(outputCards, BorderLayout.CENTER); + finalPanel.setBorder(BorderFactory.createTitledBorder("Final Response")); + + JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT, progressContainer, finalPanel); + split.setResizeWeight(0.15); + split.setBorder(null); + split.setDividerSize(6); + + final int[] lastDividerLocation = new int[]{140}; // sensible default + + progressToggle.addActionListener(e -> { + boolean show = progressToggle.isSelected(); + progressToggle.setText(show ? "Progress ▾" : "Progress ▸"); + CardLayout cl = (CardLayout) progressContainer.getLayout(); + cl.show(progressContainer, show ? "show" : "hide"); + if (!show) { + int current = split.getDividerLocation(); + if (current > 0) { + lastDividerLocation[0] = current; + } + split.setDividerLocation(0); + split.setDividerSize(0); + } else { + split.setDividerSize(6); + // Restore previous divider location (or fallback to 20% of height) + int restore = lastDividerLocation[0]; + if (restore <= 0) { + int h = split.getHeight(); + restore = (h > 0) ? Math.max(80, (int) (h * 0.2)) : 140; + } + split.setDividerLocation(restore); + } + split.revalidate(); + split.repaint(); + }); + + return split; + } + + private static void configureTextArea(JTextArea area, boolean monospaced) { + area.setEditable(false); + area.setLineWrap(false); + area.setWrapStyleWord(false); + if (monospaced) { + area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + } else { + area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + } + } + + private static Color colorForStatus(String status) { + if (status == null) return new Color(120, 120, 120); + switch (status) { + case "running": + return new Color(33, 150, 243); + case "done": + return new Color(76, 175, 80); + case "error": + return new Color(244, 67, 54); + case "cancelled": + case "cancelling": + return new Color(255, 152, 0); + default: + return new Color(120, 120, 120); + } + } + + private static final class DotIcon implements Icon { + private final int size; + private Color color; + + DotIcon(int size, Color color) { + this.size = size; + this.color = color; + } + + void setColor(Color color) { + this.color = color; + } + + @Override + public int getIconWidth() { + return size; + } + + @Override + public int getIconHeight() { + return size; + } + + @Override + public void paintIcon(Component c, Graphics g, int x, int y) { + Graphics2D g2 = (Graphics2D) g.create(); + try { + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2.setColor(color != null ? color : Color.GRAY); + g2.fillOval(x, y, size, size); + } finally { + g2.dispose(); + } + } + } + + private static final class TestRunCellRenderer implements ListCellRenderer { + private final JPanel panel = new JPanel(new BorderLayout(8, 0)); + private final JLabel dotLabel = new JLabel(); + private final JLabel titleLabel = new JLabel(); + private final JLabel metaLabel = new JLabel(); + private final JPanel textPanel = new JPanel(); + private final DotIcon dotIcon = new DotIcon(10, new Color(120, 120, 120)); + + TestRunCellRenderer() { + panel.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8)); + dotLabel.setIcon(dotIcon); + + textPanel.setLayout(new BoxLayout(textPanel, BoxLayout.Y_AXIS)); + titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD)); + metaLabel.setFont(metaLabel.getFont().deriveFont(Font.PLAIN, 11f)); + metaLabel.setForeground(new Color(102, 102, 102)); + textPanel.add(titleLabel); + textPanel.add(metaLabel); + + panel.add(dotLabel, BorderLayout.WEST); + panel.add(textPanel, BorderLayout.CENTER); + panel.setOpaque(true); + textPanel.setOpaque(false); + } + + @Override + public Component getListCellRendererComponent(JList list, TestRun value, int index, boolean isSelected, boolean cellHasFocus) { + String titleText = value != null ? value.title : ""; + String modeText = value != null ? value.agentMode : ""; + String statusText = value != null ? value.status : ""; + + String shownTitle = titleText; + if (shownTitle.length() > 80) { + shownTitle = shownTitle.substring(0, 77) + "..."; + } + titleLabel.setText(shownTitle); + metaLabel.setText(modeText + " · " + statusText); + + dotIcon.setColor(colorForStatus(statusText)); + + if (isSelected) { + panel.setBackground(list.getSelectionBackground()); + titleLabel.setForeground(list.getSelectionForeground()); + metaLabel.setForeground(list.getSelectionForeground()); + } else { + panel.setBackground(list.getBackground()); + titleLabel.setForeground(list.getForeground()); + metaLabel.setForeground(new Color(102, 102, 102)); + } + + return panel; + } + } + + // right panel builds scroll panes for each tab + + private void wireActions() { + validateButton.addActionListener(e -> { + validateButton.setEnabled(false); + statusLabel.setText("Validating..."); + log("Validating connection..."); + new Thread(() -> { + try { + CyberStrikeAIClient.Config cfg = currentConfig(); + String token = client.loginAndValidate(cfg); + tokenRef.set(token); + SwingUtilities.invokeLater(() -> statusLabel.setText("OK (token saved)")); + log("Validation OK."); + } catch (Exception ex) { + tokenRef.set(""); + SwingUtilities.invokeLater(() -> statusLabel.setText("Failed: " + ex.getMessage())); + log("Validation failed: " + ex.getMessage()); + } finally { + SwingUtilities.invokeLater(() -> validateButton.setEnabled(true)); + } + }, "CyberStrikeAI-Validate").start(); + }); + + clearButton.addActionListener(e -> { + if (selectedRunId == null) { + progressArea.setText(""); + finalRawArea.setText(""); + markdownPane.setText(""); + return; + } + TestRun run = runs.get(selectedRunId); + if (run == null) return; + synchronized (run) { + run.buffer.setLength(0); + run.progressBuffer.setLength(0); + run.finalBuffer.setLength(0); + } + progressArea.setText(""); + finalRawArea.setText(""); + markdownPane.setText(""); + }); + + copyButton.addActionListener(e -> { + String text; + int idx = rightTabs.getSelectedIndex(); + String tabName = idx >= 0 ? rightTabs.getTitleAt(idx) : ""; + if ("Request".equals(tabName)) { + text = requestArea.getText(); + } else if ("Response".equals(tabName)) { + text = responseArea.getText(); + } else { + text = finalRawArea.getText(); + } + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(text == null ? "" : text), null); + }); + + stopButton.addActionListener(e -> { + String runId = selectedRunId; + if (runId == null) return; + TestRun run = runs.get(runId); + if (run == null) return; + String token = getToken(); + if (token == null || token.trim().isEmpty()) { + appendToRun(runId, "\n[error] Not validated.\n"); + return; + } + String convId; + synchronized (run) { + convId = run.conversationId; + } + if (convId == null || convId.trim().isEmpty()) { + appendToRun(runId, "\n[info] conversationId not available yet (wait for server to create session).\n"); + return; + } + + stopButton.setEnabled(false); + new Thread(() -> { + try { + CyberStrikeAIClient.Config cfg = currentConfig(); + client.cancelByConversationId(cfg.baseUrl, token, convId); + appendToRun(runId, "\n[info] Cancel requested.\n"); + setRunStatus(runId, "cancelling"); + } catch (Exception ex) { + appendToRun(runId, "\n[error] Cancel failed: " + ex.getMessage() + "\n"); + } finally { + SwingUtilities.invokeLater(() -> stopButton.setEnabled(true)); + } + }, "CyberStrikeAI-Cancel").start(); + }); + + searchField.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() { + @Override public void insertUpdate(javax.swing.event.DocumentEvent e) { applyFilter(); } + @Override public void removeUpdate(javax.swing.event.DocumentEvent e) { applyFilter(); } + @Override public void changedUpdate(javax.swing.event.DocumentEvent e) { applyFilter(); } + }); + + renderMarkdownBox.addActionListener(e -> refreshOutputView()); + } + + CyberStrikeAIClient.Config currentConfig() { + String host = hostField.getText().trim(); + String port = portField.getText().trim(); + String password = new String(passwordField.getPassword()); + String baseUrl = "http://" + host + ":" + port; + CyberStrikeAIClient.AgentMode mode = agentModeBox.getSelectedIndex() == 1 + ? CyberStrikeAIClient.AgentMode.MULTI + : CyberStrikeAIClient.AgentMode.SINGLE; + return new CyberStrikeAIClient.Config(baseUrl, password, mode); + } + + String getToken() { + return tokenRef.get(); + } + + boolean isShowDebugEvents() { + return showDebugEventsBox.isSelected(); + } + + private String nextRunId() { + return "run_" + runSeq.getAndIncrement(); + } + + private String formatRunDisplay(String title, String agentMode, String status) { + return title + " [" + agentMode + "] - " + status; + } + + String startNewRun(String title, String agentMode, IHttpRequestResponse msg) { + String id = nextRunId(); + TestRun run = new TestRun(id, title, agentMode); + if (msg != null) { + run.requestRaw = bytesToString(msg.getRequest()); + run.responseRaw = bytesToString(msg.getResponse()); + } + runs.put(id, run); + + int index = testListModel.getSize(); + runIdToIndex.put(id, index); + testListModel.addElement(run); + filteredListModel.addElement(run); + + selectedRunId = id; + filteredList.setSelectedIndex(filteredListModel.getSize() - 1); + progressArea.setText(""); + finalRawArea.setText(""); + markdownPane.setText(""); + requestArea.setText(run.requestRaw); + responseArea.setText(run.responseRaw); + refreshOutputView(); + return id; + } + + void setRunStatus(String runId, String status) { + TestRun run = runs.get(runId); + if (run == null) return; + synchronized (run) { + run.status = status; + } + Integer index = runIdToIndex.get(runId); + if (index != null) { + SwingUtilities.invokeLater(() -> filteredList.repaint()); + } + } + + void setRunConversationId(String runId, String conversationId) { + if (runId == null) return; + TestRun run = runs.get(runId); + if (run == null) return; + synchronized (run) { + run.conversationId = conversationId == null ? "" : conversationId; + } + } + + void appendToRun(String runId, String s) { + // Backward compatibility: default to progress bucket + appendProgressToRun(runId, s); + } + + void appendProgressToRun(String runId, String s) { + if (runId == null || s == null) return; + TestRun run = runs.get(runId); + if (run == null) return; + synchronized (run) { + run.buffer.append(s); + run.progressBuffer.append(s); + } + if (runId.equals(selectedRunId)) { + SwingUtilities.invokeLater(() -> { + progressArea.append(s); + progressArea.setCaretPosition(progressArea.getDocument().getLength()); + }); + } + } + + void appendFinalToRun(String runId, String s) { + if (runId == null || s == null) return; + TestRun run = runs.get(runId); + if (run == null) return; + synchronized (run) { + run.buffer.append(s); + run.finalBuffer.append(s); + } + if (runId.equals(selectedRunId)) { + SwingUtilities.invokeLater(() -> { + finalRawArea.append(s); + finalRawArea.setCaretPosition(finalRawArea.getDocument().getLength()); + }); + } + } + + void setFinalResponse(String runId, String finalResponse) { + if (runId == null) return; + TestRun run = runs.get(runId); + if (run == null) return; + synchronized (run) { + run.finalResponse = finalResponse == null ? "" : finalResponse; + } + if (runId.equals(selectedRunId)) { + SwingUtilities.invokeLater(this::refreshOutputView); + } + } + + private String getSelectedRunIdFromList() { + TestRun run = filteredList.getSelectedValue(); + return run == null ? null : run.id; + } + + private void setLogAreaToRun(String runId) { + TestRun run = runs.get(runId); + if (run == null) return; + selectedRunId = runId; + String progress; + String fin; + synchronized (run) { + progress = run.progressBuffer.toString(); + fin = run.finalBuffer.toString(); + } + SwingUtilities.invokeLater(() -> { + progressArea.setText(progress); + progressArea.setCaretPosition(progressArea.getDocument().getLength()); + finalRawArea.setText(fin); + finalRawArea.setCaretPosition(finalRawArea.getDocument().getLength()); + requestArea.setText(run.requestRaw == null ? "" : run.requestRaw); + responseArea.setText(run.responseRaw == null ? "" : run.responseRaw); + refreshOutputView(); + }); + } + + private void clearAllRuns() { + runs.clear(); + runIdToIndex.clear(); + testListModel.clear(); + filteredListModel.clear(); + selectedRunId = null; + SwingUtilities.invokeLater(() -> { + progressArea.setText(""); + finalRawArea.setText(""); + markdownPane.setText(""); + requestArea.setText(""); + responseArea.setText(""); + }); + } + + void clearAndShowStreamHeader(String title) { + SwingUtilities.invokeLater(() -> { + progressArea.setText(""); + finalRawArea.setText(title + "\n\n"); + }); + } + + // Legacy helpers kept for Validate logging + void appendStreamLine(String s) { + if (s == null) return; + SwingUtilities.invokeLater(() -> { + progressArea.append(s); + progressArea.append("\n"); + progressArea.setCaretPosition(progressArea.getDocument().getLength()); + }); + } + + private void log(String s) { + appendStreamLine("[*] " + s); + } + + private void applyFilter() { + String q = searchField.getText(); + if (q == null) q = ""; + String query = q.trim().toLowerCase(); + filteredListModel.clear(); + for (int i = 0; i < testListModel.size(); i++) { + TestRun r = testListModel.getElementAt(i); + if (query.isEmpty() || (r.title != null && r.title.toLowerCase().contains(query))) { + filteredListModel.addElement(r); + } + } + if (filteredListModel.size() > 0 && filteredList.getSelectedIndex() < 0) { + filteredList.setSelectedIndex(0); + } + } + + private void refreshOutputView() { + if (!renderMarkdownBox.isSelected()) { + outputCardsLayout.show(outputCards, "raw"); + return; + } + + if (selectedRunId == null) { + outputCardsLayout.show(outputCards, "raw"); + return; + } + + TestRun run = runs.get(selectedRunId); + if (run == null) { + outputCardsLayout.show(outputCards, "raw"); + return; + } + + String finalResp; + synchronized (run) { + finalResp = run.finalResponse; + } + if (finalResp == null || finalResp.trim().isEmpty()) { + // while streaming, stick to raw for performance + outputCardsLayout.show(outputCards, "raw"); + return; + } + + String html = MarkdownRenderer.toHtml(finalResp); + markdownPane.setText(html); + markdownPane.setCaretPosition(0); + outputCardsLayout.show(outputCards, "md"); + } + private static String bytesToString(byte[] bytes) { + if (bytes == null || bytes.length == 0) return ""; + return new String(bytes, StandardCharsets.ISO_8859_1); + } + + @Override + public String getTabCaption() { + return "CyberStrikeAI"; + } + + @Override + public Component getUiComponent() { + return root; + } +} + diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/src/main/java/burp/HttpMessageFormatter.java b/plugins/burp-suite/cyberstrikeai-burp-extension/src/main/java/burp/HttpMessageFormatter.java new file mode 100644 index 00000000..792465b0 --- /dev/null +++ b/plugins/burp-suite/cyberstrikeai-burp-extension/src/main/java/burp/HttpMessageFormatter.java @@ -0,0 +1,66 @@ +package burp; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +final class HttpMessageFormatter { + private HttpMessageFormatter() {} + + static String getRequestTitle(IExtensionHelpers helpers, IHttpRequestResponse msg) { + IRequestInfo reqInfo = helpers.analyzeRequest(msg); + String method = reqInfo.getMethod(); + if (reqInfo.getUrl() == null) { + return method + " (unknown)"; + } + String host = reqInfo.getUrl().getHost(); + String path = reqInfo.getUrl().getPath(); + if (path == null || path.isEmpty()) path = "/"; + String query = reqInfo.getUrl().getQuery(); + String shortPath = path; + if (shortPath.length() > 80) shortPath = shortPath.substring(0, 77) + "..."; + String q = (query != null && !query.isEmpty()) ? "?" : ""; + return method + " " + host + shortPath + q; + } + + static String toPrompt(IExtensionHelpers helpers, IHttpRequestResponse msg) { + IRequestInfo reqInfo = helpers.analyzeRequest(msg); + String method = reqInfo.getMethod(); + String url = reqInfo.getUrl() != null ? reqInfo.getUrl().toString() : "(unknown)"; + + byte[] reqBytes = msg.getRequest(); + int bodyOffset = reqInfo.getBodyOffset(); + String headers = String.join("\n", reqInfo.getHeaders()); + String body = ""; + if (reqBytes != null && reqBytes.length > bodyOffset) { + body = new String(reqBytes, bodyOffset, reqBytes.length - bodyOffset, StandardCharsets.ISO_8859_1); + } + + // Include response summary if available + String respSnippet = ""; + byte[] respBytes = msg.getResponse(); + if (respBytes != null && respBytes.length > 0) { + IResponseInfo respInfo = helpers.analyzeResponse(respBytes); + List respHeaders = respInfo.getHeaders(); + int respBodyOffset = respInfo.getBodyOffset(); + String respBody = ""; + if (respBytes.length > respBodyOffset) { + int max = Math.min(respBytes.length - respBodyOffset, 4096); + respBody = new String(respBytes, respBodyOffset, max, StandardCharsets.ISO_8859_1); + } + respSnippet = "\n\n[Optional: Response (truncated)]\n" + + String.join("\n", respHeaders) + + "\n\n" + + respBody; + } + + return "" + + "针对该流量做web渗透测试,并输出测试结果,要求:只针对该接口流量做测试,切勿拓展其他接口\n\n" + + "[Target]\n" + + method + " " + url + "\n\n" + + "[Request]\n" + + headers + "\n\n" + + body + + respSnippet; + } +} + diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/src/main/java/burp/MarkdownRenderer.java b/plugins/burp-suite/cyberstrikeai-burp-extension/src/main/java/burp/MarkdownRenderer.java new file mode 100644 index 00000000..b1666f0c --- /dev/null +++ b/plugins/burp-suite/cyberstrikeai-burp-extension/src/main/java/burp/MarkdownRenderer.java @@ -0,0 +1,195 @@ +package burp; + +import java.util.ArrayList; +import java.util.List; + +/** + * Minimal Markdown -> HTML renderer for Burp UI. + * Supports: headings (#..######), fenced code blocks (```), inline code (`), + * bold (**), lists (-/*), paragraphs, and basic escaping. + * + * Not a full CommonMark implementation; kept dependency-free on purpose. + */ +final class MarkdownRenderer { + private MarkdownRenderer() {} + + static String toHtml(String markdown) { + if (markdown == null) markdown = ""; + + List lines = splitLines(markdown); + StringBuilder out = new StringBuilder(4096); + out.append("") + .append(""); + + boolean inCode = false; + boolean inList = false; + StringBuilder codeBuf = new StringBuilder(); + + for (String raw : lines) { + String line = raw == null ? "" : raw; + + if (line.trim().startsWith("```")) { + if (!inCode) { + inCode = true; + codeBuf.setLength(0); + } else { + // close code + out.append("
")
+                            .append(escapeHtml(codeBuf.toString()))
+                            .append("
"); + inCode = false; + } + continue; + } + + if (inCode) { + codeBuf.append(line).append("\n"); + continue; + } + + String trimmed = line.trim(); + if (trimmed.isEmpty()) { + if (inList) { + out.append(""); + inList = false; + } + out.append("
"); + continue; + } + + // headings + int h = headingLevel(trimmed); + if (h > 0) { + if (inList) { + out.append(""); + inList = false; + } + String text = trimmed.substring(h).trim(); + out.append("") + .append(inlineFormat(text)) + .append(""); + continue; + } + + // list items + if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) { + if (!inList) { + out.append("
    "); + inList = true; + } + String item = trimmed.substring(2).trim(); + out.append("
  • ").append(inlineFormat(item)).append("
  • "); + continue; + } + + // normal paragraph + if (inList) { + out.append("
"); + inList = false; + } + out.append("

").append(inlineFormat(trimmed)).append("

"); + } + + if (inCode) { + out.append("
")
+                    .append(escapeHtml(codeBuf.toString()))
+                    .append("
"); + } + if (inList) { + out.append(""); + } + + out.append(""); + return out.toString(); + } + + private static int headingLevel(String s) { + int i = 0; + while (i < s.length() && s.charAt(i) == '#') i++; + if (i >= 1 && i <= 6 && i < s.length() && Character.isWhitespace(s.charAt(i))) return i; + return 0; + } + + private static String inlineFormat(String text) { + // escape first, then apply simple replacements using placeholders + String escaped = escapeHtml(text); + + // inline code: `code` + escaped = replaceInlineCode(escaped); + + // bold: **text** + escaped = replaceBold(escaped); + + return escaped; + } + + private static String replaceInlineCode(String s) { + StringBuilder out = new StringBuilder(s.length() + 16); + boolean in = false; + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '`') { + if (!in) { + in = true; + buf.setLength(0); + } else { + out.append("").append(buf).append(""); + in = false; + } + continue; + } + if (in) buf.append(c); + else out.append(c); + } + if (in) { + // unmatched backtick: keep as literal + out.append("`").append(buf); + } + return out.toString(); + } + + private static String replaceBold(String s) { + // simple non-nested **...** + StringBuilder out = new StringBuilder(s.length() + 16); + int i = 0; + while (i < s.length()) { + int start = s.indexOf("**", i); + if (start < 0) { + out.append(s.substring(i)); + break; + } + int end = s.indexOf("**", start + 2); + if (end < 0) { + out.append(s.substring(i)); + break; + } + out.append(s.substring(i, start)); + out.append("").append(s, start + 2, end).append(""); + i = end + 2; + } + return out.toString(); + } + + private static String escapeHtml(String s) { + if (s == null) return ""; + return s.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """); + } + + private static List splitLines(String s) { + String[] parts = s.split("\\r?\\n", -1); + List lines = new ArrayList<>(parts.length); + for (String p : parts) lines.add(p); + return lines; + } +} + diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/src/main/java/burp/SimpleJson.java b/plugins/burp-suite/cyberstrikeai-burp-extension/src/main/java/burp/SimpleJson.java new file mode 100644 index 00000000..e798fb52 --- /dev/null +++ b/plugins/burp-suite/cyberstrikeai-burp-extension/src/main/java/burp/SimpleJson.java @@ -0,0 +1,80 @@ +package burp; + +import java.util.HashMap; +import java.util.Map; + +/** + * Minimal JSON extractor for the SSE payloads we emit: + * {"type":"...","message":"...","data":...} + * + * This is NOT a general-purpose JSON parser; it's intentionally small to avoid external deps. + */ +final class SimpleJson { + private SimpleJson() {} + + static Map extractTopLevelStringFields(String json, String... keys) { + Map out = new HashMap<>(); + if (json == null) return out; + for (String key : keys) { + out.put(key, extractStringField(json, key)); + } + return out; + } + + static String extractStringField(String json, String key) { + if (json == null || key == null) return ""; + String needle = "\"" + key + "\""; + int k = json.indexOf(needle); + if (k < 0) return ""; + int colon = json.indexOf(':', k + needle.length()); + if (colon < 0) return ""; + int i = colon + 1; + while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++; + if (i >= json.length() || json.charAt(i) != '"') return ""; + i++; // after opening quote + StringBuilder sb = new StringBuilder(); + boolean esc = false; + while (i < json.length()) { + char c = json.charAt(i++); + if (esc) { + switch (c) { + case '"': sb.append('"'); break; + case '\\': sb.append('\\'); break; + case '/': sb.append('/'); break; + case 'b': sb.append('\b'); break; + case 'f': sb.append('\f'); break; + case 'n': sb.append('\n'); break; + case 'r': sb.append('\r'); break; + case 't': sb.append('\t'); break; + case 'u': + if (i + 3 < json.length()) { + String hex = json.substring(i, i + 4); + try { + sb.append((char) Integer.parseInt(hex, 16)); + i += 4; + } catch (NumberFormatException ignored) { + // best-effort: keep raw + sb.append("\\u").append(hex); + i += 4; + } + } + break; + default: + sb.append(c); + } + esc = false; + continue; + } + if (c == '\\') { + esc = true; + continue; + } + if (c == '"') { + break; + } + sb.append(c); + } + return sb.toString(); + } +} + diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/BurpExtender$1.class b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/BurpExtender$1.class new file mode 100644 index 00000000..07d7e8b6 Binary files /dev/null and b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/BurpExtender$1.class differ diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/BurpExtender.class b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/BurpExtender.class new file mode 100644 index 00000000..57d17f83 Binary files /dev/null and b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/BurpExtender.class differ diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAIClient$AgentMode.class b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAIClient$AgentMode.class new file mode 100644 index 00000000..26566ef6 Binary files /dev/null and b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAIClient$AgentMode.class differ diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAIClient$Config.class b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAIClient$Config.class new file mode 100644 index 00000000..f2a79c3c Binary files /dev/null and b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAIClient$Config.class differ diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAIClient$StreamListener.class b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAIClient$StreamListener.class new file mode 100644 index 00000000..cb6eb8e7 Binary files /dev/null and b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAIClient$StreamListener.class differ diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAIClient.class b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAIClient.class new file mode 100644 index 00000000..d85bd5ff Binary files /dev/null and b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAIClient.class differ diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAITab$1.class b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAITab$1.class new file mode 100644 index 00000000..50419043 Binary files /dev/null and b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAITab$1.class differ diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAITab$DotIcon.class b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAITab$DotIcon.class new file mode 100644 index 00000000..e206138c Binary files /dev/null and b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAITab$DotIcon.class differ diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAITab$TestRun.class b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAITab$TestRun.class new file mode 100644 index 00000000..8f50315c Binary files /dev/null and b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAITab$TestRun.class differ diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAITab$TestRunCellRenderer.class b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAITab$TestRunCellRenderer.class new file mode 100644 index 00000000..1e732b99 Binary files /dev/null and b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAITab$TestRunCellRenderer.class differ diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAITab.class b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAITab.class new file mode 100644 index 00000000..b58b59d1 Binary files /dev/null and b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/CyberStrikeAITab.class differ diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/HttpMessageFormatter.class b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/HttpMessageFormatter.class new file mode 100644 index 00000000..e4a781e5 Binary files /dev/null and b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/HttpMessageFormatter.class differ diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/MarkdownRenderer.class b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/MarkdownRenderer.class new file mode 100644 index 00000000..329c36a3 Binary files /dev/null and b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/MarkdownRenderer.class differ diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/SimpleJson.class b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/SimpleJson.class new file mode 100644 index 00000000..c155627a Binary files /dev/null and b/plugins/burp-suite/cyberstrikeai-burp-extension/target/classes/burp/SimpleJson.class differ diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/target/cyberstrikeai-burp-extension-1.0.0.jar b/plugins/burp-suite/cyberstrikeai-burp-extension/target/cyberstrikeai-burp-extension-1.0.0.jar new file mode 100644 index 00000000..ae3e6ead Binary files /dev/null and b/plugins/burp-suite/cyberstrikeai-burp-extension/target/cyberstrikeai-burp-extension-1.0.0.jar differ diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/target/maven-archiver/pom.properties b/plugins/burp-suite/cyberstrikeai-burp-extension/target/maven-archiver/pom.properties new file mode 100644 index 00000000..e8f61d98 --- /dev/null +++ b/plugins/burp-suite/cyberstrikeai-burp-extension/target/maven-archiver/pom.properties @@ -0,0 +1,3 @@ +artifactId=cyberstrikeai-burp-extension +groupId=ai.cyberstrike +version=1.0.0 diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/plugins/burp-suite/cyberstrikeai-burp-extension/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 00000000..fd49ad75 --- /dev/null +++ b/plugins/burp-suite/cyberstrikeai-burp-extension/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,14 @@ +burp/CyberStrikeAIClient$StreamListener.class +burp/CyberStrikeAIClient$Config.class +burp/CyberStrikeAIClient$AgentMode.class +burp/MarkdownRenderer.class +burp/SimpleJson.class +burp/CyberStrikeAIClient.class +burp/CyberStrikeAITab$DotIcon.class +burp/CyberStrikeAITab.class +burp/CyberStrikeAITab$1.class +burp/BurpExtender$1.class +burp/BurpExtender.class +burp/CyberStrikeAITab$TestRun.class +burp/CyberStrikeAITab$TestRunCellRenderer.class +burp/HttpMessageFormatter.class diff --git a/plugins/burp-suite/cyberstrikeai-burp-extension/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/plugins/burp-suite/cyberstrikeai-burp-extension/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 00000000..1f4d83a9 --- /dev/null +++ b/plugins/burp-suite/cyberstrikeai-burp-extension/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,6 @@ +/Users/temp/Downloads/CyberStrikeAI-main/plugins/burp-suite/cyberstrikeai-burp-extension/src/main/java/burp/BurpExtender.java +/Users/temp/Downloads/CyberStrikeAI-main/plugins/burp-suite/cyberstrikeai-burp-extension/src/main/java/burp/CyberStrikeAIClient.java +/Users/temp/Downloads/CyberStrikeAI-main/plugins/burp-suite/cyberstrikeai-burp-extension/src/main/java/burp/CyberStrikeAITab.java +/Users/temp/Downloads/CyberStrikeAI-main/plugins/burp-suite/cyberstrikeai-burp-extension/src/main/java/burp/HttpMessageFormatter.java +/Users/temp/Downloads/CyberStrikeAI-main/plugins/burp-suite/cyberstrikeai-burp-extension/src/main/java/burp/MarkdownRenderer.java +/Users/temp/Downloads/CyberStrikeAI-main/plugins/burp-suite/cyberstrikeai-burp-extension/src/main/java/burp/SimpleJson.java