mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-11-13 15:57:22 +01:00
Merge pull request #384 from Sycun/codex/fix-plugin-rbac-login
fix(plugins): support username-based RBAC login in Burp and browser extensions
This commit is contained in:
19 files changed
+310
-33
No files matched your search
@@ -0,0 +1,28 @@
|
||||
name: Plugin authentication
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'plugins/**'
|
||||
- 'scripts/test-plugin-auth.sh'
|
||||
- '.github/workflows/plugin-auth.yml'
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'plugins/**'
|
||||
- 'scripts/test-plugin-auth.sh'
|
||||
- '.github/workflows/plugin-auth.yml'
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
auth:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '11'
|
||||
- run: bash scripts/test-plugin-auth.sh
|
||||
+1
-1
@@ -11,7 +11,7 @@ Optional integrations that connect CyberStrikeAI with other tools.
|
||||
### Browser (Chrome / Edge)
|
||||
|
||||
- **Path**: `plugins/browser-extension/cyberstrikeai-browser-extension/`
|
||||
- **Version**: **0.3.8**
|
||||
- **Version**: **0.3.11**
|
||||
- **Install**: `chrome://extensions/` → Load unpacked → F12 → **CyberStrikeAI** tab
|
||||
- **Package**: `bash package.sh` → `dist/cyberstrikeai-browser-extension.zip`
|
||||
- **Docs**: `README.zh-CN.md` (full) / `README.md` (summary)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## CyberStrikeAI Browser Extension
|
||||
|
||||
**Version 0.3.10** — Full docs: **README.zh-CN.md**
|
||||
**Version 0.3.11** — Full docs: **README.zh-CN.md**
|
||||
|
||||
Chromium DevTools extension: capture Network traffic and send it to CyberStrikeAI for AI-assisted security testing. Aligned with the Burp Suite plugin.
|
||||
|
||||
@@ -11,6 +11,12 @@ Chromium DevTools extension: capture Network traffic and send it to CyberStrikeA
|
||||
3. Open target page → **F12** → **CyberStrikeAI** tab → **Validate**
|
||||
4. Select a captured request → **Send** → view **Output**
|
||||
|
||||
### RBAC login
|
||||
|
||||
Fill in Host / Port / HTTPS and the same Username / Password used in the web UI, then click Validate. Username defaults to `admin`; custom accounts use their own username. The username is saved locally; the password is not persisted. Changing connection settings or credentials clears the cached token and requires Validate again.
|
||||
|
||||
If the server reports a missing username and the panel only has Password, reload the updated extension at `chrome://extensions/`, then close and reopen DevTools. Do not enter `username:password` in Password.
|
||||
|
||||
### Popup vs DevTools panel
|
||||
|
||||
| Location | Purpose |
|
||||
@@ -72,3 +78,7 @@ lib/ # api, storage, capture, http-normalize, markdown, …
|
||||
icons/
|
||||
package.sh
|
||||
```
|
||||
|
||||
### Authentication regression tests
|
||||
|
||||
From the repository root, run `bash scripts/test-plugin-auth.sh` (Node.js and JDK 11+). This tests both clients against local HTTP fixtures, username validation, credential escaping, settings migration, and Burp account-switch invalidation. The script downloads the compile-only Burp API; set `BURP_API_JAR` to an existing JAR for offline use.
|
||||
@@ -1,6 +1,6 @@
|
||||
## CyberStrikeAI 浏览器扩展
|
||||
|
||||
**当前版本:0.3.10**(UI 为英文;中文说明见下文)
|
||||
**当前版本:0.3.11**(UI 为英文;中文说明见下文)
|
||||
|
||||
Chrome / Edge(Chromium)DevTools 扩展:在开发者工具中捕获 **Network** 流量,发送到 CyberStrikeAI 进行 AI 辅助安全测试。能力与 Burp Suite 插件对齐,并按生产场景做了性能与体验优化。
|
||||
|
||||
@@ -11,7 +11,7 @@ Chrome / Edge(Chromium)DevTools 扩展:在开发者工具中捕获 **Netwo
|
||||
1. `chrome://extensions/` → 开发者模式 → **加载已解压的扩展程序**
|
||||
2. 选择目录:`plugins/browser-extension/cyberstrikeai-browser-extension/`
|
||||
3. 打开目标页面 → **F12** → 顶部 Tab **CyberStrikeAI**
|
||||
4. 填写 Host / Port / Password → **Validate**(首次会请求访问服务器地址权限)
|
||||
4. 填写 Host / Port / Username / Password → **Validate**(首次会请求访问服务器地址权限)
|
||||
5. 左侧选中捕获请求 → **Send** → 在 **Output** 查看 AI 结果
|
||||
|
||||
点击浏览器工具栏图标可查看 **只读连接状态**;完整配置与操作均在 DevTools 面板内完成。
|
||||
@@ -34,7 +34,7 @@ Chrome / Edge(Chromium)DevTools 扩展:在开发者工具中捕获 **Netwo
|
||||
|
||||
| 区域 | 说明 |
|
||||
|------|------|
|
||||
| **连接栏** | Host、Port、HTTPS、Password、Validate;成功后收起为 `https://host:port` 摘要 |
|
||||
| **连接栏** | Host、Port、HTTPS、Username、Password、Validate;成功后收起为 `https://host:port` 摘要 |
|
||||
| **Test History** | 最多 50 次 Send 记录,可回看 Progress / Final |
|
||||
| **Captured Requests** | 当前 Tab 捕获列表,最多 200 条,支持搜索 |
|
||||
| **Output** | 默认 Tab:流式 Progress + Final Response |
|
||||
@@ -97,7 +97,7 @@ Cookie: ...
|
||||
| 控件 | 作用 |
|
||||
|------|------|
|
||||
| **Validate** | 登录并校验 Token;进行中再次点击为 Cancel |
|
||||
| **连接设置 / 收起** | 展开或折叠 Host/Port/Password 表单 |
|
||||
| **连接设置 / 收起** | 展开或折叠 Host/Port/Username/Password 表单 |
|
||||
| **Send** | 对选中捕获发起到 CyberStrikeAI |
|
||||
| **Latest XHR** | 选中最近 XHR/Fetch 并 Send |
|
||||
| **Stop** | 停止当前 AI 流(本地 + 服务端) |
|
||||
@@ -251,3 +251,9 @@ package.sh
|
||||
| 项目/角色/模式 | Send 弹窗 | Send 弹窗 |
|
||||
| SSE 输出 | Progress + Final | Progress + Final |
|
||||
| 捕获开关 | — | ● 捕获中 / ○ 已暂停 |
|
||||
|
||||
### RBAC 用户名与密码
|
||||
|
||||
Username 填网页登录用户名(默认 `admin`,自建账号填写自己的用户名),Password 填该账号密码,不要拼接成 `用户名:密码`。用户名保存在本地,密码不持久化。修改连接信息或账号密码后,旧 Token 会清除,需要重新 Validate。
|
||||
|
||||
若提示“用户名不能为空”且界面只有 Password,请在 `chrome://extensions/` 重新加载更新后的插件,再完全关闭并重新打开 DevTools。
|
||||
Vendored
BIN
Binary file not shown.
@@ -39,11 +39,15 @@ async function apiFetch(baseUrl, path, options = {}) {
|
||||
return json;
|
||||
}
|
||||
|
||||
async function loginAndValidate(baseUrl, password, signal) {
|
||||
async function loginAndValidate(baseUrl, username, password, signal) {
|
||||
username = username.trim();
|
||||
if (!username || [...username].length > 64) {
|
||||
throw new Error('Username is required and must not exceed 64 characters');
|
||||
}
|
||||
const login = await apiFetch(baseUrl, '/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({ password }),
|
||||
body: JSON.stringify({ username, password }),
|
||||
signal,
|
||||
});
|
||||
const token = login && login.token;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* global chrome */
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
username: 'csai_username',
|
||||
host: 'csai_host',
|
||||
port: 'csai_port',
|
||||
https: 'csai_https',
|
||||
@@ -17,6 +18,7 @@ const STORAGE_KEYS = {
|
||||
const SESSION_TOKEN_KEY = 'csai_token';
|
||||
|
||||
const DEFAULTS = {
|
||||
username: 'admin',
|
||||
host: '127.0.0.1',
|
||||
port: '8080',
|
||||
https: true,
|
||||
@@ -134,6 +136,7 @@ async function loadConfig() {
|
||||
const data = await localGet(Object.values(STORAGE_KEYS));
|
||||
const sess = await sessionGet([SESSION_TOKEN_KEY, SESSION_TOKEN_EXPIRY_KEY]);
|
||||
return {
|
||||
username: data[STORAGE_KEYS.username] ?? DEFAULTS.username,
|
||||
host: data[STORAGE_KEYS.host] || DEFAULTS.host,
|
||||
port: data[STORAGE_KEYS.port] || DEFAULTS.port,
|
||||
https: data[STORAGE_KEYS.https] !== false,
|
||||
@@ -152,6 +155,7 @@ async function loadConfig() {
|
||||
|
||||
async function saveConfig(partial) {
|
||||
const localMap = {};
|
||||
if (partial.username != null) localMap[STORAGE_KEYS.username] = partial.username;
|
||||
if (partial.host != null) localMap[STORAGE_KEYS.host] = partial.host;
|
||||
if (partial.port != null) localMap[STORAGE_KEYS.port] = partial.port;
|
||||
if (partial.https != null) localMap[STORAGE_KEYS.https] = partial.https;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "CyberStrikeAI",
|
||||
"version": "0.3.10",
|
||||
"version": "0.3.11",
|
||||
"description": "Capture browser HTTP traffic and send to CyberStrikeAI for AI-assisted security testing.",
|
||||
"devtools_page": "devtools.html",
|
||||
"permissions": ["storage"],
|
||||
|
||||
@@ -5,5 +5,5 @@ OUT="$ROOT/dist/cyberstrikeai-browser-extension.zip"
|
||||
mkdir -p "$ROOT/dist"
|
||||
rm -f "$OUT"
|
||||
(cd "$ROOT" && zip -r "$OUT" . \
|
||||
-x './dist/*' -x './package.sh' -x '*/.DS_Store')
|
||||
-x './dist/*' -x './tests/*' -x './package.sh' -x '*/.DS_Store')
|
||||
echo "[+] $OUT"
|
||||
@@ -34,6 +34,10 @@
|
||||
<input id="https" type="checkbox">
|
||||
<span>HTTPS</span>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">Username</span>
|
||||
<input id="username" type="text" size="10" autocomplete="username" spellcheck="false">
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">Password</span>
|
||||
<input id="password" type="password" size="10" autocomplete="off">
|
||||
|
||||
@@ -652,6 +652,7 @@ async function initConfig() {
|
||||
}
|
||||
token = config.token || '';
|
||||
tokenExpiresAt = config.tokenExpiresAt || '';
|
||||
$('username').value = config.username;
|
||||
$('host').value = config.host;
|
||||
$('port').value = config.port;
|
||||
$('https').checked = config.https;
|
||||
@@ -686,6 +687,7 @@ async function persistConnection() {
|
||||
|
||||
function connectionConfigFromForm() {
|
||||
return {
|
||||
username: $('username').value.trim(),
|
||||
host: $('host').value.trim(),
|
||||
port: $('port').value.trim(),
|
||||
https: $('https').checked,
|
||||
@@ -698,12 +700,15 @@ function connectionConfigFromForm() {
|
||||
async function onValidate() {
|
||||
if (validating) {
|
||||
validateAbort?.abort();
|
||||
validating = false;
|
||||
$('btn-validate').textContent = 'Validate';
|
||||
setStatus('Cancelled', 'warn');
|
||||
setStatus('Cancelling...', 'warn');
|
||||
return;
|
||||
}
|
||||
validating = true;
|
||||
token = '';
|
||||
tokenExpiresAt = '';
|
||||
invalidateCatalogCache();
|
||||
updateSendButtons();
|
||||
for (const id of ['host', 'port', 'https', 'username', 'password']) $(id).disabled = true;
|
||||
validateAbort = new AbortController();
|
||||
$('btn-validate').textContent = 'Cancel';
|
||||
setStatus('Validating...', 'pending');
|
||||
@@ -722,9 +727,9 @@ async function onValidate() {
|
||||
|
||||
try {
|
||||
await hostPermission;
|
||||
await saveConfig(nextConfig);
|
||||
await saveConfig({ ...nextConfig, token: '', tokenExpiresAt: '' });
|
||||
config = { ...config, ...nextConfig };
|
||||
const auth = await loginAndValidate(baseUrl, password, validateAbort.signal);
|
||||
const auth = await loginAndValidate(baseUrl, nextConfig.username, password, validateAbort.signal);
|
||||
token = auth.token;
|
||||
tokenExpiresAt = auth.expiresAt || '';
|
||||
await saveConfig({ token, tokenExpiresAt });
|
||||
@@ -750,6 +755,7 @@ async function onValidate() {
|
||||
}
|
||||
updateSendButtons();
|
||||
} finally {
|
||||
for (const id of ['host', 'port', 'https', 'username', 'password']) $(id).disabled = false;
|
||||
validating = false;
|
||||
validateAbort = null;
|
||||
$('btn-validate').textContent = 'Validate';
|
||||
@@ -1207,9 +1213,11 @@ function setupTabs() {
|
||||
|
||||
$('btn-validate').addEventListener('click', onValidate);
|
||||
$('btn-conn-toggle').addEventListener('click', onConnToggle);
|
||||
$('host').addEventListener('input', updateConnSummary);
|
||||
$('port').addEventListener('input', updateConnSummary);
|
||||
$('https').addEventListener('change', updateConnSummary);
|
||||
for (const id of ['host', 'port', 'https', 'username', 'password']) {
|
||||
$(id).addEventListener(id === 'https' ? 'change' : 'input', () => {
|
||||
clearAuthSession('Connection changed — please Validate again').catch(onContextLoss);
|
||||
});
|
||||
}
|
||||
$('btn-send').addEventListener('click', () => openSendDialog());
|
||||
$('btn-latest-xhr').addEventListener('click', sendLatestXhr);
|
||||
$('btn-stop').addEventListener('click', onStop);
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// Run: node --test plugins/browser-extension/cyberstrikeai-browser-extension/tests/auth.test.cjs
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const vm = require('node:vm');
|
||||
const http = require('node:http');
|
||||
const path = require('node:path');
|
||||
const root = path.join(__dirname, '..');
|
||||
|
||||
test('RBAC login sends username and exact password, validates token, and reports failures', async () => {
|
||||
const requests = [];
|
||||
const server = http.createServer(async (req, res) => {
|
||||
let body = '';
|
||||
for await (const chunk of req) body += chunk;
|
||||
const data = body ? JSON.parse(body) : null;
|
||||
requests.push({ url: req.url, data, authorization: req.headers.authorization });
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
if (req.url === '/api/auth/login') {
|
||||
if (!data.username || data.password === 'wrong') {
|
||||
res.writeHead(401).end(JSON.stringify({ error: 'invalid credentials' }));
|
||||
} else res.end(JSON.stringify({ token: 'test-token', expires_at: '2099-01-01T00:00:00Z' }));
|
||||
} else res.end('{}');
|
||||
});
|
||||
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||
try {
|
||||
const ctx = vm.createContext({ fetch });
|
||||
vm.runInContext(fs.readFileSync(path.join(root, 'lib/api.js'), 'utf8'), ctx);
|
||||
const base = `http://127.0.0.1:${server.address().port}`;
|
||||
for (const username of ['admin', 'analyst', '研究员"\\']) {
|
||||
const password = ' p"\\\n密码 ';
|
||||
const result = await ctx.loginAndValidate(base, ` ${username} `, password);
|
||||
assert.equal(result.token, 'test-token');
|
||||
assert.deepEqual(requests.at(-2).data, { username, password });
|
||||
assert.equal(requests.at(-1).authorization, 'Bearer test-token');
|
||||
}
|
||||
const before = requests.length;
|
||||
for (const username of ['', ' ', '𠮷'.repeat(65)]) {
|
||||
await assert.rejects(ctx.loginAndValidate(base, username, 'x'), /Username is required/);
|
||||
}
|
||||
assert.equal(requests.length, before, 'invalid usernames never reach server');
|
||||
await assert.rejects(ctx.loginAndValidate(base, 'admin', 'wrong'), /invalid credentials/);
|
||||
assert.equal(requests.length, before + 1, 'failed login never validates a token');
|
||||
} finally { await new Promise(resolve => server.close(resolve)); }
|
||||
});
|
||||
|
||||
test('existing settings default to admin; custom username persists without password', async () => {
|
||||
const local = {}, session = {};
|
||||
const store = data => ({ get: (_, cb) => cb({ ...data }), set: (obj, cb) => { Object.assign(data, obj); cb(); } });
|
||||
const ctx = vm.createContext({
|
||||
CSAI_DEFAULT_INSTRUCTION: '', SESSION_TOKEN_EXPIRY_KEY: 'expiry',
|
||||
chrome: { runtime: { id: 'test' }, storage: { local: store(local), session: store(session) } },
|
||||
});
|
||||
vm.runInContext(fs.readFileSync(path.join(root, 'lib/storage.js'), 'utf8'), ctx);
|
||||
assert.equal((await ctx.loadConfig()).username, 'admin');
|
||||
await ctx.saveConfig({ username: 'analyst', password: 'do-not-store', token: 'session' });
|
||||
assert.equal((await ctx.loadConfig()).username, 'analyst');
|
||||
assert.equal(JSON.stringify({ local, session }).includes('do-not-store'), false);
|
||||
await ctx.saveConfig({ token: '', tokenExpiresAt: '' });
|
||||
assert.equal((await ctx.loadConfig()).token, '');
|
||||
});
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
### What it does
|
||||
|
||||
- Configure **Host / Port / HTTPS / Password** and choose an agent mode
|
||||
- Configure **Host / Port / HTTPS / Username / Password** and choose an agent mode
|
||||
- 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** (agent modes: **Eino Single**, Deep, Plan-Execute, Supervisor — maps to `/api/eino-agent/stream` or `/api/multi-agent/stream`)
|
||||
- Keep a **test history sidebar** (searchable) so you can revisit previous runs
|
||||
@@ -67,3 +67,13 @@ If you already have Gradle available, you can still use `build.gradle` to build.
|
||||
- Uncheck **HTTPS** only if your server runs plain HTTP.
|
||||
- It uses **Bearer Token** authentication obtained from the configured password.
|
||||
|
||||
|
||||
### RBAC login
|
||||
|
||||
Use the same username and password as the web UI. Username defaults to `admin`; enter your own username for a custom account. Changing connection settings or credentials clears the cached token and requires Validate again. Passwords are not persisted.
|
||||
|
||||
If the server reports a missing username and the plugin only has Password, rebuild and reload the JAR with the Username field. Entering `username:password` in Password does not work.
|
||||
|
||||
### Authentication regression tests
|
||||
|
||||
From the repository root, run `bash scripts/test-plugin-auth.sh` (Node.js and JDK 11+). This tests both clients against local HTTP fixtures, username validation, credential escaping, settings migration, and Burp account-switch invalidation. The script downloads the compile-only Burp API; set `BURP_API_JAR` to an existing JAR for offline use.
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
### 功能概述
|
||||
|
||||
- 在 Burp 的 `CyberStrikeAI` 标签页中配置 **Host、端口、密码**
|
||||
- 在 Burp 的 `CyberStrikeAI` 标签页中配置 **Host、端口、用户名、密码**
|
||||
- 点击 **Validate(验证)**:
|
||||
- 调用 `POST /api/auth/login` 用密码换取 Token
|
||||
- 调用 `POST /api/auth/login` 用用户名和密码换取 Token
|
||||
- 调用 `GET /api/auth/validate` 校验 Token
|
||||
- 验证通过后 Token 会保存在插件内存中(本次 Burp 会话有效)
|
||||
- 右键任意 HTTP 请求包 → **Send to CyberStrikeAI (stream test)**:
|
||||
@@ -84,7 +84,8 @@ cd plugins/burp-suite/cyberstrikeai-burp-extension
|
||||
- **Host**:例如 `127.0.0.1`
|
||||
- **Port**:例如 `8080`
|
||||
- **HTTPS**:默认勾选(对接 `config.yaml` 中 `tls_enabled` / 自签证书);插件会自动信任本地自签证书,无需导入
|
||||
- **Password**:你的 CyberStrikeAI 登录密码(对应服务端 `auth.password`)
|
||||
- **Username**:网页登录用户名,默认 `admin`;自建账号填写自己的用户名
|
||||
- **Password**:该账号的网页登录密码,不要拼接用户名
|
||||
3) 点击 **Validate**
|
||||
- 成功:状态显示 `OK (token saved)`
|
||||
- 失败:状态会显示错误原因(例如密码错误、服务不可达、401/403 等)
|
||||
@@ -93,10 +94,16 @@ cd plugins/burp-suite/cyberstrikeai-burp-extension
|
||||
6) 在弹出框中按需选择**项目、角色、对话模式**,并编辑测试指令后点确定
|
||||
7) 每次发送后会在 `CyberStrikeAI` 标签页左侧显示一个“测试记录”(请求标题 + 模式/角色 + 状态);点击对应记录即可在右侧查看该次的流式输出结果
|
||||
|
||||
修改连接信息或账号密码后,需要重新点击 **Validate**。密码仅保留在当前界面,不持久化保存。
|
||||
|
||||
### 常见问题(排错)
|
||||
|
||||
- **提示“用户名不能为空”,但界面只有 Password**
|
||||
- 旧插件未适配 RBAC 登录。重新构建并加载包含 Username 字段的 JAR;修改源码不会更新已加载的旧 JAR。
|
||||
|
||||
|
||||
- **Validate 失败 / 401**
|
||||
- 确认密码是否正确(服务端 `auth.password`)
|
||||
- 确认用户名和密码与网页登录一致
|
||||
- 确认 IP/端口是否能访问(例如浏览器能打开 `https://IP:PORT/`)
|
||||
- 服务端启用 TLS 时勾选 **HTTPS**(默认已勾选);自签证书无需手动导入
|
||||
- 若仍为纯 HTTP 部署,取消勾选 **HTTPS**
|
||||
|
||||
BIN
Binary file not shown.
+10
-4
@@ -29,10 +29,12 @@ final class CyberStrikeAIClient {
|
||||
|
||||
static final class Config {
|
||||
final String baseUrl; // e.g. http://127.0.0.1:8080
|
||||
final String username;
|
||||
final String password;
|
||||
|
||||
Config(String baseUrl, String password) {
|
||||
Config(String baseUrl, String username, String password) {
|
||||
this.baseUrl = baseUrl;
|
||||
this.username = username.trim();
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
@@ -119,7 +121,7 @@ final class CyberStrikeAIClient {
|
||||
}
|
||||
}, AUTH_OVERALL_TIMEOUT_MS);
|
||||
try {
|
||||
String token = login(cfg.baseUrl, cfg.password);
|
||||
String token = login(cfg.baseUrl, cfg.username, cfg.password);
|
||||
if (Thread.interrupted()) {
|
||||
throw timeoutIOException();
|
||||
}
|
||||
@@ -176,7 +178,10 @@ final class CyberStrikeAIClient {
|
||||
);
|
||||
}
|
||||
|
||||
private String login(String baseUrl, String password) throws IOException {
|
||||
private String login(String baseUrl, String username, String password) throws IOException {
|
||||
if (username.isEmpty() || username.codePointCount(0, username.length()) > 64) {
|
||||
throw new IOException("Username is required and must not exceed 64 characters");
|
||||
}
|
||||
URL url = new URL(baseUrl + "/api/auth/login");
|
||||
HttpURLConnection conn = SslTrustAll.open(url, AUTH_CONNECT_TIMEOUT_MS, AUTH_READ_TIMEOUT_MS);
|
||||
trackConnection(conn);
|
||||
@@ -185,7 +190,8 @@ final class CyberStrikeAIClient {
|
||||
conn.setDoOutput(true);
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setRequestProperty("Accept", "application/json");
|
||||
String body = "{\"password\":\"" + escapeJson(password) + "\"}";
|
||||
String body = "{\"username\":\"" + escapeJson(username)
|
||||
+ "\",\"password\":\"" + escapeJson(password) + "\"}";
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
os.write(body.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
+33
-6
@@ -15,6 +15,7 @@ final class CyberStrikeAITab implements ITab {
|
||||
private final JTextField hostField = new JTextField("127.0.0.1");
|
||||
private final JTextField portField = new JTextField("8080");
|
||||
private final JCheckBox useHttpsBox = new JCheckBox("HTTPS", true);
|
||||
private final JTextField usernameField = new JTextField("admin");
|
||||
private final JPasswordField passwordField = new JPasswordField();
|
||||
private final JButton validateButton = new JButton("Validate");
|
||||
private final JButton clearButton = new JButton("Clear Output");
|
||||
@@ -102,6 +103,7 @@ final class CyberStrikeAITab implements ITab {
|
||||
|
||||
hostField.setColumns(14);
|
||||
portField.setColumns(6);
|
||||
usernameField.setColumns(10);
|
||||
passwordField.setColumns(12);
|
||||
|
||||
JPanel row1 = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 2));
|
||||
@@ -111,6 +113,8 @@ final class CyberStrikeAITab implements ITab {
|
||||
row1.add(portField);
|
||||
useHttpsBox.setToolTipText("Use https:// for CyberStrikeAI (self-signed certs are trusted automatically)");
|
||||
row1.add(useHttpsBox);
|
||||
row1.add(new JLabel("Username"));
|
||||
row1.add(usernameField);
|
||||
row1.add(new JLabel("Password"));
|
||||
row1.add(passwordField);
|
||||
row1.add(validateButton);
|
||||
@@ -406,12 +410,37 @@ final class CyberStrikeAITab implements ITab {
|
||||
|
||||
// right panel builds scroll panes for each tab
|
||||
|
||||
private void setConnectionEditable(boolean editable) {
|
||||
hostField.setEnabled(editable);
|
||||
portField.setEnabled(editable);
|
||||
useHttpsBox.setEnabled(editable);
|
||||
usernameField.setEnabled(editable);
|
||||
passwordField.setEnabled(editable);
|
||||
}
|
||||
|
||||
private void invalidateAuthentication() {
|
||||
tokenRef.set("");
|
||||
statusLabel.setText("Connection changed — please Validate again");
|
||||
}
|
||||
|
||||
private void wireActions() {
|
||||
javax.swing.event.DocumentListener credentialsChanged = new javax.swing.event.DocumentListener() {
|
||||
@Override public void insertUpdate(javax.swing.event.DocumentEvent e) { invalidateAuthentication(); }
|
||||
@Override public void removeUpdate(javax.swing.event.DocumentEvent e) { invalidateAuthentication(); }
|
||||
@Override public void changedUpdate(javax.swing.event.DocumentEvent e) { invalidateAuthentication(); }
|
||||
};
|
||||
for (JTextField field : new JTextField[]{hostField, portField, usernameField, passwordField}) {
|
||||
field.getDocument().addDocumentListener(credentialsChanged);
|
||||
}
|
||||
useHttpsBox.addActionListener(e -> invalidateAuthentication());
|
||||
validateButton.addActionListener(e -> {
|
||||
if ("Cancel".equals(validateButton.getText())) {
|
||||
cancelValidateInProgress();
|
||||
return;
|
||||
}
|
||||
final CyberStrikeAIClient.Config cfg = currentConfig();
|
||||
tokenRef.set("");
|
||||
setConnectionEditable(false);
|
||||
validateButton.setText("Cancel");
|
||||
validateButton.setEnabled(true);
|
||||
stopButton.setEnabled(true);
|
||||
@@ -419,7 +448,6 @@ final class CyberStrikeAITab implements ITab {
|
||||
log("Validating connection... (max ~10s; click Cancel or Stop to abort)");
|
||||
Thread worker = new Thread(() -> {
|
||||
try {
|
||||
CyberStrikeAIClient.Config cfg = currentConfig();
|
||||
String token = client.loginAndValidate(cfg);
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
return;
|
||||
@@ -439,6 +467,7 @@ final class CyberStrikeAITab implements ITab {
|
||||
} finally {
|
||||
validateThreadRef.set(null);
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
setConnectionEditable(true);
|
||||
validateButton.setText("Validate");
|
||||
validateButton.setEnabled(true);
|
||||
});
|
||||
@@ -543,7 +572,7 @@ final class CyberStrikeAITab implements ITab {
|
||||
String password = new String(passwordField.getPassword());
|
||||
String scheme = useHttpsBox.isSelected() ? "https" : "http";
|
||||
String baseUrl = scheme + "://" + host + ":" + port;
|
||||
return new CyberStrikeAIClient.Config(baseUrl, password);
|
||||
return new CyberStrikeAIClient.Config(baseUrl, usernameField.getText(), password);
|
||||
}
|
||||
|
||||
String getToken() {
|
||||
@@ -773,14 +802,12 @@ final class CyberStrikeAITab implements ITab {
|
||||
|
||||
private void cancelValidateInProgress() {
|
||||
client.cancelActiveRequest();
|
||||
Thread t = validateThreadRef.getAndSet(null);
|
||||
Thread t = validateThreadRef.get();
|
||||
if (t != null) {
|
||||
t.interrupt();
|
||||
}
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
statusLabel.setText("Cancelled");
|
||||
validateButton.setText("Validate");
|
||||
validateButton.setEnabled(true);
|
||||
statusLabel.setText("Cancelling...");
|
||||
});
|
||||
log("Validation cancelled.");
|
||||
}
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package burp;
|
||||
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.SwingUtilities;
|
||||
|
||||
/** Standalone regression test; see README for commands. No test framework required. */
|
||||
public final class AuthRegressionTest {
|
||||
private static void check(boolean value, String message) {
|
||||
if (!value) throw new AssertionError(message);
|
||||
}
|
||||
public static void main(String[] args) throws Exception {
|
||||
AtomicReference<String> request = new AtomicReference<>();
|
||||
AtomicReference<String> authorization = new AtomicReference<>();
|
||||
AtomicInteger logins = new AtomicInteger();
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.createContext("/api/auth/login", exchange -> {
|
||||
String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
|
||||
request.set(body);
|
||||
logins.incrementAndGet();
|
||||
boolean wrong = body.contains("wrong");
|
||||
byte[] response = (wrong ? "{\"error\":\"invalid credentials\"}" : "{\"token\":\"test-token\"}").getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json");
|
||||
exchange.sendResponseHeaders(wrong ? 401 : 200, response.length);
|
||||
exchange.getResponseBody().write(response);
|
||||
exchange.close();
|
||||
});
|
||||
server.createContext("/api/auth/validate", exchange -> {
|
||||
authorization.set(exchange.getRequestHeaders().getFirst("Authorization"));
|
||||
byte[] response = "{}".getBytes(StandardCharsets.UTF_8);
|
||||
exchange.sendResponseHeaders(200, response.length);
|
||||
exchange.getResponseBody().write(response);
|
||||
exchange.close();
|
||||
});
|
||||
server.start();
|
||||
try {
|
||||
CyberStrikeAIClient client = new CyberStrikeAIClient();
|
||||
String base = "http://127.0.0.1:" + server.getAddress().getPort();
|
||||
for (String username : new String[]{"admin", "analyst", "研究员\"\\"}) {
|
||||
String password = " p\"\\\n密码 ";
|
||||
String token = client.loginAndValidate(new CyberStrikeAIClient.Config(base, " " + username + " ", password));
|
||||
check("test-token".equals(token), "login token");
|
||||
check(username.equals(SimpleJson.extractStringField(request.get(), "username")), "username roundtrip");
|
||||
check(password.equals(SimpleJson.extractStringField(request.get(), "password")), "exact password roundtrip");
|
||||
check("Bearer test-token".equals(authorization.get()), "token validation");
|
||||
}
|
||||
int before = logins.get();
|
||||
for (String username : new String[]{"", " ", "𠮷".repeat(65)}) {
|
||||
try {
|
||||
client.loginAndValidate(new CyberStrikeAIClient.Config(base, username, "x"));
|
||||
throw new AssertionError("invalid username accepted");
|
||||
} catch (java.io.IOException expected) {
|
||||
check(expected.getMessage().contains("Username is required"), "username error");
|
||||
}
|
||||
}
|
||||
check(logins.get() == before, "invalid username sent to server");
|
||||
authorization.set(null);
|
||||
try {
|
||||
client.loginAndValidate(new CyberStrikeAIClient.Config(base, "admin", "wrong"));
|
||||
throw new AssertionError("wrong password accepted");
|
||||
} catch (java.io.IOException expected) {
|
||||
check(expected.getMessage().contains("invalid credentials"), "server error preserved");
|
||||
}
|
||||
check(authorization.get() == null, "failed login validated token");
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
try {
|
||||
CyberStrikeAITab tab = new CyberStrikeAITab();
|
||||
check("admin".equals(tab.currentConfig().username), "default admin");
|
||||
java.lang.reflect.Field tokenField = CyberStrikeAITab.class.getDeclaredField("tokenRef");
|
||||
tokenField.setAccessible(true);
|
||||
@SuppressWarnings("unchecked") AtomicReference<String> token = (AtomicReference<String>) tokenField.get(tab);
|
||||
token.set("old-session");
|
||||
java.lang.reflect.Field userField = CyberStrikeAITab.class.getDeclaredField("usernameField");
|
||||
userField.setAccessible(true);
|
||||
((JTextField) userField.get(tab)).setText("analyst");
|
||||
check(tab.getToken().isEmpty(), "account switch must clear old token");
|
||||
check("analyst".equals(tab.currentConfig().username), "UI username reaches config");
|
||||
} catch (ReflectiveOperationException e) { throw new RuntimeException(e); }
|
||||
});
|
||||
System.out.println("PASS: Burp RBAC login, errors, escaping, and account switch");
|
||||
} finally { server.stop(0); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# RBAC login regressions for both shipped plugins. Requires Node.js and JDK 11+.
|
||||
set -euo pipefail
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
BURP_DIR="$ROOT_DIR/plugins/burp-suite/cyberstrikeai-burp-extension"
|
||||
TEST_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||
# Burp supplies this API at runtime; it is only needed here for compilation/UI tests.
|
||||
API_JAR="${BURP_API_JAR:-$TEST_DIR/burp-extender-api.jar}"
|
||||
if [[ ! -f "$API_JAR" ]]; then
|
||||
curl -fsSL https://repo.maven.apache.org/maven2/net/portswigger/burp/extender/burp-extender-api/2.3/burp-extender-api-2.3.jar -o "$API_JAR"
|
||||
fi
|
||||
node --test "$ROOT_DIR/plugins/browser-extension/cyberstrikeai-browser-extension/tests/auth.test.cjs"
|
||||
find "$BURP_DIR/src/main/java" "$BURP_DIR/src/test/java" -name '*.java' -print0 |
|
||||
xargs -0 javac --release 11 -encoding UTF-8 -cp "$API_JAR" -d "$TEST_DIR/classes"
|
||||
java -Djava.awt.headless=true -cp "$TEST_DIR/classes:$API_JAR" burp.AuthRegressionTest
|
||||
Reference in new issue
Block a user