feat(tasks): 删除任务可选连带删除资产/漏洞/流量/文件(默认不选)

删除弹窗新增四个默认不勾的复选框:
- 删除关联的资产:仅删本任务独有资产,与其他任务共享的仅解除关联(保留)。
- 删除发现的漏洞:删本任务全部 findings。
- 删除测试的流量:按本任务资产的 host 清录制流量(host 无任务标签,会连带清
  其他任务命中同一 host 的流量)。
- 删除测试过程中写的文件:删 <workDir>/tasks/<id> 目录(worker 产物 + 上传)。

- db: AssetStore.DeleteByTaskID(独有删+共享解关联)/HostsByTask;DeleteFindingsByTask。
- server: pgDeleteTask 读可选 JSON body {assets,findings,traffic,files},先按 host
  清流量(需在删资产前取 host)→删漏洞→删资产→删任务→删文件目录;空 body=只删任务。
- web: api.deleteTask(id,opts);删除弹窗四复选框(默认不选)+说明。

go build/vet、tsc 通过;新增 TestDeleteByTaskID(独有删/共享解关联/host 反查)。
This commit is contained in:
Autumn-27 committed 2026-08-19 10:40:19 -04:00
1 parent 2aeb2e49da
commit 4f7c1a199d
6 files changed
+215 -14

No files matched your search

+48 -1
View File
@@ -66,14 +66,61 @@ func decode(r *http.Request, v any) error { return json.NewDecoder(r.Body).Decod
// ---------- tasks (delete) ----------
// pgDeleteTask removes a task and, per opt-in flags in the (optional) JSON body,
// its associated data: assets (task-owned), findings, recorded traffic (by the
// task's hosts) and the per-task working directory. Empty body → all flags false →
// only the task + its exploration subgraph are removed (legacy behavior).
func (s *Server) pgDeleteTask(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
var opts struct {
Assets bool `json:"assets"`
Findings bool `json:"findings"`
Traffic bool `json:"traffic"`
Files bool `json:"files"`
}
_ = json.NewDecoder(r.Body).Decode(&opts) // optional body; ignore EOF/parse errors
result := map[string]any{"deleted": id}
nid, _ := strconv.ParseInt(id, 10, 64)
as := s.m.Assets()
// Traffic first: it needs the task's hosts, which come from assets that a later
// asset-delete would remove. Traffic is host-keyed (no per-task tag), so this
// purges ALL recorded traffic for those hosts, not just this task's.
if opts.Traffic && nid > 0 && as != nil {
if tr := s.m.Traffic(); tr != nil {
if hosts, err := as.HostsByTask(nid); err == nil && len(hosts) > 0 {
if n, err := tr.DeleteHostsExact(hosts); err == nil {
result["traffic_deleted"] = n
}
}
}
}
if opts.Findings && nid > 0 {
if n, err := s.m.pg.DeleteFindingsByTask(nid); err == nil {
result["findings_deleted"] = n
}
}
if opts.Assets && nid > 0 && as != nil {
if n, err := as.DeleteByTaskID(nid); err == nil {
result["assets_deleted"] = n
}
}
s.engine.Pause(id, agent.AbortTaskDeleted) // cancel running loops before removing
if err := s.m.DeleteTask(id); err != nil {
writeErr(w, 500, err.Error())
return
}
writeJSON(w, 200, map[string]any{"deleted": id})
// Files last: the per-task working dir <workDir>/tasks/<id> (worker artifacts +
// uploads). Confined to the work dir; id is numeric so no traversal risk.
if opts.Files && nid > 0 {
if err := os.RemoveAll(filepath.Join(s.m.dir, "tasks", id)); err == nil {
result["files_deleted"] = true
}
}
writeJSON(w, 200, result)
}
// ---------- agents ----------