ARTEX — AI 自主渗透测试系统(Go 后端 + Next.js 前端,单二进制内嵌)。 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
427 lines
12 KiB
Go
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})
|
|
}
|