Add recon deploy action matrix and playbook wizard API.

Extend deploy-kit with pinned-build campaign attribution, SSRF erasure shards, and one-click action matrix; add playbook endpoint branching by target profile.
This commit is contained in:
AetherForge
2026-06-07 12:15:04 -07:00
parent 825bf81131
commit 06486b9bcb
4 changed files with 340 additions and 92 deletions

View File

@@ -59,15 +59,36 @@ func TestGetDeployKitWinRM(t *testing.T) {
if out["join_lane"] != "winrm" {
t.Fatalf("join_lane: %v", out["join_lane"])
}
dropper, ok := out["dropper_urls"].(map[string]interface{})
if !ok || dropper["install_ps1"] == "" {
t.Fatalf("dropper_urls: %v", out["dropper_urls"])
if out["campaign"] != "recon-10.1.2.50" {
t.Fatalf("campaign: %v", out["campaign"])
}
if out["spread_kit_zip"] == nil {
t.Fatal("expected spread_kit_zip")
if out["action_matrix"] == nil {
t.Fatal("expected action_matrix")
}
if out["deploy_plan_template"] == nil {
t.Fatal("expected deploy_plan_template")
}
func TestGetDeployKitActionMatrix(t *testing.T) {
spreadH, _ := testReconSpreadHandler(t)
req := httptest.NewRequest(http.MethodGet, "/api/v1/recon/deploy-kit?host=10.1.2.50&finding=WinRM&open_ports=22", nil)
rec := httptest.NewRecorder()
spreadH.GetDeployKit(rec, req)
var out map[string]interface{}
json.Unmarshal(rec.Body.Bytes(), &out)
matrix, ok := out["action_matrix"].([]interface{})
if !ok || len(matrix) == 0 {
t.Fatalf("action_matrix: %v", out["action_matrix"])
}
}
func TestGetDeployKitSSRFErasureManifest(t *testing.T) {
spreadH, _ := testReconSpreadHandler(t)
req := httptest.NewRequest(http.MethodGet, "/api/v1/recon/deploy-kit?host=app.lab&finding=ssrf", nil)
rec := httptest.NewRecorder()
spreadH.GetDeployKit(rec, req)
var out map[string]interface{}
json.Unmarshal(rec.Body.Bytes(), &out)
if out["erasure_manifest"] == nil {
t.Fatal("missing erasure_manifest")
}
}
@@ -77,14 +98,7 @@ func TestGetDeployKitSSM(t *testing.T) {
rec := httptest.NewRecorder()
spreadH.GetDeployKit(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status %d body %s", rec.Code, rec.Body.String())
}
var out map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
t.Fatal(err)
}
if out["join_lane"] != "ssm_document" {
t.Fatalf("join_lane: %v", out["join_lane"])
t.Fatalf("status %d", rec.Code)
}
}
@@ -98,13 +112,31 @@ func TestGetDeployKitRequiresHost(t *testing.T) {
}
}
func TestResolveReconFinding(t *testing.T) {
matched, lane, ok := resolveReconFinding("gpsvc", NormalizeServiceDeployAllowlist(nil))
if !ok || lane.Lane != "gpo" {
t.Fatalf("gpsvc → gpo: matched=%q lane=%q ok=%v", matched, lane.Lane, ok)
func TestGetReconPlaybook(t *testing.T) {
spreadH, _ := testReconSpreadHandler(t)
req := httptest.NewRequest(http.MethodGet, "/api/v1/recon/playbook?host=127.0.0.1", nil)
rec := httptest.NewRecorder()
spreadH.GetReconPlaybook(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status %d body %s", rec.Code, rec.Body.String())
}
_, lane, ok = resolveReconFinding("", NormalizeServiceDeployAllowlist(nil))
if !ok || lane.Lane != "bits_curl" {
t.Fatalf("empty finding default: %v", lane.Lane)
var out map[string]interface{}
json.Unmarshal(rec.Body.Bytes(), &out)
if out["profile"] == nil {
t.Fatalf("profile missing: %v", out)
}
}
func TestResolveReconFinding(t *testing.T) {
_, lane, ok := resolveReconFinding("gpsvc", NormalizeServiceDeployAllowlist(nil))
if !ok || lane.Lane != "gpo" {
t.Fatalf("gpsvc lane=%q ok=%v", lane.Lane, ok)
}
}
func TestResolveReconFindingSSRF(t *testing.T) {
_, lane, ok := resolveReconFinding("ssrf", nil)
if !ok || lane.Lane != "stage_fetch" {
t.Fatalf("lane=%q ok=%v", lane.Lane, ok)
}
}