diff --git a/plugins/README.md b/plugins/README.md index c3896a05..7503c38a 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -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.5** +- **Version**: **0.3.8** - **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) @@ -20,6 +20,7 @@ Optional integrations that connect CyberStrikeAI with other tools. | Feature | Description | |---------|-------------| +| Token expiry | Remaining time + 30s validate probe; detects server restart / unreachable | | Capture pause | **● Capturing** / **○ Paused** — stop recording without closing DevTools | | HTTP/1.1 display | Raw HAR in memory; UI + AI prompt normalized (no `:method` pseudo-headers) | | Collapsible conn bar | Host/Port/Validate collapses after success | diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/README.md b/plugins/browser-extension/cyberstrikeai-browser-extension/README.md index 33b93a0e..b7af0ca7 100644 --- a/plugins/browser-extension/cyberstrikeai-browser-extension/README.md +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/README.md @@ -1,6 +1,6 @@ ## CyberStrikeAI Browser Extension -**Version 0.3.5** — Full docs: **README.zh-CN.md** +**Version 0.3.8** — 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. @@ -28,7 +28,7 @@ Chromium DevTools extension: capture Network traffic and send it to CyberStrikeA - **Deferred Markdown** — plain text while streaming; render after done; skip above 100KB - **Stop** — abort local stream + server cancel via `conversationId` - **Latest XHR**, **Copy**, project/role/agent send dialog -- Session token + optional host permissions +- Session token with **expires_at** tracking, 30s server probe, restart/unreachable detection ### Data limits (no unbounded growth) diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/README.zh-CN.md b/plugins/browser-extension/cyberstrikeai-browser-extension/README.zh-CN.md index be6a652b..069f90c7 100644 --- a/plugins/browser-extension/cyberstrikeai-browser-extension/README.zh-CN.md +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/README.zh-CN.md @@ -1,6 +1,6 @@ ## CyberStrikeAI 浏览器扩展 -**当前版本:0.3.5** +**当前版本:0.3.8**(UI 为英文;中文说明见下文) Chrome / Edge(Chromium)DevTools 扩展:在开发者工具中捕获 **Network** 流量,发送到 CyberStrikeAI 进行 AI 辅助安全测试。能力与 Burp Suite 插件对齐,并按生产场景做了性能与体验优化。 @@ -81,8 +81,13 @@ Cookie: ... #### 安全与权限 - Token 存 **chrome.storage.session**(关浏览器失效) -- **optional_host_permissions**:Validate 时按需授权 CyberStrikeAI 地址 -- Markdown iframe 使用 `sandbox` +- 登录后保存 **`expires_at`**,状态栏显示 **剩余时间**(如 `OK · 剩余 11h 30m`) +- **不会自动续期**:过期后需重新 Validate(需 Password) +- 本地过期检测(30s)+ 服务端 `/api/auth/validate` 探测(同周期;切回面板时立即探测) +- 服务不可达时显示 **无法连接服务**;重启后 Token 失效显示 **服务已重启或 Token 已失效** +- **401/403** 时自动清空 Token 并展开连接栏 +- Send 前主动校验 Token 有效性 +- **optional_host_permissions**:Validate 时按需授权 --- @@ -141,6 +146,12 @@ Cookie: ... **扩展更新后报错 `chrome.runtime.connect` undefined?** 扩展重载后旧 DevTools 面板上下文失效。请:**关闭 DevTools → 重新加载扩展 → 再开 F12**。 +**Token 过期会自动刷新吗?** +**不会自动续期**(无 refresh token)。插件会保存 `expires_at`、显示剩余时间;每 30s 向服务端校验,切回 DevTools 时立即校验。服务重启后 session 清空,会提示重新 Validate。 + +**重启服务后状态还显示 OK?** +v0.3.7 起每 30s 探测 `/api/auth/validate`;不可达显示黄色警告,Token 失效则清空并展开连接栏。重载扩展后请关闭 DevTools 再开 F12。 + **Request 里为什么曾经有 `:authority`、`:method`?** HTTP/2 伪首部。展示与 AI Prompt 已归一化为 HTTP/1.1;原始 HAR 仍保存在内存 entry 中。 @@ -205,8 +216,9 @@ panel/ popup/ popup.html / popup.js / popup.css # 只读状态 lib/ + auth-session.js # Token 过期检测与剩余时间提示 api.js # 登录、SSE、项目/角色 API - storage.js # 配置与 session token + storage.js # 配置 + session token + expires_at capture.js # HAR 摘要、静态过滤 http-normalize.js # HTTP/2 → HTTP/1.1 展示/Prompt formatter.js # toPrompt 组装 diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/dist/cyberstrikeai-browser-extension.zip b/plugins/browser-extension/cyberstrikeai-browser-extension/dist/cyberstrikeai-browser-extension.zip index 5c708c99..f9f83fb5 100644 Binary files a/plugins/browser-extension/cyberstrikeai-browser-extension/dist/cyberstrikeai-browser-extension.zip and b/plugins/browser-extension/cyberstrikeai-browser-extension/dist/cyberstrikeai-browser-extension.zip differ diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/lib/api.js b/plugins/browser-extension/cyberstrikeai-browser-extension/lib/api.js index 00766bde..694a2f6d 100644 --- a/plugins/browser-extension/cyberstrikeai-browser-extension/lib/api.js +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/lib/api.js @@ -10,7 +10,14 @@ function agentModeById(id) { } async function apiFetch(baseUrl, path, options = {}) { - const res = await fetch(baseUrl + path, options); + let res; + try { + res = await fetch(baseUrl + path, options); + } catch (err) { + const e = err instanceof Error ? err : new Error(String(err)); + e.network = true; + throw e; + } const text = await res.text(); let json = null; try { @@ -19,8 +26,9 @@ async function apiFetch(baseUrl, path, options = {}) { json = null; } if (!res.ok) { - const err = (json && json.error) || text || `HTTP ${res.status}`; - throw new Error(err); + const err = new Error((json && json.error) || text || `HTTP ${res.status}`); + err.status = res.status; + throw err; } return json; } @@ -34,12 +42,19 @@ async function loginAndValidate(baseUrl, password, signal) { }); const token = login && login.token; if (!token) throw new Error('Login response missing token'); + await validateTokenSession(baseUrl, token, signal); + return { + token, + expiresAt: (login && login.expires_at) || '', + }; +} + +async function validateTokenSession(baseUrl, token, signal) { await apiFetch(baseUrl, '/api/auth/validate', { method: 'GET', headers: { Authorization: `Bearer ${token}` }, signal, }); - return token; } async function fetchProjects(baseUrl, token, signal) { @@ -48,11 +63,11 @@ async function fetchProjects(baseUrl, token, signal) { signal, }); const list = (data && data.projects) || []; - const out = [{ id: '', label: '(无)' }]; + const out = [{ id: '', label: '(none)' }]; for (const p of list) { if (!p.id) continue; let label = p.name || p.id; - if (p.status === 'archived') label += ' [已归档]'; + if (p.status === 'archived') label += ' [archived]'; out.push({ id: p.id, label }); } return out; @@ -64,7 +79,7 @@ async function fetchRoles(baseUrl, token, signal) { signal, }); const list = (data && data.roles) || []; - const out = [{ id: '', label: '默认' }]; + const out = [{ id: '', label: 'Default' }]; for (const r of list) { if (r.enabled === false) continue; if (!r.name) continue; @@ -106,7 +121,9 @@ async function streamTest(baseUrl, token, options, handlers) { if (!res.ok) { const t = await res.text(); - throw new Error(t || `HTTP ${res.status}`); + const err = new Error(t || `HTTP ${res.status}`); + err.status = res.status; + throw err; } const reader = res.body.getReader(); diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/lib/auth-session.js b/plugins/browser-extension/cyberstrikeai-browser-extension/lib/auth-session.js new file mode 100644 index 00000000..2d99826f --- /dev/null +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/lib/auth-session.js @@ -0,0 +1,54 @@ +/** Session token expiry helpers (aligned with web auth.js patterns). */ + +const SESSION_TOKEN_EXPIRY_KEY = 'csai_token_expires_at'; + +/** Warn when remaining session time is below this (ms). */ +const TOKEN_WARN_BEFORE_MS = 30 * 60 * 1000; + +function parseExpiresAt(iso) { + if (!iso) return null; + const d = new Date(iso); + return Number.isNaN(d.getTime()) ? null : d; +} + +function isTokenExpiredByTime(expiresAtIso) { + const d = parseExpiresAt(expiresAtIso); + if (!d) return false; + return d.getTime() <= Date.now(); +} + +function tokenExpiresWithin(expiresAtIso, withinMs) { + const d = parseExpiresAt(expiresAtIso); + if (!d) return false; + return d.getTime() - Date.now() <= withinMs; +} + +function formatTokenExpiryHint(expiresAtIso) { + const d = parseExpiresAt(expiresAtIso); + if (!d) return 'OK (token saved)'; + const ms = d.getTime() - Date.now(); + if (ms <= 0) return 'Session expired'; + const h = Math.floor(ms / 3600000); + const m = Math.floor((ms % 3600000) / 60000); + if (h >= 1) return `OK · ${h}h ${m}m left`; + if (m >= 1) return `OK · ${m}m left`; + return 'OK · expiring soon'; +} + +function isAuthHttpStatus(status) { + return status === 401 || status === 403; +} + +/** fetch() failed before HTTP response (server down, connection refused, etc.). */ +function isNetworkFetchError(err) { + if (!err) return false; + if (err.network === true) return true; + if (err.name === 'AbortError') return false; + const msg = String(err.message || err); + return err.name === 'TypeError' && /fetch|network|Failed to fetch/i.test(msg); +} + +function attachHttpStatus(err, status) { + if (err && typeof err === 'object') err.status = status; + return err; +} diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/lib/constants.js b/plugins/browser-extension/cyberstrikeai-browser-extension/lib/constants.js index 70b1ffed..7dfad46a 100644 --- a/plugins/browser-extension/cyberstrikeai-browser-extension/lib/constants.js +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/lib/constants.js @@ -14,4 +14,4 @@ const CSAI_LIMITS = { }; const CSAI_DEFAULT_INSTRUCTION = - '针对该流量做web渗透测试,并输出测试结果,要求:只针对该接口流量做测试,切勿拓展其他接口'; + 'Perform web penetration testing on this traffic and output results. Test only this endpoint; do not expand to other APIs.'; diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/lib/storage.js b/plugins/browser-extension/cyberstrikeai-browser-extension/lib/storage.js index 2ba1cdcc..22f42b63 100644 --- a/plugins/browser-extension/cyberstrikeai-browser-extension/lib/storage.js +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/lib/storage.js @@ -27,42 +27,118 @@ const DEFAULTS = { showDebugEvents: false, }; +function extensionContextAlive() { + try { + return !!(typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.id); + } catch (_) { + return false; + } +} + +function extensionContextError() { + const err = new Error('Extension context invalidated'); + err.name = 'ExtensionContextError'; + err.contextInvalidated = true; + return err; +} + +function isExtensionContextError(err) { + if (!err) return false; + if (err.contextInvalidated || err.name === 'ExtensionContextError') return true; + return /extension context invalidated/i.test(String(err.message || err)); +} + +function normalizeStorageError(err) { + if (isExtensionContextError(err)) return extensionContextError(); + return err instanceof Error ? err : new Error(String(err)); +} + +function rejectLastError(reject) { + const msg = chrome.runtime.lastError && chrome.runtime.lastError.message; + if (!msg) return false; + reject(/invalidated/i.test(msg) ? extensionContextError() : new Error(msg)); + return true; +} + function localGet(keys) { - return new Promise((resolve) => chrome.storage.local.get(keys, resolve)); + return new Promise((resolve, reject) => { + try { + if (!extensionContextAlive()) { + reject(extensionContextError()); + return; + } + chrome.storage.local.get(keys, (data) => { + if (rejectLastError(reject)) return; + resolve(data); + }); + } catch (err) { + reject(normalizeStorageError(err)); + } + }); } function localSet(obj) { - return new Promise((resolve) => chrome.storage.local.set(obj, resolve)); + return new Promise((resolve, reject) => { + try { + if (!extensionContextAlive()) { + reject(extensionContextError()); + return; + } + chrome.storage.local.set(obj, () => { + if (rejectLastError(reject)) return; + resolve(); + }); + } catch (err) { + reject(normalizeStorageError(err)); + } + }); } function sessionGet(keys) { - return new Promise((resolve) => { - if (!chrome.storage.session) { - chrome.storage.local.get(keys, resolve); - return; + return new Promise((resolve, reject) => { + try { + if (!extensionContextAlive()) { + reject(extensionContextError()); + return; + } + const store = chrome.storage.session || chrome.storage.local; + store.get(keys, (data) => { + if (rejectLastError(reject)) return; + resolve(data); + }); + } catch (err) { + reject(normalizeStorageError(err)); } - chrome.storage.session.get(keys, resolve); }); } function sessionSet(obj) { - return new Promise((resolve) => { - if (!chrome.storage.session) { - chrome.storage.local.set(obj, resolve); - return; + return new Promise((resolve, reject) => { + try { + if (!extensionContextAlive()) { + reject(extensionContextError()); + return; + } + const store = chrome.storage.session || chrome.storage.local; + store.set(obj, () => { + if (rejectLastError(reject)) return; + resolve(); + }); + } catch (err) { + reject(normalizeStorageError(err)); } - chrome.storage.session.set(obj, resolve); }); } async function loadConfig() { const data = await localGet(Object.values(STORAGE_KEYS)); - const sess = await sessionGet([SESSION_TOKEN_KEY]); + const sess = await sessionGet([SESSION_TOKEN_KEY, SESSION_TOKEN_EXPIRY_KEY]); return { host: data[STORAGE_KEYS.host] || DEFAULTS.host, port: data[STORAGE_KEYS.port] || DEFAULTS.port, https: data[STORAGE_KEYS.https] !== false, token: sess[SESSION_TOKEN_KEY] || '', + tokenExpiresAt: sess[SESSION_TOKEN_EXPIRY_KEY] || '', lastProjectId: data[STORAGE_KEYS.lastProjectId] || '', lastRole: data[STORAGE_KEYS.lastRole] || '', lastAgentMode: data[STORAGE_KEYS.lastAgentMode] || 'eino_single', @@ -88,7 +164,10 @@ async function saveConfig(partial) { if (partial.renderMarkdown != null) localMap[STORAGE_KEYS.renderMarkdown] = partial.renderMarkdown; if (partial.showDebugEvents != null) localMap[STORAGE_KEYS.showDebugEvents] = partial.showDebugEvents; if (Object.keys(localMap).length) await localSet(localMap); - if (partial.token != null) await sessionSet({ [SESSION_TOKEN_KEY]: partial.token }); + const sessionMap = {}; + if (partial.token != null) sessionMap[SESSION_TOKEN_KEY] = partial.token; + if (partial.tokenExpiresAt != null) sessionMap[SESSION_TOKEN_EXPIRY_KEY] = partial.tokenExpiresAt; + if (Object.keys(sessionMap).length) await sessionSet(sessionMap); } function baseUrlFrom(cfg) { @@ -98,6 +177,7 @@ function baseUrlFrom(cfg) { /** Request optional host permission for the configured CyberStrikeAI origin. */ async function ensureHostPermission(baseUrl) { + if (!extensionContextAlive()) throw extensionContextError(); if (!chrome.permissions || !chrome.permissions.request) return; let origin; try { @@ -109,6 +189,6 @@ async function ensureHostPermission(baseUrl) { if (has) return; const granted = await chrome.permissions.request({ origins: [origin] }); if (!granted) { - throw new Error('需要授权访问 CyberStrikeAI 服务器地址'); + throw new Error('Permission required to access the CyberStrikeAI server'); } } diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/manifest.json b/plugins/browser-extension/cyberstrikeai-browser-extension/manifest.json index fbe2bfa3..afcd1dc6 100644 --- a/plugins/browser-extension/cyberstrikeai-browser-extension/manifest.json +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "CyberStrikeAI", - "version": "0.3.5", + "version": "0.3.9", "description": "Capture browser HTTP traffic and send to CyberStrikeAI for AI-assisted security testing.", "devtools_page": "devtools.html", "permissions": ["storage"], diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/panel/panel.html b/plugins/browser-extension/cyberstrikeai-browser-extension/panel/panel.html index 936d92c2..3e32ea59 100644 --- a/plugins/browser-extension/cyberstrikeai-browser-extension/panel/panel.html +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/panel/panel.html @@ -1,5 +1,5 @@ - +
@@ -8,7 +8,7 @@