From 983fe650c1bd432303678f2a5ab9cbab0620717c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Tue, 26 May 2026 17:49:46 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 118 +++++++++++++++++ web/static/js/projects.js | 261 +++++++++++++++++++++++++++++++++++--- web/templates/index.html | 37 ++++-- 3 files changed, 392 insertions(+), 24 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index 07941ed1..c10635e2 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -21755,6 +21755,55 @@ button.chat-files-dropdown-item:hover:not(:disabled) { background: #f1f5f9; color: #64748b; } +.projects-category { + display: inline-block; + font-size: 0.6875rem; + font-weight: 600; + padding: 3px 8px; + border-radius: 6px; + text-transform: lowercase; + white-space: nowrap; +} +.projects-category--target { + background: #dbeafe; + color: #1e40af; +} +.projects-category--auth { + background: #ede9fe; + color: #5b21b6; +} +.projects-category--infra { + background: #e2e8f0; + color: #334155; +} +.projects-category--business { + background: #ccfbf1; + color: #0f766e; +} +.projects-category--finding { + background: #ffedd5; + color: #c2410c; +} +.projects-category--chain { + background: #fed7aa; + color: #9a3412; +} +.projects-category--exploit { + background: #fee2e2; + color: #991b1b; +} +.projects-category--poc { + background: #ffe4e6; + color: #be123c; +} +.projects-category--note { + background: #f1f5f9; + color: #64748b; +} +.projects-category--custom { + background: #e0e7ff; + color: #4338ca; +} .projects-severity { display: inline-block; font-size: 0.6875rem; @@ -21928,6 +21977,75 @@ body.projects-modal-open { border: 1px solid #e2e8f0; color: #334155; } +.projects-field-hint { + margin: 6px 0 0; + font-size: 0.75rem; + line-height: 1.45; + color: #64748b; +} +.projects-field-hint--warn { + color: #b45309; +} +.projects-form-label-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 6px; +} +.projects-form-label-row > label { + margin-bottom: 0; +} +.projects-form-label-actions { + display: flex; + gap: 10px; + flex-wrap: wrap; +} +.projects-form-label-actions .btn-link { + font-size: 0.75rem; + padding: 0; + border: none; + background: none; + color: #2563eb; + cursor: pointer; + text-decoration: underline; +} +.fact-modal-body-input { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 0.8125rem; + line-height: 1.5; +} +.projects-fact-sparse-warn { + margin: 0 0 10px; + padding: 10px 12px; + font-size: 0.8125rem; + line-height: 1.45; + color: #92400e; + background: #fffbeb; + border: 1px solid #fcd34d; + border-radius: 8px; +} +.projects-fact-badge { + display: inline-block; + font-size: 0.6875rem; + font-weight: 600; + padding: 2px 7px; + border-radius: 4px; + white-space: nowrap; +} +.projects-fact-badge--ok { + color: #166534; + background: #dcfce7; +} +.projects-fact-badge--warn { + color: #92400e; + background: #fef3c7; +} +.projects-fact-badge--na { + color: #64748b; + background: #f1f5f9; +} .vulnerability-filter-field--project select { min-width: 120px; max-width: 160px; diff --git a/web/static/js/projects.js b/web/static/js/projects.js index a6bd8850..c1c95b18 100644 --- a/web/static/js/projects.js +++ b/web/static/js/projects.js @@ -11,6 +11,129 @@ let _projectsFetchPromise = null; const PROJECT_ACTIVE_KEY = 'cyberstrike.activeProjectId'; +/** 与后端 internal/project/fact_template.go 对齐 */ +const FACT_ATTACK_CHAIN_BODY_TEMPLATE = `## 结论(可验证,一句话) +<勿仅写「存在漏洞」;写明类型 + 位置 + 触发条件> + +## 目标与入口 +- 目标: +- 入口: <路径 / 接口 / 参数> +- 前置条件: <匿名 / 角色 / Cookie / 其他依赖> + +## 攻击链(逐步可复现) +1. <侦察/发现> +2. <利用/触发> +3. <影响证明(读文件、RCE 回显、越权数据等)> + +## Exploit / POC +### 请求 +\`\`\`http + HTTP/1.1 +Host: ... +... + + +\`\`\` + +### 响应 / 现象 +<关键响应片段、状态码、差异点> + +### 命令 / 脚本(如有) +\`\`\`bash + +\`\`\` + +## 关键证据 +- <工具输出摘要 / 截图路径 / 会话或消息 ID> + +## 关联 +- related_vulnerability_id: <可选> +- 依赖事实: + +## 备注与不确定性 +<待验证假设、环境差异、绕过尝试记录>`; + +const FACT_ENV_BODY_TEMPLATE = `## 摘要 +<该事实的核心认知> + +## 细节 +<端口/版本/路径/凭据特征/业务规则等> + +## 来源与证据 +<命令输出、响应片段、发现时间> + +## 关联 +- 相关 fact_key: <可选>`; + +const FACT_ATTACK_CHAIN_PREFIXES = ['finding/', 'chain/', 'exploit/', 'poc/']; +const FACT_ATTACK_CHAIN_CATEGORIES = new Set(['finding', 'chain', 'exploit', 'poc', 'vuln']); + +function requiresAttackChainFact(category, factKey) { + const c = (category || '').trim().toLowerCase(); + if (FACT_ATTACK_CHAIN_CATEGORIES.has(c)) return true; + const key = (factKey || '').trim().toLowerCase(); + return FACT_ATTACK_CHAIN_PREFIXES.some((p) => key.startsWith(p)); +} + +function isSparseFactBody(category, factKey, body) { + if (!requiresAttackChainFact(category, factKey)) return false; + const text = (body || '').trim(); + if (!text) return true; + const lower = text.toLowerCase(); + const hasSteps = + lower.includes('攻击链') || + lower.includes('## 攻击') || + lower.includes('## exploit') || + lower.includes('## poc'); + const hasHTTP = + lower.includes('```http') || + lower.includes('```bash') || + lower.includes('curl ') || + lower.includes('get ') || + lower.includes('post '); + const hasReq = lower.includes('请求') || lower.includes('响应') || lower.includes('payload'); + return !(hasSteps || hasHTTP || hasReq); +} + +function formatFactBodyBadge(f) { + if (!requiresAttackChainFact(f.category, f.fact_key)) { + const hasBody = !!(f.body || '').trim(); + return `${hasBody ? '有详情' : '—'}`; + } + if (isSparseFactBody(f.category, f.fact_key, f.body)) { + return '待补全'; + } + return '可复现'; +} + +function updateFactFormHints() { + const cat = document.getElementById('fact-modal-category')?.value || ''; + const key = document.getElementById('fact-modal-key')?.value || ''; + const body = document.getElementById('fact-modal-body')?.value || ''; + const hint = document.getElementById('fact-modal-body-hint'); + if (!hint) return; + if (requiresAttackChainFact(cat, key)) { + const sparse = isSparseFactBody(cat, key, body); + hint.textContent = sparse + ? '⚠ 攻击链类事实:请填写完整 body(步骤、HTTP/命令、响应现象),勿仅写结论。可点「插入攻击链模板」。' + : '攻击链类:body 将用于审计复现,请保留原始请求/响应与逐步步骤。'; + hint.classList.toggle('projects-field-hint--warn', sparse); + } else { + hint.textContent = '环境认知类:body 建议记录来源证据;发现/利用请改用 finding|chain|exploit|poc 分类。'; + hint.classList.remove('projects-field-hint--warn'); + } +} + +function insertFactBodyTemplate(kind) { + const ta = document.getElementById('fact-modal-body'); + if (!ta) return; + const tpl = kind === 'env' ? FACT_ENV_BODY_TEMPLATE : FACT_ATTACK_CHAIN_BODY_TEMPLATE; + if (ta.value.trim() && !confirm('将覆盖当前 body 内容为模板,是否继续?')) return; + ta.value = tpl; + updateFactFormHints(); + ta.focus(); +} + function getActiveProjectId() { try { return localStorage.getItem(PROJECT_ACTIVE_KEY) || ''; @@ -126,6 +249,27 @@ function updateProjectsListCount() { if (el) el.textContent = String(projectsCache.length); } +/** 事实分类 → 徽章样式(与 fact_template.go 常量对齐) */ +const FACT_CATEGORY_BADGE = { + target: 'projects-category--target', + auth: 'projects-category--auth', + infra: 'projects-category--infra', + business: 'projects-category--business', + finding: 'projects-category--finding', + chain: 'projects-category--chain', + exploit: 'projects-category--exploit', + poc: 'projects-category--poc', + note: 'projects-category--note', + vuln: 'projects-category--exploit', +}; + +function formatCategoryBadge(category) { + const raw = (category || '').trim(); + const c = raw.toLowerCase() || 'note'; + const cls = FACT_CATEGORY_BADGE[c] || 'projects-category--custom'; + return `${escapeHtml(raw || '—')}`; +} + function formatConfidenceBadge(confidence) { const c = (confidence || '').toLowerCase(); let cls = 'projects-confidence--tentative'; @@ -268,15 +412,15 @@ function switchProjectTab(tab) { async function loadProjectFacts() { const tbody = document.getElementById('project-facts-tbody'); if (!tbody || !currentProjectId) return; - tbody.innerHTML = '加载中…'; + tbody.innerHTML = '加载中…'; const res = await apiFetch(`/api/projects/${currentProjectId}/facts?limit=200`); if (!res.ok) { - tbody.innerHTML = '加载失败'; + tbody.innerHTML = '加载失败'; return; } const facts = await res.json(); if (!facts.length) { - tbody.innerHTML = '暂无事实,点击「添加事实」或由 Agent 自动写入'; + tbody.innerHTML = '暂无事实,点击「添加事实」或由 Agent 自动写入'; refreshProjectHeaderStats(); return; } @@ -285,8 +429,9 @@ async function loadProjectFacts() { const idEsc = escapeHtml(f.id); return ` ${keyEsc} - ${escapeHtml(f.category)} + ${formatCategoryBadge(f.category)} ${escapeHtml(f.summary)} + ${formatFactBodyBadge(f)} ${formatConfidenceBadge(f.confidence)} ${formatProjectTime(f.updated_at, f.created_at)} ${renderProjectFactActions(keyEsc, idEsc)} @@ -327,10 +472,26 @@ async function viewProjectFactBody(factKey) { const f = await res.json(); _factDetailKey = f.fact_key; document.getElementById('fact-detail-title').textContent = `[${f.fact_key}]`; - document.getElementById('fact-detail-meta').textContent = - `分类: ${f.category} · 置信度: ${f.confidence} · 更新: ${formatProjectTime(f.updated_at, f.created_at)}` + - (f.related_vulnerability_id ? ` · 关联漏洞: ${f.related_vulnerability_id}` : ''); + const metaParts = [ + `分类: ${f.category}`, + `置信度: ${f.confidence}`, + `更新: ${formatProjectTime(f.updated_at, f.created_at)}`, + ]; + if (f.related_vulnerability_id) metaParts.push(`关联漏洞: ${f.related_vulnerability_id}`); + if (f.source_conversation_id) metaParts.push(`来源对话: ${f.source_conversation_id}`); + document.getElementById('fact-detail-meta').textContent = metaParts.join(' · '); document.getElementById('fact-detail-body').textContent = f.body || '(无 body)'; + const warnEl = document.getElementById('fact-detail-sparse-warn'); + if (warnEl) { + if (isSparseFactBody(f.category, f.fact_key, f.body)) { + warnEl.hidden = false; + warnEl.textContent = + '⚠ 该事实属于攻击链/利用类,但 body 缺少可复现结构(攻击链步骤、HTTP/命令、请求响应等)。建议编辑后补全以便审计复现。'; + } else { + warnEl.hidden = true; + warnEl.textContent = ''; + } + } openProjectsOverlay('fact-detail-modal'); } @@ -562,6 +723,7 @@ function resetFactModalForm() { document.getElementById('fact-modal-confidence').value = 'tentative'; const rel = document.getElementById('fact-modal-related-vuln'); if (rel) rel.value = ''; + updateFactFormHints(); } function fillFactModalForm(f) { @@ -569,7 +731,19 @@ function fillFactModalForm(f) { document.getElementById('fact-modal-title').textContent = '编辑事实'; document.getElementById('fact-modal-submit-btn').textContent = '保存修改'; document.getElementById('fact-modal-key').value = f.fact_key || ''; - document.getElementById('fact-modal-category').value = f.category || 'note'; + const catEl = document.getElementById('fact-modal-category'); + const cat = (f.category || 'note').trim().toLowerCase(); + if (catEl) { + const known = Array.from(catEl.options).some((o) => o.value === cat); + if (known) catEl.value = cat; + else { + const opt = document.createElement('option'); + opt.value = f.category; + opt.textContent = `${f.category}(自定义)`; + catEl.appendChild(opt); + catEl.value = f.category; + } + } document.getElementById('fact-modal-summary').value = f.summary || ''; document.getElementById('fact-modal-body').value = f.body || ''; const conf = (f.confidence || 'tentative').toLowerCase(); @@ -580,6 +754,7 @@ function fillFactModalForm(f) { } const rel = document.getElementById('fact-modal-related-vuln'); if (rel) rel.value = f.related_vulnerability_id || ''; + updateFactFormHints(); } function showAddFactModal() { @@ -608,12 +783,20 @@ function closeFactModal() { async function saveFactModal() { const fact_key = document.getElementById('fact-modal-key').value.trim(); const summary = document.getElementById('fact-modal-summary').value.trim(); + const category = document.getElementById('fact-modal-category').value.trim() || 'note'; + const body = document.getElementById('fact-modal-body').value; if (!fact_key || !summary) return alert('fact_key 与 summary 必填'); + if (isSparseFactBody(category, fact_key, body)) { + const ok = confirm( + '该事实属于攻击链/利用类,但 body 尚未包含可复现结构(步骤、HTTP/命令、请求响应等)。\n仍要保存吗?建议先插入攻击链模板并填写 POC。', + ); + if (!ok) return; + } const payload = { fact_key, - category: document.getElementById('fact-modal-category').value.trim() || 'note', + category, summary, - body: document.getElementById('fact-modal-body').value, + body, confidence: document.getElementById('fact-modal-confidence').value, related_vulnerability_id: document.getElementById('fact-modal-related-vuln')?.value?.trim() || '', }; @@ -708,17 +891,63 @@ function getChatProjectSelection() { return getActiveProjectId(); } +function isActiveChatProjectId(id) { + if (!id) return false; + return projectsCache.some((p) => p.id === id && p.status !== 'archived'); +} + +/** 用于 UI:无效/已删除/无可用项目时视为未绑定 */ +function resolveChatProjectSelection() { + const raw = getChatProjectSelection(); + if (!raw) return ''; + if (!_projectsListReady) return raw; + return isActiveChatProjectId(raw) ? raw : ''; +} + +let _normalizingStaleProject = false; + +/** 项目列表加载后,清除 localStorage 或对话上残留的失效项目 ID */ +async function normalizeStaleChatProjectSelection() { + if (!_projectsListReady || _normalizingStaleProject) return; + const raw = getChatProjectSelection(); + if (!raw || isActiveChatProjectId(raw)) return; + + _normalizingStaleProject = true; + try { + if (window.currentConversationId) { + window._loadedConversationProjectId = ''; + try { + const res = await apiFetch( + `/api/conversations/${encodeURIComponent(window.currentConversationId)}/project`, + { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ projectId: '' }), + } + ); + if (!res.ok) console.warn('清除失效的项目绑定失败'); + } catch (e) { + console.warn(e); + } + } else { + setActiveProjectId(''); + } + } finally { + _normalizingStaleProject = false; + } +} + function updateChatProjectButtonLabel() { const textEl = document.getElementById('chat-project-text'); if (!textEl) return; - const id = getChatProjectSelection(); - textEl.textContent = id ? getProjectName(id) || id : '无项目'; + const id = resolveChatProjectSelection(); + textEl.textContent = id && projectNameById[id] ? projectNameById[id] : '无项目'; } function renderChatProjectPanelList() { const list = document.getElementById('chat-project-list'); if (!list) return; - const selected = getChatProjectSelection(); + const selected = resolveChatProjectSelection(); const activeProjects = projectsCache.filter((p) => p.status !== 'archived'); const items = [{ id: '', name: '无项目', description: '不绑定项目黑板' }, ...activeProjects]; if (!items.length) { @@ -839,6 +1068,7 @@ async function refreshChatProjectSelector() { if (!document.getElementById('chat-project-btn')) return; try { await ensureProjectsLoaded(); + await normalizeStaleChatProjectSelection(); } catch (e) { console.warn(e); } @@ -857,8 +1087,7 @@ async function onChatProjectChange() { function initChatProjectSelector() { if (window._chatProjectSelectorInited) return; window._chatProjectSelectorInited = true; - prefetchProjectsForChat(); - updateChatProjectButtonLabel(); + refreshChatProjectSelector().catch(() => {}); document.addEventListener('click', (e) => { const panel = document.getElementById('chat-project-panel'); const wrapper = document.querySelector('.project-selector-wrapper'); @@ -899,6 +1128,8 @@ window.prefetchProjectsForChat = prefetchProjectsForChat; window.getActiveProjectId = getActiveProjectId; window.getProjectName = getProjectName; window.viewProjectFactBody = viewProjectFactBody; +window.insertFactBodyTemplate = insertFactBodyTemplate; +window.updateFactFormHints = updateFactFormHints; window.deprecateProjectFactByKey = deprecateProjectFactByKey; window.openVulnerabilitiesForProject = openVulnerabilitiesForProject; window.openVulnerabilityDetail = openVulnerabilityDetail; diff --git a/web/templates/index.html b/web/templates/index.html index b196fb60..f2b30484 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -1467,12 +1467,12 @@
- Agent 每轮可见 key + 摘要;完整内容通过 get_project_fact 获取 + 索引仅含 key + 摘要(须含「什么+在哪+如何验证」);攻击链/POC 写在 body,Agent 通过 get_project_fact 复现
- +
Key分类摘要置信度更新操作
Key分类摘要Body置信度更新操作
@@ -3972,7 +3972,7 @@

添加事实

-

摘要会注入 Agent;完整内容通过 get_project_fact 获取

+

摘要注入黑板索引;body 沉淀攻击链与 POC,供审计复现(与漏洞记录分工)

@@ -3980,12 +3980,23 @@
- + +

环境类:target/、auth/、infra/、business/;发现/利用:finding/、chain/、exploit/、poc/

- +
@@ -3997,12 +4008,19 @@
- - + +
- - +
+ +
+ + +
+
+ +

@@ -4027,6 +4045,7 @@
+