feat(findings): 发现页新增「按资产」视图
左侧资产树 + 右侧该子树下的发现,与另外两个视图共用同一张表和同一批筛选。 后端 - db/finding_assets.go:按当前筛选构建资产树。只收有发现的资产,祖先链逐层 补齐(endpoint→service→subdomain/ip→root_domain),父子优先级与 BuildCoverageGraph 同源;计数沿祖先链去重累加,一条发现挂多个资产不会让 父节点重复计数 - FindingFilter 加 AssetScope:选中节点=选中整棵子树,解析成资产 id 集合后 下推 SQL,列表 / 分组 / 导出三个入口共用,导出「当前筛选」自动带上 - 「未关联资产」同时收 asset_ids 为空与指向已删资产的发现,与树上桶的口径 一致(只判空数组会让桶上的数字大于点开后的条数) - findingFilterFromQuery 统一四处的筛选解析;新增 GIN 索引 idx_findings_asset_ids,按资产反查发现不再全表扫 前端 - 树默认展开一层,支持过滤(匹配项自动展开)、面包屑下钻、手动刷新 - 每层只显示相对上一层的增量:子域名去根域名后缀、服务显示 `https :443`、 接口只留路径。完整值在 title 与面包屑,搜索仍按完整值匹配 - 不轮询:进入视图 / 改筛选 / 页内改动发现 / 点刷新时才查 - 节点超上限时整层丢弃接口/服务并提示,计数已在上层不会丢
This commit is contained in:
1 parent
796dbf5600
commit
23cfb9ed4e
11 files changed
+1582
-82
No files matched your search
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
@@ -24,6 +25,32 @@ func findingPaginationParam(raw string, fallback, upperBound int) int {
|
||||
return value
|
||||
}
|
||||
|
||||
// findingFilterFromQuery builds the shared findings filter from a request's
|
||||
// query string. 列表 / 分组 / 资产树 / 导出走同一份解析,新增筛选项只改这里。
|
||||
func findingFilterFromQuery(q url.Values) db.FindingFilter {
|
||||
return db.FindingFilter{
|
||||
Severity: normFilter(q.Get("severity")),
|
||||
Status: normFilter(q.Get("status")),
|
||||
VulnClass: normFilter(q.Get("vulnclass")),
|
||||
// task_id(独立于会切到「按任务节点」分支的 task 参数):全局表按任务筛选。
|
||||
TaskID: normFilter(q.Get("task_id")),
|
||||
Query: q.Get("q"),
|
||||
Sort: q.Get("sort"),
|
||||
AssetScope: strings.TrimSpace(q.Get("asset_scope")),
|
||||
}
|
||||
}
|
||||
|
||||
// findingAssetTree serves the「按资产」view's left-hand tree: every asset that
|
||||
// carries at least one matching finding, plus the ancestors needed to place it.
|
||||
func (s *Server) findingAssetTree(w http.ResponseWriter, r *http.Request) {
|
||||
tree, err := s.m.pg.BuildFindingAssetTree(findingFilterFromQuery(r.URL.Query()))
|
||||
if err != nil {
|
||||
writeErr(w, 500, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, 200, tree)
|
||||
}
|
||||
|
||||
// findingGroups serves the task-group page used by the global findings view.
|
||||
// Findings inside a group continue to use the existing flat findings endpoint,
|
||||
// preserving dashboard/export clients and giving every expanded group its own
|
||||
@@ -32,15 +59,7 @@ func (s *Server) findingGroups(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
page := findingPaginationParam(q.Get("page"), 1, 0)
|
||||
limit := findingPaginationParam(q.Get("limit"), 10, 100)
|
||||
filter := db.FindingFilter{
|
||||
Severity: normFilter(q.Get("severity")),
|
||||
Status: normFilter(q.Get("status")),
|
||||
VulnClass: normFilter(q.Get("vulnclass")),
|
||||
TaskID: normFilter(q.Get("task_id")),
|
||||
Query: q.Get("q"),
|
||||
Sort: q.Get("sort"),
|
||||
}
|
||||
groups, total, findingTotal, err := s.m.pg.ListFindingGroups(filter, page, limit)
|
||||
groups, total, findingTotal, err := s.m.pg.ListFindingGroups(findingFilterFromQuery(q), page, limit)
|
||||
if err != nil {
|
||||
writeErr(w, 500, err.Error())
|
||||
return
|
||||
|
||||
+3
-18
@@ -775,6 +775,7 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("GET /api/exploration/frontier", s.frontier)
|
||||
mux.HandleFunc("GET /api/exploration/findings", s.findings)
|
||||
mux.HandleFunc("GET /api/exploration/findings/groups", s.findingGroups)
|
||||
mux.HandleFunc("GET /api/exploration/findings/asset-tree", s.findingAssetTree)
|
||||
mux.HandleFunc("GET /api/exploration/findings/stats", s.findingStats)
|
||||
mux.HandleFunc("GET /api/exploration/findings/export", s.findingsExport)
|
||||
mux.HandleFunc("GET /api/exploration/findings/{id}", s.getFinding)
|
||||
@@ -1887,16 +1888,7 @@ func (s *Server) findings(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
page := findingPaginationParam(q.Get("page"), 1, 0)
|
||||
limit := findingPaginationParam(q.Get("limit"), 20, 200)
|
||||
filter := db.FindingFilter{
|
||||
Severity: normFilter(q.Get("severity")),
|
||||
Status: normFilter(q.Get("status")),
|
||||
VulnClass: normFilter(q.Get("vulnclass")),
|
||||
// task_id(独立于会切到「按任务节点」分支的 task 参数):全局表按任务筛选。
|
||||
TaskID: normFilter(q.Get("task_id")),
|
||||
Query: q.Get("q"),
|
||||
Sort: q.Get("sort"),
|
||||
}
|
||||
fs, total, err := s.m.pg.ListFindingsPage(filter, page, limit)
|
||||
fs, total, err := s.m.pg.ListFindingsPage(findingFilterFromQuery(q), page, limit)
|
||||
if err != nil {
|
||||
writeErr(w, 500, err.Error())
|
||||
return
|
||||
@@ -2092,14 +2084,7 @@ func (s *Server) findingsExport(w http.ResponseWriter, r *http.Request) {
|
||||
case "all":
|
||||
// 空 filter = 不加任何条件。
|
||||
case "filtered", "":
|
||||
filter = db.FindingFilter{
|
||||
Severity: normFilter(q.Get("severity")),
|
||||
Status: normFilter(q.Get("status")),
|
||||
VulnClass: normFilter(q.Get("vulnclass")),
|
||||
TaskID: normFilter(q.Get("task_id")),
|
||||
Query: q.Get("q"),
|
||||
Sort: q.Get("sort"),
|
||||
}
|
||||
filter = findingFilterFromQuery(q)
|
||||
default:
|
||||
writeErr(w, 400, "bad scope: "+scope)
|
||||
return
|
||||
|
||||
Reference in new issue
Block a user