feat(findings): group findings and add deepen workflows
This commit is contained in:
1 parent
31dc9e7446
commit
27dc6d8bad
9 files changed
+2098
-502
No files matched your search
@@ -0,0 +1,165 @@
|
||||
package report
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"path"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Autumn-27/artex/db"
|
||||
)
|
||||
|
||||
// 发现页「导出」用的渲染:把一批 findings 表行渲染成汇总 Markdown、单条 Markdown、
|
||||
// 或 CSV。JSON 由 server 层直接用 DTO 序列化,不在此处。
|
||||
|
||||
// sortFindingsForExport 按严重等级降序、再按时间倒序排,与汇总报告的分组一致。
|
||||
func sortFindingsForExport(fs []*db.DBFinding) {
|
||||
sort.SliceStable(fs, func(i, j int) bool {
|
||||
ri, rj := sevRank[fs[i].Severity], sevRank[fs[j].Severity]
|
||||
if ri != rj {
|
||||
return ri < rj // sevRank 越小越严重
|
||||
}
|
||||
return fs[i].CreatedAt.After(fs[j].CreatedAt)
|
||||
})
|
||||
}
|
||||
|
||||
// findingTitle 取漏洞可读标题:名称 → 类别 → 「未分类」。
|
||||
func findingTitle(f *db.DBFinding) string {
|
||||
return nz(f.Name, nz(f.VulnClass, "未分类"))
|
||||
}
|
||||
|
||||
// FindingsMarkdown 把一批 findings 整合成一份汇总报告(摘要 + 按严重等级分组,
|
||||
// 每条含类别/状态/所属任务/证据/详细报告)。
|
||||
func FindingsMarkdown(fs []*db.DBFinding, generatedAt time.Time) string {
|
||||
items := append([]*db.DBFinding(nil), fs...)
|
||||
sortFindingsForExport(items)
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("# 漏洞发现汇总报告\n\n")
|
||||
fmt.Fprintf(&b, "- **生成时间**:%s\n", generatedAt.Format("2006-01-02 15:04:05"))
|
||||
fmt.Fprintf(&b, "- **发现总数**:%d 个\n\n", len(items))
|
||||
|
||||
// 摘要:各严重等级计数。
|
||||
counts := map[string]int{}
|
||||
for _, f := range items {
|
||||
counts[f.Severity]++
|
||||
}
|
||||
b.WriteString("## 摘要\n\n")
|
||||
b.WriteString("| 严重等级 | 数量 |\n| --- | --- |\n")
|
||||
for _, s := range []struct{ key, label string }{
|
||||
{"critical", "严重"}, {"high", "高危"}, {"medium", "中危"}, {"low", "低危"},
|
||||
} {
|
||||
fmt.Fprintf(&b, "| %s | %d |\n", s.label, counts[s.key])
|
||||
}
|
||||
b.WriteString("\n")
|
||||
|
||||
if len(items) == 0 {
|
||||
b.WriteString("_无匹配的漏洞。_\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
b.WriteString("## 漏洞明细\n\n")
|
||||
for i, f := range items {
|
||||
fmt.Fprintf(&b, "### %d. [%s] %s\n\n", i+1, strings.ToUpper(nz(f.Severity, "info")), findingTitle(f))
|
||||
if f.VulnClass != "" {
|
||||
fmt.Fprintf(&b, "- **类别**:%s\n", f.VulnClass)
|
||||
}
|
||||
fmt.Fprintf(&b, "- **状态**:%s\n", nz(f.Status, "pending"))
|
||||
if desc := strings.TrimSpace(f.TaskDescription); desc != "" {
|
||||
fmt.Fprintf(&b, "- **所属任务**:%s\n", desc)
|
||||
}
|
||||
fmt.Fprintf(&b, "- **发现时间**:%s\n\n", f.CreatedAt.Format("2006-01-02 15:04:05"))
|
||||
if s := strings.TrimSpace(f.Summary); s != "" {
|
||||
fmt.Fprintf(&b, "%s\n\n", s)
|
||||
}
|
||||
if e := strings.TrimSpace(f.Evidence); e != "" {
|
||||
fmt.Fprintf(&b, "**证据:**\n\n```\n%s\n```\n\n", e)
|
||||
}
|
||||
if rep := strings.TrimSpace(f.Report); rep != "" {
|
||||
b.WriteString("**详细报告:**\n\n")
|
||||
b.WriteString(rep)
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
b.WriteString("---\n\n")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// SingleFindingMarkdown 渲染单条漏洞为一份独立 Markdown(用于「一漏洞一文件」打包)。
|
||||
func SingleFindingMarkdown(f *db.DBFinding, generatedAt time.Time) string {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "# [%s] %s\n\n", strings.ToUpper(nz(f.Severity, "info")), findingTitle(f))
|
||||
if f.VulnClass != "" {
|
||||
fmt.Fprintf(&b, "- **类别**:%s\n", f.VulnClass)
|
||||
}
|
||||
fmt.Fprintf(&b, "- **严重等级**:%s\n", nz(f.Severity, "info"))
|
||||
fmt.Fprintf(&b, "- **状态**:%s\n", nz(f.Status, "pending"))
|
||||
if desc := strings.TrimSpace(f.TaskDescription); desc != "" {
|
||||
fmt.Fprintf(&b, "- **所属任务**:%s\n", desc)
|
||||
}
|
||||
fmt.Fprintf(&b, "- **发现时间**:%s\n", f.CreatedAt.Format("2006-01-02 15:04:05"))
|
||||
fmt.Fprintf(&b, "- **生成时间**:%s\n\n", generatedAt.Format("2006-01-02 15:04:05"))
|
||||
if s := strings.TrimSpace(f.Summary); s != "" {
|
||||
fmt.Fprintf(&b, "## 概述\n\n%s\n\n", s)
|
||||
}
|
||||
if e := strings.TrimSpace(f.Evidence); e != "" {
|
||||
fmt.Fprintf(&b, "## 证据\n\n```\n%s\n```\n\n", e)
|
||||
}
|
||||
if rep := strings.TrimSpace(f.Report); rep != "" {
|
||||
b.WriteString("## 详细报告\n\n")
|
||||
b.WriteString(rep)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
var unsafeFilenameChars = regexp.MustCompile(`[^\p{Han}\p{L}\p{N}._-]+`)
|
||||
|
||||
// FindingFilename 为「一漏洞一文件」生成安全的 .md 文件名,形如
|
||||
// `critical_SQL注入_#123.md`。去掉路径分隔符与控制字符,避免 zip 内非法路径。
|
||||
func FindingFilename(f *db.DBFinding) string {
|
||||
sev := nz(f.Severity, "info")
|
||||
title := findingTitle(f)
|
||||
name := fmt.Sprintf("%s_%s_#%d", sev, title, f.ID)
|
||||
name = unsafeFilenameChars.ReplaceAllString(name, "_")
|
||||
name = strings.Trim(name, "._")
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("finding_%d", f.ID)
|
||||
}
|
||||
// 防御性:再剥一层路径,杜绝 zip slip。
|
||||
name = path.Base(name)
|
||||
if len(name) > 120 {
|
||||
name = name[:120]
|
||||
}
|
||||
return name + ".md"
|
||||
}
|
||||
|
||||
// FindingsCSV 把一批 findings 渲染成 CSV(带 UTF-8 BOM,便于 Excel 正确识别中文)。
|
||||
// 不含大段 report/evidence 全文,只放摘要类字段;需要全文用 Markdown/JSON 导出。
|
||||
func FindingsCSV(fs []*db.DBFinding) []byte {
|
||||
items := append([]*db.DBFinding(nil), fs...)
|
||||
sortFindingsForExport(items)
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("\xEF\xBB\xBF") // UTF-8 BOM
|
||||
w := csv.NewWriter(&buf)
|
||||
_ = w.Write([]string{"ID", "名称", "类别", "严重等级", "状态", "所属任务", "发现时间", "概述"})
|
||||
for _, f := range items {
|
||||
_ = w.Write([]string{
|
||||
fmt.Sprintf("%d", f.ID),
|
||||
findingTitle(f),
|
||||
f.VulnClass,
|
||||
nz(f.Severity, "info"),
|
||||
nz(f.Status, "pending"),
|
||||
f.TaskDescription,
|
||||
f.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
strings.TrimSpace(f.Summary),
|
||||
})
|
||||
}
|
||||
w.Flush()
|
||||
return buf.Bytes()
|
||||
}
|
||||
Reference in new issue
Block a user