Files
ARTEX-chixuweihu/server/assets.go
T
Autumn-27andClaude Opus 4.8 0ef8561411 chore: ARTEX 初始化
ARTEX — AI 自主渗透测试系统(Go 后端 + Next.js 前端,单二进制内嵌)。

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-26 04:27:45 -04:00

427 lines
12 KiB
Go

package server
import (
"encoding/json"
"net/http"
"strconv"
"github.com/Autumn-27/artex/db"
)
// assetStore returns the asset store.
func (s *Server) assetStore() *db.AssetStore {
if s.m.pg == nil {
return nil
}
return s.m.pg.Assets()
}
// companyStore returns the company store.
func (s *Server) companyStore() *db.CompanyStore {
if s.m.pg == nil {
return nil
}
return s.m.pg.Companies()
}
// =====================================================================
// GET /api/companies
// =====================================================================
func (s *Server) listCompanies(w http.ResponseWriter, r *http.Request) {
cs := s.companyStore()
if cs == nil {
writeErr(w, 503, "database unavailable")
return
}
companies, err := cs.ListCompanies()
if err != nil {
writeErr(w, 500, err.Error())
return
}
writeJSON(w, 200, companies)
}
// =====================================================================
// POST /api/companies
// =====================================================================
func (s *Server) createCompany(w http.ResponseWriter, r *http.Request) {
cs := s.companyStore()
if cs == nil {
writeErr(w, 503, "database unavailable")
return
}
var req struct {
Name string `json:"name"`
Logo string `json:"logo"`
Scope []string `json:"scope"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErr(w, 400, "invalid JSON: "+err.Error())
return
}
if req.Name == "" {
writeErr(w, 400, "name required")
return
}
id, created, err := cs.UpsertCompany(req.Name, req.Logo)
if err != nil {
writeErr(w, 500, err.Error())
return
}
out := map[string]any{"id": id, "created": created}
if len(req.Scope) > 0 {
added, skipped, invalid, errs := cs.AddScope(id, req.Scope, "api")
out["scope_added"] = added
out["scope_skipped"] = skipped
out["scope_invalid"] = invalid
if len(errs) > 0 {
out["scope_errors"] = errs
}
}
writeJSON(w, 201, out)
}
// =====================================================================
// GET /api/companies/{id}
// =====================================================================
func (s *Server) getCompany(w http.ResponseWriter, r *http.Request) {
cs := s.companyStore()
if cs == nil {
writeErr(w, 503, "database unavailable")
return
}
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
writeErr(w, 400, "invalid id")
return
}
c, err := cs.GetCompany(id)
if err != nil {
writeErr(w, 500, err.Error())
return
}
if c == nil {
writeErr(w, 404, "company not found")
return
}
scope, err := cs.GetScope(id)
if err != nil {
writeErr(w, 500, err.Error())
return
}
writeJSON(w, 200, map[string]any{"company": c, "scope": scope})
}
// =====================================================================
// POST /api/companies/{id}/scope
// =====================================================================
func (s *Server) addCompanyScope(w http.ResponseWriter, r *http.Request) {
cs := s.companyStore()
if cs == nil {
writeErr(w, 503, "database unavailable")
return
}
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
writeErr(w, 400, "invalid company id")
return
}
var req struct {
Scope []string `json:"scope"`
Reason string `json:"reason"`
Reset bool `json:"reset"` // if true, replace existing scope
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErr(w, 400, "invalid JSON: "+err.Error())
return
}
var added, skipped, invalid int
var errs []string
if req.Reset {
added, invalid, errs = cs.UpdateScope(id, req.Scope, req.Reason)
} else {
added, skipped, invalid, errs = cs.AddScope(id, req.Scope, req.Reason)
}
out := map[string]any{
"added": added,
"skipped": skipped,
"invalid": invalid,
}
if len(errs) > 0 {
out["errors"] = errs
}
writeJSON(w, 200, out)
}
// =====================================================================
// DELETE /api/companies/{id}
// =====================================================================
func (s *Server) deleteCompany(w http.ResponseWriter, r *http.Request) {
cs := s.companyStore()
if cs == nil {
writeErr(w, 503, "database unavailable")
return
}
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
writeErr(w, 400, "invalid id")
return
}
var req struct {
DeleteAssets bool `json:"delete_assets"`
}
// Body is optional; ignore decode errors (e.g. empty body).
_ = json.NewDecoder(r.Body).Decode(&req)
assetsDeleted := int64(0)
if req.DeleteAssets {
as := s.assetStore()
if as != nil {
assetsDeleted, err = as.DeleteByCompanyID(id)
if err != nil {
writeErr(w, 500, err.Error())
return
}
}
}
if err := cs.DeleteCompany(id); err != nil {
writeErr(w, 500, err.Error())
return
}
writeJSON(w, 200, map[string]any{"deleted": 1, "assets_deleted": assetsDeleted})
}
// =====================================================================
// POST /api/companies/reattribute
// =====================================================================
func (s *Server) reattribute(w http.ResponseWriter, r *http.Request) {
cs := s.companyStore()
if cs == nil {
writeErr(w, 503, "database unavailable")
return
}
if err := cs.RecomputeAttribution(); err != nil {
writeErr(w, 500, err.Error())
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
// =====================================================================
// GET /api/assets
// =====================================================================
func (s *Server) listAssets(w http.ResponseWriter, r *http.Request) {
as := s.assetStore()
if as == nil {
writeErr(w, 503, "database unavailable")
return
}
q := r.URL.Query()
typ := q.Get("type")
limit, _ := strconv.Atoi(q.Get("limit"))
offset, _ := strconv.Atoi(q.Get("offset"))
if limit <= 0 {
limit = 50
}
var assets []*db.Asset
var err error
// total is the full match count ignoring limit/offset — lets the UI paginate on
// the server. For company/task scopes it falls back to the returned length.
total := 0
if dsl := q.Get("dsl"); dsl != "" {
assets, err = as.QueryDSL(dsl, typ, limit, offset)
if err == nil {
total, _ = as.CountDSL(dsl, typ)
}
} else {
companyID, _ := strconv.ParseInt(q.Get("company_id"), 10, 64)
taskID, _ := strconv.ParseInt(q.Get("task_id"), 10, 64)
switch {
case companyID > 0:
assets, err = as.QueryByCompany(companyID, typ, limit)
total = len(assets)
case taskID > 0:
assets, err = as.QueryByTask(taskID, typ, limit)
total = len(assets)
default:
if typ == "" {
typ = "root_domain"
}
assets, err = as.QueryByType(typ, limit, offset)
if err == nil {
total, _ = as.CountByType(typ)
}
}
}
if err != nil {
writeErr(w, 400, err.Error())
return
}
writeJSON(w, 200, map[string]any{
"count": len(assets),
"total": total,
"assets": assets,
})
}
// =====================================================================
// GET /api/assets/counts
// =====================================================================
func (s *Server) assetCounts(w http.ResponseWriter, r *http.Request) {
as := s.assetStore()
if as == nil {
writeErr(w, 503, "database unavailable")
return
}
counts, err := as.CountsByType()
if err != nil {
writeErr(w, 500, err.Error())
return
}
writeJSON(w, 200, counts)
}
// =====================================================================
// DELETE /api/assets
// =====================================================================
func (s *Server) deleteAssets(w http.ResponseWriter, r *http.Request) {
as := s.assetStore()
if as == nil {
writeErr(w, 503, "database unavailable")
return
}
var req struct {
IDs []int64 `json:"ids"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErr(w, 400, "invalid JSON: "+err.Error())
return
}
if len(req.IDs) == 0 {
writeErr(w, 400, "ids required")
return
}
deleted, err := as.DeleteByIDs(req.IDs)
if err != nil {
writeErr(w, 500, err.Error())
return
}
writeJSON(w, 200, map[string]any{"deleted": deleted})
}
// =====================================================================
// POST /api/assets
// =====================================================================
func (s *Server) insertAssets(w http.ResponseWriter, r *http.Request) {
as := s.assetStore()
if as == nil {
writeErr(w, 503, "database unavailable")
return
}
var req struct {
TaskID int64 `json:"task_id"`
Assets []struct {
Type string `json:"type"`
// root_domain / subdomain
Domain string `json:"domain"`
ICP string `json:"icp"`
RecordType string `json:"record_type"`
RecordValue []string `json:"record_value"`
// ip
IP string `json:"ip"`
BoundDomains []string `json:"bound_domains"`
OpenPorts []db.PortService `json:"open_ports"`
// app
AppName string `json:"app_name"`
BundleID string `json:"bundle_id"`
Category string `json:"category"`
Description string `json:"description"`
AppICP string `json:"app_icp"`
// service http
URL string `json:"url"`
Technologies []string `json:"technologies"`
StatusCode *int `json:"status_code"`
ContentLength *int64 `json:"content_length"`
PageTitle string `json:"page_title"`
FaviconMMH3 string `json:"favicon_mmh3"`
Auth []map[string]any `json:"auth"`
ServiceIP string `json:"service_ip"`
// service other
Port int `json:"port"`
ServiceName string `json:"service_name"`
// endpoint
Method string `json:"method"`
Params []map[string]any `json:"params"`
} `json:"assets"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErr(w, 400, "invalid JSON: "+err.Error())
return
}
type result struct {
Index int `json:"index"`
ID int64 `json:"id"`
Type string `json:"type"`
}
type errEntry struct {
Index int `json:"index"`
Error string `json:"error"`
}
var results []result
var errs []errEntry
for i, a := range req.Assets {
var id int64
var err error
switch a.Type {
case "root_domain":
id, err = as.UpsertRootDomain(db.UpsertRootDomainReq{Domain: a.Domain, ICP: a.ICP, TaskID: req.TaskID})
case "ip":
id, err = as.UpsertIP(db.UpsertIPReq{IP: a.IP, BoundDomains: a.BoundDomains, OpenPorts: a.OpenPorts, TaskID: req.TaskID})
case "subdomain":
id, err = as.UpsertSubdomain(db.UpsertSubdomainReq{Domain: a.Domain, RecordType: a.RecordType, RecordValue: a.RecordValue, ICP: a.ICP, TaskID: req.TaskID})
case "app":
id, err = as.UpsertApp(db.UpsertAppReq{Name: a.AppName, BundleID: a.BundleID, Category: a.Category, Description: a.Description, ICP: a.AppICP, TaskID: req.TaskID})
case "service":
if a.URL != "" {
svcIP := a.ServiceIP
if svcIP == "" {
svcIP = a.IP
}
id, err = as.UpsertHTTPService(db.UpsertHTTPServiceReq{URL: a.URL, Technologies: a.Technologies, StatusCode: a.StatusCode, ContentLength: a.ContentLength, PageTitle: a.PageTitle, FaviconMMH3: a.FaviconMMH3, Auth: a.Auth, IP: svcIP, TaskID: req.TaskID})
} else {
id, err = as.UpsertOtherService(db.UpsertOtherServiceReq{Domain: a.Domain, IP: a.IP, Port: a.Port, ServiceName: a.ServiceName, Auth: a.Auth, TaskID: req.TaskID})
}
case "endpoint":
svcIP := a.ServiceIP
if svcIP == "" {
svcIP = a.IP
}
id, err = as.UpsertEndpoint(db.UpsertEndpointReq{URL: a.URL, Method: a.Method, Params: a.Params, IP: svcIP, TaskID: req.TaskID})
default:
errs = append(errs, errEntry{Index: i, Error: "unknown type: " + a.Type})
continue
}
if err != nil {
errs = append(errs, errEntry{Index: i, Error: err.Error()})
continue
}
results = append(results, result{Index: i, ID: id, Type: a.Type})
}
writeJSON(w, 200, map[string]any{"results": results, "errors": errs})
}