feat(findings): 发现页支持导出(Markdown 单文件/分文件zip、CSV、JSON,按筛选/全部/勾选)
- 后端新增 GET /exploration/findings/export:按 scope(filtered|all|selected) × format(md-single|md-zip|csv|json) 导出,md-zip 用 archive/zip 后端打包 - db.ListFindingsForExport:带完整 report、不分页,按严重等级+时间排序 - report/findings.go:汇总/单条 Markdown、安全文件名、带 BOM 的 CSV 渲染 - 前端发现页加行勾选 + 导出弹窗(范围×格式);复用 lib/utils.copyText - 顺带修复报告复制在 HTTP 非安全上下文失效(execCommand 降级)+ 复用 CopyButton Claude-Session: https://claude.ai/code/session_01Bs2rrsVHUBTxRusm9espwB
This commit is contained in:
1 parent
ed96f256a4
commit
43f40dd74f
9 files changed
+658
-37
No files matched your search
@@ -210,6 +210,57 @@ func (d *DB) ListFindingsPage(f FindingFilter, page, pageSize int) ([]*DBFinding
|
||||
return out, total, err
|
||||
}
|
||||
|
||||
// ListFindingsForExport returns findings for the 发现 page 导出功能,携带完整
|
||||
// report 字段、不分页。ids 非空时按这批 finding id 精确导出(勾选导出),忽略
|
||||
// filter;ids 为空时按 filter 导出(导出当前筛选/全部)。结果按严重等级降序、
|
||||
// 再按时间倒序,与「导出汇总报告」的分组顺序一致。
|
||||
func (d *DB) ListFindingsForExport(f FindingFilter, ids []int64) ([]*DBFinding, error) {
|
||||
const order = `ORDER BY CASE f.severity WHEN 'critical' THEN 4 WHEN 'high' THEN 3 WHEN 'medium' THEN 2 WHEN 'low' THEN 1 ELSE 0 END DESC, f.created_at DESC`
|
||||
cols := findingSelectCols + `, COALESCE(f.report, '')`
|
||||
|
||||
var q string
|
||||
var args []any
|
||||
if len(ids) > 0 {
|
||||
ph := make([]string, len(ids))
|
||||
for i, id := range ids {
|
||||
ph[i] = fmt.Sprintf("$%d", i+1)
|
||||
args = append(args, id)
|
||||
}
|
||||
q = `SELECT ` + cols + `
|
||||
FROM findings f
|
||||
LEFT JOIN tasks t ON f.task_id = t.id
|
||||
WHERE f.id IN (` + strings.Join(ph, ",") + `)
|
||||
` + order
|
||||
} else {
|
||||
where, wargs := f.where()
|
||||
q = `SELECT ` + cols + `
|
||||
FROM findings f
|
||||
LEFT JOIN tasks t ON f.task_id = t.id` + where + `
|
||||
` + order
|
||||
args = wargs
|
||||
}
|
||||
|
||||
rows, err := d.Query(q, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []*DBFinding
|
||||
for rows.Next() {
|
||||
f := &DBFinding{}
|
||||
var aidsJSON string
|
||||
if err := rows.Scan(&f.ID, &f.TaskID, &f.NodeID, &f.VulnClass, &f.Name, &f.Severity,
|
||||
&f.Summary, &f.Evidence, &f.Worker, &aidsJSON, &f.Status, &f.CreatedAt,
|
||||
&f.TaskDescription, &f.Report); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = json.Unmarshal([]byte(aidsJSON), &f.AssetIDs)
|
||||
out = append(out, f)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// FindingStats is the whole-table aggregate powering the 发现 page's stat cards
|
||||
// and vuln-class filter — computed server-side so it stays exact regardless of
|
||||
// pagination.
|
||||
|
||||
Reference in new issue
Block a user