mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-15 21:08:01 +02:00
Add files via upload
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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 更新漏洞请求
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = '<div class="loading-spinner">加载中...</div>';
|
||||
|
||||
@@ -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 = `<div class="error-message">加载失败: ${error.message}</div>`;
|
||||
@@ -121,6 +164,11 @@ function renderVulnerabilities(vulnerabilities) {
|
||||
|
||||
if (vulnerabilities.length === 0) {
|
||||
listContainer.innerHTML = '<div class="empty-state">暂无漏洞记录</div>';
|
||||
// 清空分页信息
|
||||
const paginationContainer = document.getElementById('vulnerability-pagination');
|
||||
if (paginationContainer) {
|
||||
paginationContainer.innerHTML = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -160,6 +208,13 @@ function renderVulnerabilities(vulnerabilities) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="vulnerability-actions" onclick="event.stopPropagation();">
|
||||
<button class="btn-ghost" onclick="downloadVulnerabilityAsMarkdown('${vuln.id}', event)" title="下载Markdown">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polyline points="7 10 12 15 17 10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="12" y1="15" x2="12" y2="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-ghost" onclick="editVulnerability('${vuln.id}')" title="编辑">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
@@ -192,6 +247,131 @@ function renderVulnerabilities(vulnerabilities) {
|
||||
listContainer.innerHTML = html;
|
||||
}
|
||||
|
||||
// 渲染分页控件
|
||||
function renderVulnerabilityPagination() {
|
||||
const paginationContainer = document.getElementById('vulnerability-pagination');
|
||||
if (!paginationContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { currentPage, totalPages, total, pageSize } = vulnerabilityPagination;
|
||||
|
||||
// 如果没有数据,不显示分页控件
|
||||
if (total === 0) {
|
||||
paginationContainer.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算显示的页码范围
|
||||
let startPage = Math.max(1, currentPage - 2);
|
||||
let endPage = Math.min(totalPages, currentPage + 2);
|
||||
|
||||
// 确保显示5个页码(如果可能)
|
||||
if (endPage - startPage < 4) {
|
||||
if (startPage === 1) {
|
||||
endPage = Math.min(totalPages, startPage + 4);
|
||||
} else if (endPage === totalPages) {
|
||||
startPage = Math.max(1, endPage - 4);
|
||||
}
|
||||
}
|
||||
|
||||
let paginationHTML = '<div class="pagination">';
|
||||
|
||||
// 显示总数和当前范围
|
||||
const startItem = (currentPage - 1) * pageSize + 1;
|
||||
const endItem = Math.min(currentPage * pageSize, total);
|
||||
paginationHTML += `<div class="pagination-info">显示 ${startItem}-${endItem} / 共 ${total} 条</div>`;
|
||||
|
||||
// 每页条数选择器(始终显示)
|
||||
const savedPageSize = getVulnerabilityPageSize();
|
||||
paginationHTML += `
|
||||
<div class="pagination-page-size">
|
||||
<label for="vulnerability-page-size-pagination">每页:</label>
|
||||
<select id="vulnerability-page-size-pagination" onchange="changeVulnerabilityPageSize()">
|
||||
<option value="10" ${savedPageSize === 10 ? 'selected' : ''}>10</option>
|
||||
<option value="20" ${savedPageSize === 20 ? 'selected' : ''}>20</option>
|
||||
<option value="50" ${savedPageSize === 50 ? 'selected' : ''}>50</option>
|
||||
<option value="100" ${savedPageSize === 100 ? 'selected' : ''}>100</option>
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 只有当有多页时才显示页码导航
|
||||
if (totalPages > 1) {
|
||||
paginationHTML += '<div class="pagination-controls">';
|
||||
|
||||
// 上一页按钮
|
||||
if (currentPage > 1) {
|
||||
paginationHTML += `<button class="pagination-btn" onclick="loadVulnerabilities(${currentPage - 1})" title="上一页">‹</button>`;
|
||||
} else {
|
||||
paginationHTML += '<button class="pagination-btn disabled" disabled>‹</button>';
|
||||
}
|
||||
|
||||
// 第一页
|
||||
if (startPage > 1) {
|
||||
paginationHTML += `<button class="pagination-btn" onclick="loadVulnerabilities(1)">1</button>`;
|
||||
if (startPage > 2) {
|
||||
paginationHTML += '<span class="pagination-ellipsis">...</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// 页码按钮
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
if (i === currentPage) {
|
||||
paginationHTML += `<button class="pagination-btn active">${i}</button>`;
|
||||
} else {
|
||||
paginationHTML += `<button class="pagination-btn" onclick="loadVulnerabilities(${i})">${i}</button>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 最后一页
|
||||
if (endPage < totalPages) {
|
||||
if (endPage < totalPages - 1) {
|
||||
paginationHTML += '<span class="pagination-ellipsis">...</span>';
|
||||
}
|
||||
paginationHTML += `<button class="pagination-btn" onclick="loadVulnerabilities(${totalPages})">${totalPages}</button>`;
|
||||
}
|
||||
|
||||
// 下一页按钮
|
||||
if (currentPage < totalPages) {
|
||||
paginationHTML += `<button class="pagination-btn" onclick="loadVulnerabilities(${currentPage + 1})" title="下一页">›</button>`;
|
||||
} else {
|
||||
paginationHTML += '<button class="pagination-btn disabled" disabled>›</button>';
|
||||
}
|
||||
|
||||
paginationHTML += '</div>';
|
||||
}
|
||||
|
||||
paginationHTML += '</div>';
|
||||
|
||||
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');
|
||||
|
||||
@@ -520,6 +520,9 @@
|
||||
<div id="vulnerabilities-list" class="vulnerabilities-list">
|
||||
<div class="loading-spinner">加载中...</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页控件 -->
|
||||
<div id="vulnerability-pagination"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user