From 3e0867d459dbed371a46565e31e05f0dddd43d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Sat, 27 Dec 2025 03:08:01 +0800 Subject: [PATCH] Add files via upload --- internal/database/vulnerability.go | 31 +++ internal/handler/vulnerability.go | 58 +++++- web/static/css/style.css | 45 ++++- web/static/js/vulnerability.js | 310 ++++++++++++++++++++++++++++- web/templates/index.html | 3 + 5 files changed, 437 insertions(+), 10 deletions(-) diff --git a/internal/database/vulnerability.go b/internal/database/vulnerability.go index 1423759b..c4ec69b2 100644 --- a/internal/database/vulnerability.go +++ b/internal/database/vulnerability.go @@ -145,6 +145,37 @@ func (db *DB) ListVulnerabilities(limit, offset int, id, conversationID, severit return vulnerabilities, nil } +// CountVulnerabilities 统计漏洞总数(支持筛选条件) +func (db *DB) CountVulnerabilities(id, conversationID, severity, status string) (int, error) { + query := "SELECT COUNT(*) FROM vulnerabilities WHERE 1=1" + args := []interface{}{} + + if id != "" { + query += " AND id = ?" + args = append(args, id) + } + if conversationID != "" { + query += " AND conversation_id = ?" + args = append(args, conversationID) + } + if severity != "" { + query += " AND severity = ?" + args = append(args, severity) + } + if status != "" { + query += " AND status = ?" + args = append(args, status) + } + + var count int + err := db.QueryRow(query, args...).Scan(&count) + if err != nil { + return 0, fmt.Errorf("统计漏洞总数失败: %w", err) + } + + return count, nil +} + // UpdateVulnerability 更新漏洞 func (db *DB) UpdateVulnerability(id string, vuln *Vulnerability) error { vuln.UpdatedAt = time.Now() diff --git a/internal/handler/vulnerability.go b/internal/handler/vulnerability.go index 3bd42af2..9975efa7 100644 --- a/internal/handler/vulnerability.go +++ b/internal/handler/vulnerability.go @@ -82,10 +82,20 @@ func (h *VulnerabilityHandler) GetVulnerability(c *gin.Context) { c.JSON(http.StatusOK, vuln) } +// ListVulnerabilitiesResponse 漏洞列表响应 +type ListVulnerabilitiesResponse struct { + Vulnerabilities []*database.Vulnerability `json:"vulnerabilities"` + Total int `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + TotalPages int `json:"total_pages"` +} + // ListVulnerabilities 列出漏洞 func (h *VulnerabilityHandler) ListVulnerabilities(c *gin.Context) { - limitStr := c.DefaultQuery("limit", "50") + limitStr := c.DefaultQuery("limit", "20") offsetStr := c.DefaultQuery("offset", "0") + pageStr := c.Query("page") id := c.Query("id") conversationID := c.Query("conversation_id") severity := c.Query("severity") @@ -93,11 +103,32 @@ func (h *VulnerabilityHandler) ListVulnerabilities(c *gin.Context) { limit, _ := strconv.Atoi(limitStr) offset, _ := strconv.Atoi(offsetStr) + page := 1 - if limit <= 0 || limit > 100 { - limit = 50 + // 如果提供了page参数,优先使用page计算offset + if pageStr != "" { + if p, err := strconv.Atoi(pageStr); err == nil && p > 0 { + page = p + offset = (page - 1) * limit + } } + if limit <= 0 || limit > 100 { + limit = 20 + } + if offset < 0 { + offset = 0 + } + + // 获取总数 + total, err := h.db.CountVulnerabilities(id, conversationID, severity, status) + if err != nil { + h.logger.Error("获取漏洞总数失败", zap.Error(err)) + // 继续执行,使用0作为总数 + total = 0 + } + + // 获取漏洞列表 vulnerabilities, err := h.db.ListVulnerabilities(limit, offset, id, conversationID, severity, status) if err != nil { h.logger.Error("获取漏洞列表失败", zap.Error(err)) @@ -105,7 +136,26 @@ func (h *VulnerabilityHandler) ListVulnerabilities(c *gin.Context) { return } - c.JSON(http.StatusOK, vulnerabilities) + // 计算总页数 + totalPages := (total + limit - 1) / limit + if totalPages == 0 { + totalPages = 1 + } + + // 如果使用offset计算page,需要重新计算 + if pageStr == "" { + page = (offset / limit) + 1 + } + + response := ListVulnerabilitiesResponse{ + Vulnerabilities: vulnerabilities, + Total: total, + Page: page, + PageSize: limit, + TotalPages: totalPages, + } + + c.JSON(http.StatusOK, response) } // UpdateVulnerabilityRequest 更新漏洞请求 diff --git a/web/static/css/style.css b/web/static/css/style.css index 2b0834e7..81705980 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -2951,7 +2951,8 @@ header { } .tools-pagination, -.monitor-pagination { +.monitor-pagination, +.pagination { display: flex; justify-content: space-between; align-items: center; @@ -2980,11 +2981,51 @@ header { min-width: auto; } -.pagination-controls button:disabled { +.pagination-controls button:disabled, +.pagination-btn.disabled { opacity: 0.5; cursor: not-allowed; } +.pagination-btn { + padding: 6px 12px; + font-size: 0.875rem; + min-width: 36px; + height: 32px; + border: 1px solid var(--border-color); + border-radius: 6px; + background: var(--bg-primary); + color: var(--text-primary); + cursor: pointer; + transition: all 0.2s; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.pagination-btn:hover:not(.disabled):not(.active) { + background: var(--bg-tertiary); + border-color: var(--accent-color); +} + +.pagination-btn.active { + background: var(--accent-color); + color: white; + border-color: var(--accent-color); + font-weight: 500; +} + +.pagination-btn.disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.pagination-ellipsis { + padding: 0 8px; + color: var(--text-secondary); + font-size: 0.875rem; +} + .pagination-page { font-size: 0.875rem; color: var(--text-secondary); diff --git a/web/static/js/vulnerability.js b/web/static/js/vulnerability.js index 94b21b01..c75473b3 100644 --- a/web/static/js/vulnerability.js +++ b/web/static/js/vulnerability.js @@ -1,5 +1,11 @@ // 漏洞管理相关功能 +// 从localStorage读取每页显示数量,默认为20 +const getVulnerabilityPageSize = () => { + const saved = localStorage.getItem('vulnerabilityPageSize'); + return saved ? parseInt(saved, 10) : 20; +}; + let currentVulnerabilityId = null; let vulnerabilityFilters = { id: '', @@ -7,9 +13,17 @@ let vulnerabilityFilters = { severity: '', status: '' }; +let vulnerabilityPagination = { + currentPage: 1, + pageSize: getVulnerabilityPageSize(), + total: 0, + totalPages: 1 +}; // 初始化漏洞管理页面 function initVulnerabilityPage() { + // 从localStorage加载每页条数设置 + vulnerabilityPagination.pageSize = getVulnerabilityPageSize(); loadVulnerabilityStats(); loadVulnerabilities(); } @@ -66,7 +80,7 @@ function updateVulnerabilityStats(stats) { } // 加载漏洞列表 -async function loadVulnerabilities() { +async function loadVulnerabilities(page = null) { const listContainer = document.getElementById('vulnerabilities-list'); listContainer.innerHTML = '
加载中...
'; @@ -77,9 +91,14 @@ async function loadVulnerabilities() { throw new Error('apiFetch未定义'); } + // 如果指定了页码,使用页码;否则使用当前页码 + if (page !== null) { + vulnerabilityPagination.currentPage = page; + } + const params = new URLSearchParams(); - params.append('limit', '100'); - params.append('offset', '0'); + params.append('page', vulnerabilityPagination.currentPage.toString()); + params.append('limit', vulnerabilityPagination.pageSize.toString()); if (vulnerabilityFilters.id) { params.append('id', vulnerabilityFilters.id); @@ -101,8 +120,32 @@ async function loadVulnerabilities() { throw new Error(`获取漏洞列表失败: ${response.status}`); } - const vulnerabilities = await response.json(); + const data = await response.json(); + + // 判断响应格式:新格式(有total字段)还是旧格式(直接是数组) + let vulnerabilities; + if (Array.isArray(data)) { + // 旧格式:直接是数组 + vulnerabilities = data; + // 使用数组长度作为总数(可能不准确,但至少能显示分页控件) + vulnerabilityPagination.total = data.length; + vulnerabilityPagination.totalPages = Math.max(1, Math.ceil(data.length / vulnerabilityPagination.pageSize)); + console.warn('后端返回的是旧格式(数组),建议更新后端API以支持分页'); + } else if (data.vulnerabilities) { + // 新格式:包含分页信息的对象 + vulnerabilities = data.vulnerabilities; + vulnerabilityPagination.total = data.total || 0; + vulnerabilityPagination.currentPage = data.page || vulnerabilityPagination.currentPage; + vulnerabilityPagination.pageSize = data.page_size || vulnerabilityPagination.pageSize; + vulnerabilityPagination.totalPages = data.total_pages || 1; + } else { + // 未知格式,尝试作为数组处理 + vulnerabilities = []; + console.error('未知的响应格式:', data); + } + renderVulnerabilities(vulnerabilities); + renderVulnerabilityPagination(); } catch (error) { console.error('加载漏洞列表失败:', error); listContainer.innerHTML = `
加载失败: ${error.message}
`; @@ -121,6 +164,11 @@ function renderVulnerabilities(vulnerabilities) { if (vulnerabilities.length === 0) { listContainer.innerHTML = '
暂无漏洞记录
'; + // 清空分页信息 + const paginationContainer = document.getElementById('vulnerability-pagination'); + if (paginationContainer) { + paginationContainer.innerHTML = ''; + } return; } @@ -160,6 +208,13 @@ function renderVulnerabilities(vulnerabilities) {
+ `; + } else { + paginationHTML += ''; + } + + // 第一页 + if (startPage > 1) { + paginationHTML += ``; + if (startPage > 2) { + paginationHTML += '...'; + } + } + + // 页码按钮 + for (let i = startPage; i <= endPage; i++) { + if (i === currentPage) { + paginationHTML += ``; + } else { + paginationHTML += ``; + } + } + + // 最后一页 + if (endPage < totalPages) { + if (endPage < totalPages - 1) { + paginationHTML += '...'; + } + paginationHTML += ``; + } + + // 下一页按钮 + if (currentPage < totalPages) { + paginationHTML += ``; + } else { + paginationHTML += ''; + } + + paginationHTML += '
'; + } + + paginationHTML += ''; + + paginationContainer.innerHTML = paginationHTML; +} + +// 改变每页显示数量 +async function changeVulnerabilityPageSize() { + const pageSizeSelect = document.getElementById('vulnerability-page-size-pagination'); + if (!pageSizeSelect) return; + + const newPageSize = parseInt(pageSizeSelect.value, 10); + if (isNaN(newPageSize) || newPageSize < 1) { + return; + } + + // 保存到localStorage + localStorage.setItem('vulnerabilityPageSize', newPageSize.toString()); + + // 更新分页配置 + vulnerabilityPagination.pageSize = newPageSize; + + // 重新计算当前页(保持显示的数据范围尽可能接近) + const currentStartItem = (vulnerabilityPagination.currentPage - 1) * vulnerabilityPagination.pageSize + 1; + const newPage = Math.max(1, Math.floor((currentStartItem - 1) / newPageSize) + 1); + vulnerabilityPagination.currentPage = newPage; + + // 重新加载数据 + await loadVulnerabilities(); +} + // 显示添加漏洞模态框 function showAddVulnerabilityModal() { currentVulnerabilityId = null; @@ -286,6 +466,8 @@ async function saveVulnerability() { closeVulnerabilityModal(); loadVulnerabilityStats(); + // 保存/更新后,重置到第一页 + vulnerabilityPagination.currentPage = 1; loadVulnerabilities(); } catch (error) { console.error('保存漏洞失败:', error); @@ -307,6 +489,13 @@ async function deleteVulnerability(id) { if (!response.ok) throw new Error('删除失败'); loadVulnerabilityStats(); + // 删除后,如果当前页没有数据了,回到上一页 + if (vulnerabilityPagination.currentPage > 1 && vulnerabilityPagination.total > 0) { + const itemsOnCurrentPage = vulnerabilityPagination.total - (vulnerabilityPagination.currentPage - 1) * vulnerabilityPagination.pageSize; + if (itemsOnCurrentPage <= 1) { + vulnerabilityPagination.currentPage--; + } + } loadVulnerabilities(); } catch (error) { console.error('删除漏洞失败:', error); @@ -327,6 +516,9 @@ function filterVulnerabilities() { vulnerabilityFilters.severity = document.getElementById('vulnerability-severity-filter').value; vulnerabilityFilters.status = document.getElementById('vulnerability-status-filter').value; + // 重置到第一页 + vulnerabilityPagination.currentPage = 1; + loadVulnerabilityStats(); loadVulnerabilities(); } @@ -345,6 +537,9 @@ function clearVulnerabilityFilters() { status: '' }; + // 重置到第一页 + vulnerabilityPagination.currentPage = 1; + loadVulnerabilityStats(); loadVulnerabilities(); } @@ -378,6 +573,113 @@ function escapeHtml(text) { return div.innerHTML; } +// 将漏洞格式化为Markdown +function formatVulnerabilityAsMarkdown(vuln) { + const severityText = { + 'critical': '严重', + 'high': '高危', + 'medium': '中危', + 'low': '低危', + 'info': '信息' + }[vuln.severity] || vuln.severity; + + const statusText = { + 'open': '待处理', + 'confirmed': '已确认', + 'fixed': '已修复', + 'false_positive': '误报' + }[vuln.status] || vuln.status; + + const createdDate = new Date(vuln.created_at).toLocaleString('zh-CN'); + const updatedDate = new Date(vuln.updated_at).toLocaleString('zh-CN'); + + let markdown = `# ${vuln.title}\n\n`; + + markdown += `## 基本信息\n\n`; + markdown += `- **漏洞ID**: \`${vuln.id}\`\n`; + markdown += `- **严重程度**: ${severityText}\n`; + markdown += `- **状态**: ${statusText}\n`; + if (vuln.type) { + markdown += `- **类型**: ${vuln.type}\n`; + } + if (vuln.target) { + markdown += `- **目标**: ${vuln.target}\n`; + } + markdown += `- **会话ID**: \`${vuln.conversation_id}\`\n`; + markdown += `- **创建时间**: ${createdDate}\n`; + markdown += `- **更新时间**: ${updatedDate}\n\n`; + + if (vuln.description) { + markdown += `## 描述\n\n${vuln.description}\n\n`; + } + + if (vuln.proof) { + markdown += `## 证明(POC)\n\n\`\`\`\n${vuln.proof}\n\`\`\`\n\n`; + } + + if (vuln.impact) { + markdown += `## 影响\n\n${vuln.impact}\n\n`; + } + + if (vuln.recommendation) { + markdown += `## 修复建议\n\n${vuln.recommendation}\n\n`; + } + + return markdown; +} + +// 下载漏洞为Markdown格式 +async function downloadVulnerabilityAsMarkdown(id, event) { + try { + const response = await apiFetch(`/api/vulnerabilities/${id}`); + if (!response.ok) { + throw new Error('获取漏洞失败'); + } + + const vuln = await response.json(); + const markdown = formatVulnerabilityAsMarkdown(vuln); + + // 创建Blob对象 + const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8' }); + + // 创建下载链接 + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + + // 生成文件名(使用漏洞标题,清理特殊字符,保留中文) + const cleanTitle = vuln.title + .replace(/[<>:"/\\|?*]/g, '') // 移除Windows不允许的字符 + .replace(/\s+/g, '_') // 空格替换为下划线 + .substring(0, 50); // 限制长度 + const fileName = `${cleanTitle}_${vuln.id.substring(0, 8)}.md`; + link.download = fileName; + + // 触发下载 + document.body.appendChild(link); + link.click(); + + // 清理 + document.body.removeChild(link); + URL.revokeObjectURL(url); + + // 显示成功提示 + if (event && event.target) { + const button = event.target.closest('button'); + if (button) { + const originalTitle = button.title || '下载Markdown'; + button.title = '下载成功!'; + setTimeout(() => { + button.title = originalTitle; + }, 2000); + } + } + } catch (error) { + console.error('下载失败:', error); + alert('下载失败: ' + error.message); + } +} + // 点击模态框外部关闭 window.onclick = function(event) { const modal = document.getElementById('vulnerability-modal'); diff --git a/web/templates/index.html b/web/templates/index.html index 8e53ca10..0683228b 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -520,6 +520,9 @@
加载中...
+ + +