mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-11 10:18:56 +02:00
Add files via upload
This commit is contained in:
3 files changed
+392
-24
No files matched your search
@@ -21755,6 +21755,55 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
|
|||||||
background: #f1f5f9;
|
background: #f1f5f9;
|
||||||
color: #64748b;
|
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 {
|
.projects-severity {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 0.6875rem;
|
font-size: 0.6875rem;
|
||||||
@@ -21928,6 +21977,75 @@ body.projects-modal-open {
|
|||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
color: #334155;
|
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 {
|
.vulnerability-filter-field--project select {
|
||||||
min-width: 120px;
|
min-width: 120px;
|
||||||
max-width: 160px;
|
max-width: 160px;
|
||||||
|
|||||||
+246
-15
@@ -11,6 +11,129 @@ let _projectsFetchPromise = null;
|
|||||||
|
|
||||||
const PROJECT_ACTIVE_KEY = 'cyberstrike.activeProjectId';
|
const PROJECT_ACTIVE_KEY = 'cyberstrike.activeProjectId';
|
||||||
|
|
||||||
|
/** 与后端 internal/project/fact_template.go 对齐 */
|
||||||
|
const FACT_ATTACK_CHAIN_BODY_TEMPLATE = `## 结论(可验证,一句话)
|
||||||
|
<勿仅写「存在漏洞」;写明类型 + 位置 + 触发条件>
|
||||||
|
|
||||||
|
## 目标与入口
|
||||||
|
- 目标: <URL / IP:Port / 主机名>
|
||||||
|
- 入口: <路径 / 接口 / 参数>
|
||||||
|
- 前置条件: <匿名 / 角色 / Cookie / 其他依赖>
|
||||||
|
|
||||||
|
## 攻击链(逐步可复现)
|
||||||
|
1. <侦察/发现>
|
||||||
|
2. <利用/触发>
|
||||||
|
3. <影响证明(读文件、RCE 回显、越权数据等)>
|
||||||
|
|
||||||
|
## Exploit / POC
|
||||||
|
### 请求
|
||||||
|
\`\`\`http
|
||||||
|
<METHOD> <path> HTTP/1.1
|
||||||
|
Host: ...
|
||||||
|
...
|
||||||
|
|
||||||
|
<body>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### 响应 / 现象
|
||||||
|
<关键响应片段、状态码、差异点>
|
||||||
|
|
||||||
|
### 命令 / 脚本(如有)
|
||||||
|
\`\`\`bash
|
||||||
|
<command>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## 关键证据
|
||||||
|
- <工具输出摘要 / 截图路径 / 会话或消息 ID>
|
||||||
|
|
||||||
|
## 关联
|
||||||
|
- related_vulnerability_id: <可选>
|
||||||
|
- 依赖事实: <fact_key,如 auth/session_cookie>
|
||||||
|
|
||||||
|
## 备注与不确定性
|
||||||
|
<待验证假设、环境差异、绕过尝试记录>`;
|
||||||
|
|
||||||
|
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 `<span class="projects-fact-badge projects-fact-badge--na" title="环境类事实">${hasBody ? '有详情' : '—'}</span>`;
|
||||||
|
}
|
||||||
|
if (isSparseFactBody(f.category, f.fact_key, f.body)) {
|
||||||
|
return '<span class="projects-fact-badge projects-fact-badge--warn" title="缺少攻击链/POC 结构">待补全</span>';
|
||||||
|
}
|
||||||
|
return '<span class="projects-fact-badge projects-fact-badge--ok" title="含可复现结构">可复现</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
function getActiveProjectId() {
|
||||||
try {
|
try {
|
||||||
return localStorage.getItem(PROJECT_ACTIVE_KEY) || '';
|
return localStorage.getItem(PROJECT_ACTIVE_KEY) || '';
|
||||||
@@ -126,6 +249,27 @@ function updateProjectsListCount() {
|
|||||||
if (el) el.textContent = String(projectsCache.length);
|
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 `<span class="projects-category ${cls}">${escapeHtml(raw || '—')}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
function formatConfidenceBadge(confidence) {
|
function formatConfidenceBadge(confidence) {
|
||||||
const c = (confidence || '').toLowerCase();
|
const c = (confidence || '').toLowerCase();
|
||||||
let cls = 'projects-confidence--tentative';
|
let cls = 'projects-confidence--tentative';
|
||||||
@@ -268,15 +412,15 @@ function switchProjectTab(tab) {
|
|||||||
async function loadProjectFacts() {
|
async function loadProjectFacts() {
|
||||||
const tbody = document.getElementById('project-facts-tbody');
|
const tbody = document.getElementById('project-facts-tbody');
|
||||||
if (!tbody || !currentProjectId) return;
|
if (!tbody || !currentProjectId) return;
|
||||||
tbody.innerHTML = '<tr class="is-empty-row"><td colspan="6">加载中…</td></tr>';
|
tbody.innerHTML = '<tr class="is-empty-row"><td colspan="7">加载中…</td></tr>';
|
||||||
const res = await apiFetch(`/api/projects/${currentProjectId}/facts?limit=200`);
|
const res = await apiFetch(`/api/projects/${currentProjectId}/facts?limit=200`);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
tbody.innerHTML = '<tr class="is-empty-row"><td colspan="6">加载失败</td></tr>';
|
tbody.innerHTML = '<tr class="is-empty-row"><td colspan="7">加载失败</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const facts = await res.json();
|
const facts = await res.json();
|
||||||
if (!facts.length) {
|
if (!facts.length) {
|
||||||
tbody.innerHTML = '<tr class="is-empty-row"><td colspan="6">暂无事实,点击「添加事实」或由 Agent 自动写入</td></tr>';
|
tbody.innerHTML = '<tr class="is-empty-row"><td colspan="7">暂无事实,点击「添加事实」或由 Agent 自动写入</td></tr>';
|
||||||
refreshProjectHeaderStats();
|
refreshProjectHeaderStats();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -285,8 +429,9 @@ async function loadProjectFacts() {
|
|||||||
const idEsc = escapeHtml(f.id);
|
const idEsc = escapeHtml(f.id);
|
||||||
return `<tr>
|
return `<tr>
|
||||||
<td><code>${keyEsc}</code></td>
|
<td><code>${keyEsc}</code></td>
|
||||||
<td>${escapeHtml(f.category)}</td>
|
<td>${formatCategoryBadge(f.category)}</td>
|
||||||
<td class="cell-summary" title="${escapeHtml(f.summary)}">${escapeHtml(f.summary)}</td>
|
<td class="cell-summary" title="${escapeHtml(f.summary)}">${escapeHtml(f.summary)}</td>
|
||||||
|
<td>${formatFactBodyBadge(f)}</td>
|
||||||
<td>${formatConfidenceBadge(f.confidence)}</td>
|
<td>${formatConfidenceBadge(f.confidence)}</td>
|
||||||
<td>${formatProjectTime(f.updated_at, f.created_at)}</td>
|
<td>${formatProjectTime(f.updated_at, f.created_at)}</td>
|
||||||
<td class="col-actions">${renderProjectFactActions(keyEsc, idEsc)}</td>
|
<td class="col-actions">${renderProjectFactActions(keyEsc, idEsc)}</td>
|
||||||
@@ -327,10 +472,26 @@ async function viewProjectFactBody(factKey) {
|
|||||||
const f = await res.json();
|
const f = await res.json();
|
||||||
_factDetailKey = f.fact_key;
|
_factDetailKey = f.fact_key;
|
||||||
document.getElementById('fact-detail-title').textContent = `[${f.fact_key}]`;
|
document.getElementById('fact-detail-title').textContent = `[${f.fact_key}]`;
|
||||||
document.getElementById('fact-detail-meta').textContent =
|
const metaParts = [
|
||||||
`分类: ${f.category} · 置信度: ${f.confidence} · 更新: ${formatProjectTime(f.updated_at, f.created_at)}` +
|
`分类: ${f.category}`,
|
||||||
(f.related_vulnerability_id ? ` · 关联漏洞: ${f.related_vulnerability_id}` : '');
|
`置信度: ${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)';
|
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');
|
openProjectsOverlay('fact-detail-modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,6 +723,7 @@ function resetFactModalForm() {
|
|||||||
document.getElementById('fact-modal-confidence').value = 'tentative';
|
document.getElementById('fact-modal-confidence').value = 'tentative';
|
||||||
const rel = document.getElementById('fact-modal-related-vuln');
|
const rel = document.getElementById('fact-modal-related-vuln');
|
||||||
if (rel) rel.value = '';
|
if (rel) rel.value = '';
|
||||||
|
updateFactFormHints();
|
||||||
}
|
}
|
||||||
|
|
||||||
function fillFactModalForm(f) {
|
function fillFactModalForm(f) {
|
||||||
@@ -569,7 +731,19 @@ function fillFactModalForm(f) {
|
|||||||
document.getElementById('fact-modal-title').textContent = '编辑事实';
|
document.getElementById('fact-modal-title').textContent = '编辑事实';
|
||||||
document.getElementById('fact-modal-submit-btn').textContent = '保存修改';
|
document.getElementById('fact-modal-submit-btn').textContent = '保存修改';
|
||||||
document.getElementById('fact-modal-key').value = f.fact_key || '';
|
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-summary').value = f.summary || '';
|
||||||
document.getElementById('fact-modal-body').value = f.body || '';
|
document.getElementById('fact-modal-body').value = f.body || '';
|
||||||
const conf = (f.confidence || 'tentative').toLowerCase();
|
const conf = (f.confidence || 'tentative').toLowerCase();
|
||||||
@@ -580,6 +754,7 @@ function fillFactModalForm(f) {
|
|||||||
}
|
}
|
||||||
const rel = document.getElementById('fact-modal-related-vuln');
|
const rel = document.getElementById('fact-modal-related-vuln');
|
||||||
if (rel) rel.value = f.related_vulnerability_id || '';
|
if (rel) rel.value = f.related_vulnerability_id || '';
|
||||||
|
updateFactFormHints();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAddFactModal() {
|
function showAddFactModal() {
|
||||||
@@ -608,12 +783,20 @@ function closeFactModal() {
|
|||||||
async function saveFactModal() {
|
async function saveFactModal() {
|
||||||
const fact_key = document.getElementById('fact-modal-key').value.trim();
|
const fact_key = document.getElementById('fact-modal-key').value.trim();
|
||||||
const summary = document.getElementById('fact-modal-summary').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 (!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 = {
|
const payload = {
|
||||||
fact_key,
|
fact_key,
|
||||||
category: document.getElementById('fact-modal-category').value.trim() || 'note',
|
category,
|
||||||
summary,
|
summary,
|
||||||
body: document.getElementById('fact-modal-body').value,
|
body,
|
||||||
confidence: document.getElementById('fact-modal-confidence').value,
|
confidence: document.getElementById('fact-modal-confidence').value,
|
||||||
related_vulnerability_id: document.getElementById('fact-modal-related-vuln')?.value?.trim() || '',
|
related_vulnerability_id: document.getElementById('fact-modal-related-vuln')?.value?.trim() || '',
|
||||||
};
|
};
|
||||||
@@ -708,17 +891,63 @@ function getChatProjectSelection() {
|
|||||||
return getActiveProjectId();
|
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() {
|
function updateChatProjectButtonLabel() {
|
||||||
const textEl = document.getElementById('chat-project-text');
|
const textEl = document.getElementById('chat-project-text');
|
||||||
if (!textEl) return;
|
if (!textEl) return;
|
||||||
const id = getChatProjectSelection();
|
const id = resolveChatProjectSelection();
|
||||||
textEl.textContent = id ? getProjectName(id) || id : '无项目';
|
textEl.textContent = id && projectNameById[id] ? projectNameById[id] : '无项目';
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderChatProjectPanelList() {
|
function renderChatProjectPanelList() {
|
||||||
const list = document.getElementById('chat-project-list');
|
const list = document.getElementById('chat-project-list');
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
const selected = getChatProjectSelection();
|
const selected = resolveChatProjectSelection();
|
||||||
const activeProjects = projectsCache.filter((p) => p.status !== 'archived');
|
const activeProjects = projectsCache.filter((p) => p.status !== 'archived');
|
||||||
const items = [{ id: '', name: '无项目', description: '不绑定项目黑板' }, ...activeProjects];
|
const items = [{ id: '', name: '无项目', description: '不绑定项目黑板' }, ...activeProjects];
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
@@ -839,6 +1068,7 @@ async function refreshChatProjectSelector() {
|
|||||||
if (!document.getElementById('chat-project-btn')) return;
|
if (!document.getElementById('chat-project-btn')) return;
|
||||||
try {
|
try {
|
||||||
await ensureProjectsLoaded();
|
await ensureProjectsLoaded();
|
||||||
|
await normalizeStaleChatProjectSelection();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
}
|
}
|
||||||
@@ -857,8 +1087,7 @@ async function onChatProjectChange() {
|
|||||||
function initChatProjectSelector() {
|
function initChatProjectSelector() {
|
||||||
if (window._chatProjectSelectorInited) return;
|
if (window._chatProjectSelectorInited) return;
|
||||||
window._chatProjectSelectorInited = true;
|
window._chatProjectSelectorInited = true;
|
||||||
prefetchProjectsForChat();
|
refreshChatProjectSelector().catch(() => {});
|
||||||
updateChatProjectButtonLabel();
|
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
const panel = document.getElementById('chat-project-panel');
|
const panel = document.getElementById('chat-project-panel');
|
||||||
const wrapper = document.querySelector('.project-selector-wrapper');
|
const wrapper = document.querySelector('.project-selector-wrapper');
|
||||||
@@ -899,6 +1128,8 @@ window.prefetchProjectsForChat = prefetchProjectsForChat;
|
|||||||
window.getActiveProjectId = getActiveProjectId;
|
window.getActiveProjectId = getActiveProjectId;
|
||||||
window.getProjectName = getProjectName;
|
window.getProjectName = getProjectName;
|
||||||
window.viewProjectFactBody = viewProjectFactBody;
|
window.viewProjectFactBody = viewProjectFactBody;
|
||||||
|
window.insertFactBodyTemplate = insertFactBodyTemplate;
|
||||||
|
window.updateFactFormHints = updateFactFormHints;
|
||||||
window.deprecateProjectFactByKey = deprecateProjectFactByKey;
|
window.deprecateProjectFactByKey = deprecateProjectFactByKey;
|
||||||
window.openVulnerabilitiesForProject = openVulnerabilitiesForProject;
|
window.openVulnerabilitiesForProject = openVulnerabilitiesForProject;
|
||||||
window.openVulnerabilityDetail = openVulnerabilityDetail;
|
window.openVulnerabilityDetail = openVulnerabilityDetail;
|
||||||
|
|||||||
@@ -1467,12 +1467,12 @@
|
|||||||
</nav>
|
</nav>
|
||||||
<div id="project-panel-facts" class="projects-panel" role="tabpanel">
|
<div id="project-panel-facts" class="projects-panel" role="tabpanel">
|
||||||
<div class="projects-panel-toolbar">
|
<div class="projects-panel-toolbar">
|
||||||
<span class="projects-panel-hint">Agent 每轮可见 key + 摘要;完整内容通过 get_project_fact 获取</span>
|
<span class="projects-panel-hint">索引仅含 key + 摘要(须含「什么+在哪+如何验证」);攻击链/POC 写在 body,Agent 通过 get_project_fact 复现</span>
|
||||||
<button class="btn-primary btn-small" type="button" onclick="showAddFactModal()">+ 添加事实</button>
|
<button class="btn-primary btn-small" type="button" onclick="showAddFactModal()">+ 添加事实</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="projects-table-wrap">
|
<div class="projects-table-wrap">
|
||||||
<table class="data-table data-table--projects">
|
<table class="data-table data-table--projects">
|
||||||
<thead><tr><th>Key</th><th>分类</th><th>摘要</th><th>置信度</th><th>更新</th><th class="col-actions">操作</th></tr></thead>
|
<thead><tr><th>Key</th><th>分类</th><th>摘要</th><th>Body</th><th>置信度</th><th>更新</th><th class="col-actions">操作</th></tr></thead>
|
||||||
<tbody id="project-facts-tbody"></tbody>
|
<tbody id="project-facts-tbody"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -3972,7 +3972,7 @@
|
|||||||
<div class="projects-modal-header-text">
|
<div class="projects-modal-header-text">
|
||||||
<div>
|
<div>
|
||||||
<h3 id="fact-modal-title">添加事实</h3>
|
<h3 id="fact-modal-title">添加事实</h3>
|
||||||
<p class="projects-modal-subtitle">摘要会注入 Agent;完整内容通过 get_project_fact 获取</p>
|
<p class="projects-modal-subtitle">摘要注入黑板索引;body 沉淀攻击链与 POC,供审计复现(与漏洞记录分工)</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="projects-modal-close" onclick="closeFactModal()" aria-label="关闭">×</button>
|
<button type="button" class="projects-modal-close" onclick="closeFactModal()" aria-label="关闭">×</button>
|
||||||
@@ -3980,12 +3980,23 @@
|
|||||||
<div class="projects-modal-body">
|
<div class="projects-modal-body">
|
||||||
<div class="projects-form-field">
|
<div class="projects-form-field">
|
||||||
<label for="fact-modal-key">fact_key</label>
|
<label for="fact-modal-key">fact_key</label>
|
||||||
<input type="text" id="fact-modal-key" class="form-input" placeholder="target/primary_domain">
|
<input type="text" id="fact-modal-key" class="form-input" placeholder="target/primary_domain 或 finding/sqli-login" oninput="updateFactFormHints()">
|
||||||
|
<p class="projects-field-hint">环境类:target/、auth/、infra/、business/;发现/利用:finding/、chain/、exploit/、poc/</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="projects-form-row">
|
<div class="projects-form-row">
|
||||||
<div class="projects-form-field">
|
<div class="projects-form-field">
|
||||||
<label for="fact-modal-category">分类</label>
|
<label for="fact-modal-category">分类</label>
|
||||||
<input type="text" id="fact-modal-category" class="form-input" value="note">
|
<select id="fact-modal-category" class="form-input" onchange="updateFactFormHints()">
|
||||||
|
<option value="target">target(目标)</option>
|
||||||
|
<option value="auth">auth(认证)</option>
|
||||||
|
<option value="infra">infra(基础设施)</option>
|
||||||
|
<option value="business">business(业务)</option>
|
||||||
|
<option value="finding">finding(发现)</option>
|
||||||
|
<option value="chain">chain(攻击链)</option>
|
||||||
|
<option value="exploit">exploit(利用)</option>
|
||||||
|
<option value="poc">poc(POC)</option>
|
||||||
|
<option value="note" selected>note(备注)</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="projects-form-field">
|
<div class="projects-form-field">
|
||||||
<label for="fact-modal-confidence">置信度</label>
|
<label for="fact-modal-confidence">置信度</label>
|
||||||
@@ -3997,12 +4008,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="projects-form-field">
|
<div class="projects-form-field">
|
||||||
<label for="fact-modal-summary">摘要</label>
|
<label for="fact-modal-summary">摘要(索引一行)</label>
|
||||||
<input type="text" id="fact-modal-summary" class="form-input" placeholder="一行概述,会注入到 Agent 上下文">
|
<input type="text" id="fact-modal-summary" class="form-input" maxlength="400" placeholder="什么 + 在哪 + 如何验证(勿仅写「存在 XSS」)" oninput="updateFactFormHints()">
|
||||||
</div>
|
</div>
|
||||||
<div class="projects-form-field">
|
<div class="projects-form-field">
|
||||||
<label for="fact-modal-body">body(完整详情)</label>
|
<div class="projects-form-label-row">
|
||||||
<textarea id="fact-modal-body" class="form-input" rows="5" placeholder="POC、长文本、原始输出等"></textarea>
|
<label for="fact-modal-body">body(可复现详情)</label>
|
||||||
|
<div class="projects-form-label-actions">
|
||||||
|
<button type="button" class="btn-link btn-small" onclick="insertFactBodyTemplate('attack')">插入攻击链模板</button>
|
||||||
|
<button type="button" class="btn-link btn-small" onclick="insertFactBodyTemplate('env')">插入环境模板</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea id="fact-modal-body" class="form-input fact-modal-body-input" rows="14" placeholder="攻击链步骤、HTTP/命令 POC、响应现象、证据…" oninput="updateFactFormHints()"></textarea>
|
||||||
|
<p id="fact-modal-body-hint" class="projects-field-hint" role="status"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="projects-form-field">
|
<div class="projects-form-field">
|
||||||
<label for="fact-modal-related-vuln">关联漏洞 ID</label>
|
<label for="fact-modal-related-vuln">关联漏洞 ID</label>
|
||||||
@@ -4027,6 +4045,7 @@
|
|||||||
<button type="button" class="projects-modal-close" onclick="closeFactDetailModal()" aria-label="关闭">×</button>
|
<button type="button" class="projects-modal-close" onclick="closeFactDetailModal()" aria-label="关闭">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="projects-modal-body">
|
<div class="projects-modal-body">
|
||||||
|
<p id="fact-detail-sparse-warn" class="projects-fact-sparse-warn" hidden></p>
|
||||||
<pre id="fact-detail-body" class="fact-detail-body"></pre>
|
<pre id="fact-detail-body" class="fact-detail-body"></pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="projects-modal-footer">
|
<div class="projects-modal-footer">
|
||||||
|
|||||||
Reference in new issue
Block a user