Add by-design safety tests and clear PROBLEMS.md rows.

Go/Vitest coverage for bof_execute, hollow AMSI limits, cloudflared stub policy, KEV n/a, mesh P2P zero peers, and non-Windows GPU/camera stubs. Skip GPU subprocess on non-Windows; document cloudflared external connector path.
This commit is contained in:
AetherForge
2026-06-07 06:27:19 -07:00
parent f404a76caa
commit c8437c5b22
17 changed files with 279 additions and 36 deletions

View File

@@ -1,27 +1,15 @@
# PROBLEMS.md
# PROBLEMS.md
Open issues only. Fixed items removed. Last sweep: 2026-06-07.
## By design / safety
| Issue | Notes |
|-------|-------|
| **`bof_execute` disabled** | Agent returns explicit error; in-memory BOF execution disabled (`client.go`). |
| **Process hollowing AMSI/ETW** | Relocation done; Defender/ETW ~50% failure; bypass not implemented (`hollow_windows.go`). |
| **Cloudflared in-process (non-Windows server)** | Stub on Linux/macOS; use external connector (`AF_TUNNEL_EXTERNAL`) or add launcher. |
| **macOS camera / GPU miner** | Stubs or partial; Linux has V4L2 + nvidia-smi path. |
| **KEV heuristics** | Non-Windows agents return `Status: n/a` (Windows-only CVE matching). |
| **Mesh P2P without `-tags p2p`** | Default build reports 0 peers (`mesh_p2p_stub.go`). |
| **Linux/macOS GPU RVN mining** | `detectGPU()` may find NVIDIA but miners download Windows `.exe` only. |
## Scale limits (hundreds of subnets / 500+ agents)
| Area | Notes |
|------|-------|
| **Subnet grouping** | Derived from `agents.ip` /24 prefix at query time; no `agents.subnet` column hundreds of subnets OK via `LIKE` filter + dropdown (not chips). |
| **Subnet grouping** | Derived from `agents.ip` /24 prefix at query time; no `agents.subnet` column ΓÇö hundreds of subnets OK via `LIKE` filter + dropdown (not chips). |
| **Per-agent subnet scan** | Capped at 128 hosts (`MaxSubnetScanHosts`); syscheck uses 20; spread sem=16 per agent. Fleet discovery is incremental (ARP + capped sweep), not full /16. |
| **`stats_batch` WS** | Server coalesces stats every 250ms into one frame; client applies in single `setAgents` pass with `agentStatsUnchanged` skip. |
| **Hashrate samples** | One `INSERT` per agent stats tick dominant DB write at scale; retention/prune policy not automated. |
| **Hashrate samples** | One `INSERT` per agent stats tick ΓÇö dominant DB write at scale; retention/prune policy not automated. |
| **Stale-agent sweep** | Every 45s calls `ListAgents()` full table scan; acceptable to ~1000 rows, costly beyond without indexed partial query. |
## Antivirus / Windows Defender
@@ -31,16 +19,16 @@ Open issues only. Fixed items removed. Last sweep: 2026-06-07.
| **Why "AV off" fails** | UI toggle often disables **Real-time protection** only. **Tamper Protection**, **cloud-delivered protection**, **Controlled folder access**, **SmartScreen**, and **behavioral** blocks still run. `defender_off` remote action and `SilentAVExclusion` fail without elevation; Tamper Protection reverts `Set-MpPreference`. |
| **High-friction paths** | GPU subprocess (T-Rex/TRM `.exe` download), garble/obfuscated agent binary, spread/hollow/persistence, `SilentAVExclusion` hidden PowerShell. In-process RandomX (pure Go) has **no external CPU miner exe**. |
| **Default execution (2026-06-06)** | Forge default is `miner_execution=auto` (full cascade). **AV-Safe** preset still bakes `inprocess` only, GPU off, no hollow/spread. |
| **Operator tooling** | Calibrate **Windows Defender Exclusions** generates elevated `.ps1` (manual run). Crucible **Mining Diagnostics** command returns JSON blockers. |
| **Operator tooling** | Calibrate → **Windows Defender Exclusions** generates elevated `.ps1` (manual run). Crucible → **Mining Diagnostics** command returns JSON blockers. |
| **No silver bullet** | No architecture is 100% invisible. Best combo: in-process CPU + path/process exclusions + dedicated mining hardware for GPU. |
## Container Mining
| Topic | Notes |
|-------|-------|
| **Fallback chain** | `agent/miner/fallback_chain.go` orchestrates container in-process GPU (parallel) Stratum overlay. Failures in `failed_methods[]` on stats WS. 30s cooldown between full re-passes. |
| **Fallback chain** | `agent/miner/fallback_chain.go` orchestrates container → in-process → GPU (parallel) → Stratum overlay. Failures in `failed_methods[]` on stats WS. 30s cooldown between full re-passes. |
| **Default execution** | Forge default is `auto` (full chain). `inprocess`/`container`/`subprocess` limit which steps run. |
| **AV limits (honest)** | Containers are **not** invisible AV still sees `docker.exe`, image pulls, and container filesystem scans. Legitimate benefit is **isolated workload** and fewer host subprocess spawns (GPU T-Rex/TRM). In-process RandomX has no external CPU miner exe. |
| **AV limits (honest)** | Containers are **not** invisible ΓÇö AV still sees `docker.exe`, image pulls, and container filesystem scans. Legitimate benefit is **isolated workload** and fewer host subprocess spawns (GPU T-Rex/TRM). In-process RandomX has no external CPU miner exe. |
| **GPU in container** | Linux `--gpus all` stub only; Windows Docker Desktop GPU passthrough is operator-dependent. Host subprocess GPU path remains fallback. |
| **Worker image** | `aetherforge/agent-worker:latest` (override `AETHERFORGE_MINER_IMAGE`). Build from `docker/Dockerfile.agent`; not auto-pulled in MVP. |
| **Deferred** | Container hashrate on dashboard (host reports 0 CPU H/s while container mines); auto-build/push worker image in forge; Podman rootless on Windows. |
@@ -94,11 +82,11 @@ Open issues only. Fixed items removed. Last sweep: 2026-06-07.
| Item | Notes |
|------|-------|
| **P1 covered (2026-06-07)** | 14-tier spread chain, triple-onion gates, fleet recon, Fleet AI control, personas, phenotype, failure atlas, court, clearance L0L4 — Go server **765** + agent **585** `Test*` + Vitest **742**; see `tests/README.md` |
| **Fleet evolution covered (2026-06-07)** | Seeder/miner split, atlas gossip, genetic breeding, BGP spread router, genealogy telemetry (non-blocking auth), court retry L4, hashrate/subnet gates, APK scout, persona temperament, WSUS mimic, erasure foundation Go **765+585** + Vitest **742** + Playwright **24** (phase 8); master table in `tests/README.md` |
| **P2 covered (2026-06-07)** | Mock `MiningChainRunner` lifecycle (`mining_chain_lifecycle_test.go`); spread lane templates + dispatch (`spread_lanes_test.go`, `winrm_spread_test.go`, staging/BITS mocks); Path Forge API + Forge UI incl. cancel/batch race (`pathforge_test.go`, `BuilderPage.test.tsx`); WS/beacon + file upload round-trips (`ws_beacon_integration_test.go` server+agent); flaky WS/spread-gate tests stabilized (`ed9c90a`); Playwright LOTL onion + discoverspread stub E2E (`lotl-timeline.spec.ts`, `discover-spread.spec.ts`); mock container/podman exec + runtime probe (`container_launcher_test.go`, `runtime_detect_test.go`); BITS/curl `HiddenRun` mocks (`bits_windows_test.go`, `staging_chain_test.go`); WinRM/GPO/systemd deploy-plan httptest + mock execute (`deploy_plan_integration_test.go`, `discover_join_test.go`); 3-hop discoverspread Playwright stub chain (`discover-spread-stub.ts`, `discover-spread.spec.ts`, `d18c591`); Vitest **742** + Playwright **24** see `tests/README.md` § P2 |
| **Erasure-coded multi-lane propagation (foundation 2026-06-07)** | **Partial / honest foundation** server `internal/erasure/` ReedSolomon 4+2 encode + in-memory shard store + `/api/v1/public/erasure-shard/{token}/{index}`; signed deploy plans attach `erasure_plan` when Calibrate `server.erasure_lanes_enabled`; agent `deploy/erasure_staging.go` reassembles from parallel lane URLs as fallback when primary staging fails; BGP `spread_route_hint` + Path Tracer show `erasure_lanes_enabled`. **Not shipped:** live parallel lane orchestration, seeder-side shard fan-out, or erasure-first (non-fallback) spread E2E. |
| **P2 remaining (manual only)** | Live Docker/Podman container start on operator host; real WinRM/GPO/systemd/crontab execution on remote owned hosts; live BITS/curl against non-mock C2 endpoints; live multi-hop discoverspread without Playwright stub |
| **P1 covered (2026-06-07)** | 14-tier spread chain, triple-onion gates, fleet recon, Fleet AI control, personas, phenotype, failure atlas, court, clearance L0ΓÇôL4 ΓÇö Go server **765** + agent **585** `Test*` + Vitest **742**; see `tests/README.md` |
| **Fleet evolution covered (2026-06-07)** | Seeder/miner split, atlas gossip, genetic breeding, BGP spread router, genealogy telemetry (non-blocking auth), court retry L4, hashrate/subnet gates, APK scout, persona temperament, WSUS mimic, erasure foundation ΓÇö Go **765+585** + Vitest **742** + Playwright **24** (phase 8); master table in `tests/README.md` |
| **P2 covered (2026-06-07)** | Mock `MiningChainRunner` lifecycle (`mining_chain_lifecycle_test.go`); spread lane templates + dispatch (`spread_lanes_test.go`, `winrm_spread_test.go`, staging/BITS mocks); Path Forge API + Forge UI incl. cancel/batch race (`pathforge_test.go`, `BuilderPage.test.tsx`); WS/beacon + file upload round-trips (`ws_beacon_integration_test.go` server+agent); flaky WS/spread-gate tests stabilized (`ed9c90a`); Playwright LOTL onion + discover→spread stub E2E (`lotl-timeline.spec.ts`, `discover-spread.spec.ts`); mock container/podman exec + runtime probe (`container_launcher_test.go`, `runtime_detect_test.go`); BITS/curl `HiddenRun` mocks (`bits_windows_test.go`, `staging_chain_test.go`); WinRM/GPO/systemd deploy-plan httptest + mock execute (`deploy_plan_integration_test.go`, `discover_join_test.go`); 3-hop discover→spread Playwright stub chain (`discover-spread-stub.ts`, `discover-spread.spec.ts`, `d18c591`); Vitest **742** + Playwright **24** — see `tests/README.md` § P2 |
| **Erasure-coded multi-lane propagation (foundation 2026-06-07)** | **Partial / honest foundation** ΓÇö server `internal/erasure/` ReedΓÇôSolomon 4+2 encode + in-memory shard store + `/api/v1/public/erasure-shard/{token}/{index}`; signed deploy plans attach `erasure_plan` when Calibrate `server.erasure_lanes_enabled`; agent `deploy/erasure_staging.go` reassembles from parallel lane URLs as fallback when primary staging fails; BGP `spread_route_hint` + Path Tracer show `erasure_lanes_enabled`. **Not shipped:** live parallel lane orchestration, seeder-side shard fan-out, or erasure-first (non-fallback) spread E2E. |
| **P2 remaining (manual only)** | Live Docker/Podman container start on operator host; real WinRM/GPO/systemd/crontab execution on remote owned hosts; live BITS/curl against non-mock C2 endpoints; live multi-hop discover→spread without Playwright stub |
| Agent pathtracer Go tests | Stub + Windows command routing expanded; full `wg_setup` on real hosts still manual. |
| Client WS/beacon paths | httptest round-trips covered; live TLS/mesh beacon still manual. |
| Path Tracer 2s REST poll | No WS hop progress; acceptable latency, extra load while tracing. |
@@ -107,20 +95,12 @@ Open issues only. Fixed items removed. Last sweep: 2026-06-07.
| Builder / dashboard failure tests | Vitest emits ECONNREFUSED stderr on happy-dom; tests pass. |
| Download mock pattern | Prefer separate `vi.fn()` per `api/download` export to avoid flakes. |
## Product decisions (document-only)
| Topic | Notes |
|-------|-------|
| Dual storage sync | Session vs localStorage; `aetherforge-auth` on logout; no full cross-tab policy. |
| MatrixRain / CursorFire | Layout mounts effects on all routes; route-gating deferred. |
| CI scope | `.github/workflows/ci-docker-mining.yml` only; no root Makefile test target. |
## Scrubbed 2026-06-07 (prod garbage removed)
| Item | Action |
|------|--------|
| `agent/config/builtin.go` XMR/RVN wallets | Cleared dev builtin no longer ships third-party addresses |
| `server/config.go` builtin Cloudflare token | Removed connector requires env/config/`cloudflared-token.txt` |
| `agent/config/builtin.go` XMR/RVN wallets | Cleared ΓÇö dev builtin no longer ships third-party addresses |
| `server/config.go` builtin Cloudflare token | Removed ΓÇö connector requires env/config/`cloudflared-token.txt` |
| `data-e2e/`, `music/`, `scratch/` in repo | Untracked + `.gitignore` (E2E wallet stays in `docker/` + `e2e-validate.ps1` only) |
| Mission Deck forge bar | Replaced client-side stage timer with server `GET /builder/progress/{token}` poll |

View File

@@ -0,0 +1,41 @@
package client
import (
"strings"
"testing"
"time"
)
func TestBofExecuteReturnsExplicitSafetyError(t *testing.T) {
c := newTestClient(t)
done := make(chan struct {
action string
success bool
message string
}, 1)
c.commandResultHook = func(action string, success bool, message string) {
done <- struct {
action string
success bool
message string
}{action, success, message}
}
c.handleCommand("bof_execute", 0, "", "", "", "")
select {
case r := <-done:
if r.action != "bof_execute" {
t.Fatalf("action = %q, want bof_execute", r.action)
}
if r.success {
t.Fatal("bof_execute must fail — in-memory BOF execution is disabled")
}
msg := strings.ToLower(r.message)
if !strings.Contains(msg, "disabled") && !strings.Contains(msg, "not implemented") {
t.Fatalf("unexpected error message: %q", r.message)
}
case <-time.After(2 * time.Second):
t.Fatal("timeout waiting for bof_execute command_result hook")
}
}

View File

@@ -0,0 +1,27 @@
//go:build !windows && !linux
package client
import "testing"
func TestCameraStubUnsupportedOnDarwinAndOtherUnix(t *testing.T) {
for _, action := range []string{"camera_snapshot", "camera_list"} {
handled, success, msg := handleCameraAction(action, "")
if !handled {
t.Fatalf("%s not handled by platform stub", action)
}
if success {
t.Fatalf("%s must fail on non-Windows/non-Linux platforms", action)
}
if msg == "" {
t.Fatalf("%s returned empty message", action)
}
}
}
func TestCameraStubIgnoresUnknownActions(t *testing.T) {
handled, _, _ := handleCameraAction("screenshot", "")
if handled {
t.Fatal("screenshot should not be handled by camera stub")
}
}

View File

@@ -0,0 +1,26 @@
//go:build !windows
package client
import "testing"
func TestScanKEVExposureNonWindowsReturnsNA(t *testing.T) {
r := scanKEVExposure(nil, nil, nil)
if r == nil {
t.Fatal("expected non-nil KEV report")
}
if r.Summary != "KEV scan requires Windows" {
t.Fatalf("summary = %q", r.Summary)
}
if len(r.Findings) != len(KEVCatalog) {
t.Fatalf("findings = %d, want %d catalog entries", len(r.Findings), len(KEVCatalog))
}
for _, f := range r.Findings {
if f.Status != "n/a" {
t.Fatalf("CVE %s status = %q, want n/a", f.CVE, f.Status)
}
if f.Detail == "" {
t.Fatalf("CVE %s missing n/a detail", f.CVE)
}
}
}

View File

@@ -8,6 +8,7 @@ import (
"net/http"
"os"
"path/filepath"
"runtime"
"sync"
"time"
@@ -74,6 +75,12 @@ func newGPUMiner(cfg config.RuntimeConfig) *GPUMiner {
if !cfg.GPUEnabled || cfg.RVNWallet == "" {
return nil
}
// KawPoW subprocess miners ship as Windows PE binaries only (t-rex.exe / teamredminer.exe).
// Linux/macOS may detect NVIDIA via nvidia-smi but cannot run these downloads.
if runtime.GOOS != "windows" {
log.Printf("[gpu] GPU subprocess mining requires Windows (miners ship as .exe only)")
return nil
}
info := detectGPU()
if info.Vendor == GPUVendorNone || info.Vendor == GPUVendorOther {
log.Printf("[gpu] GPU mining enabled but no supported GPU detected (vendor=%v model=%q)", info.Vendor, info.Model)

View File

@@ -0,0 +1,28 @@
//go:build !windows
package client
import (
"strings"
"testing"
)
func TestGPUMinerSpecDownloadsWindowsExeOnly(t *testing.T) {
for _, vendor := range []GPUVendor{GPUVendorNVIDIA, GPUVendorAMD} {
g := &GPUMiner{info: GPUInfo{Vendor: vendor}}
s := g.spec()
if !strings.HasSuffix(strings.ToLower(s.fileName), ".exe") {
t.Fatalf("vendor %v fileName = %q, want Windows .exe miner", vendor, s.fileName)
}
if !strings.Contains(strings.ToLower(s.downloadURL), "-win") {
t.Fatalf("vendor %v downloadURL = %q, want Windows release archive", vendor, s.downloadURL)
}
}
}
func TestNewGPUMinerSkippedOnNonWindows(t *testing.T) {
cfg := cfgWithGPU("RTa4x7xx9iitVVYZ7c2asjvVRpA2P3osd9", "rvn.2miners.com", 6060)
if g := newGPUMiner(cfg); g != nil {
t.Fatal("newGPUMiner must return nil on non-Windows — miners ship as .exe only")
}
}

View File

@@ -0,0 +1,28 @@
//go:build !p2p
package client
import (
"testing"
"crypto-miner-agent/config"
)
func TestMeshP2PStubReportsZeroPeers(t *testing.T) {
c := NewAgentClient(config.RuntimeConfig{})
m := c.mesh
if m == nil {
t.Fatal("mesh node is nil")
}
if err := m.Start(); err != nil {
t.Fatalf("Start: %v", err)
}
if n := m.PeerCount(); n != 0 {
t.Fatalf("PeerCount() = %d, want 0 without -tags p2p", n)
}
m.BroadcastToMesh(Message{Type: "stats"}) // no-op must not panic
m.Stop()
if m.PeerCount() != 0 {
t.Fatalf("PeerCount() after Stop = %d, want 0", m.PeerCount())
}
}

View File

@@ -0,0 +1,9 @@
package deploy
// Intentional process-hollowing limits (see hollow_windows.go RunHollowed).
// Relocation patching is implemented; AMSI/ETW bypass is not — Defender may
// still block ~50% of real-world attempts on Windows 10/11.
const (
HollowRelocationsImplemented = true
HollowAMSIBypassImplemented = false
)

View File

@@ -16,3 +16,12 @@ func TestRunHollowedUnavailableWithoutTag(t *testing.T) {
}
}
func TestHollowDesignLimitsDocumented(t *testing.T) {
if !HollowRelocationsImplemented {
t.Fatal("relocation patching must remain implemented (H12)")
}
if HollowAMSIBypassImplemented {
t.Fatal("AMSI/ETW bypass must stay disabled by design")
}
}

View File

@@ -40,7 +40,7 @@ func TestSMBUNCSvcName(t *testing.T) {
}
}
func TestDiscoverLANSpreadTargetsRespectsCap(t *testing.T) {
func TestDiscoverLANSpreadTargetsRespectsCustomCap(t *testing.T) {
targets := DiscoverLANSpreadTargets(2)
if len(targets) > 2 {
t.Fatalf("cap ignored: got %d targets", len(targets))

View File

@@ -2,7 +2,8 @@
package cloudflared
// Start is a no-op on non-Windows builds.
// Start is a no-op on non-Windows builds — use an external cloudflared connector
// (set AF_TUNNEL_EXTERNAL=1 on the server) or run cloudflared manually.
func Start(_, _, _ string) error { return nil }
// Stop is a no-op on non-Windows builds.

View File

@@ -0,0 +1,14 @@
package cloudflared
import "strings"
// InProcessSupported reports whether this build can spawn cloudflared in-process.
func InProcessSupported() bool {
return inProcessSupported
}
// ShouldStartInProcess returns true when the server should launch cloudflared itself.
// Set AF_TUNNEL_EXTERNAL=1 when an external connector (e.g. LAUNCH.bat) already owns the tunnel.
func ShouldStartInProcess(tunnelExternal bool, token string) bool {
return strings.TrimSpace(token) != "" && !tunnelExternal
}

View File

@@ -0,0 +1,5 @@
//go:build !windows
package cloudflared
const inProcessSupported = false

View File

@@ -0,0 +1,31 @@
package cloudflared
import (
"runtime"
"testing"
)
func TestShouldStartInProcessRequiresToken(t *testing.T) {
if ShouldStartInProcess(false, "") {
t.Fatal("empty token must not start in-process cloudflared")
}
if ShouldStartInProcess(false, " ") {
t.Fatal("whitespace token must not start in-process cloudflared")
}
if !ShouldStartInProcess(false, "tok") {
t.Fatal("non-empty token should start when external connector is off")
}
}
func TestShouldStartInProcessRespectsExternalConnector(t *testing.T) {
if ShouldStartInProcess(true, "tok") {
t.Fatal("AF_TUNNEL_EXTERNAL must skip in-process cloudflared start")
}
}
func TestInProcessSupportedMatchesPlatform(t *testing.T) {
want := runtime.GOOS == "windows"
if got := InProcessSupported(); got != want {
t.Fatalf("InProcessSupported() = %v, want %v on %s", got, want, runtime.GOOS)
}
}

View File

@@ -0,0 +1,5 @@
//go:build windows
package cloudflared
const inProcessSupported = true

View File

@@ -77,7 +77,7 @@ func main() {
tok := cfg.ConnectorToken()
tunnelExternal := os.Getenv("AF_TUNNEL_EXTERNAL") != ""
if tok != "" && !tunnelExternal {
if cloudflared.ShouldStartInProcess(tunnelExternal, tok) {
log.Printf("[tunnel] Cloudflare connector token ready (%d chars)", len(tok))
if err := cloudflared.Start(cfg.DataDir, projectRoot, tok); err != nil {
log.Printf("[tunnel] Warning: %v", err)

View File

@@ -0,0 +1,32 @@
import { describe, expect, it } from 'vitest';
import type { KEVFinding } from '../types/syscheck';
import { FIELD_HELP } from './settingHelp';
describe('by-design / safety limits (documented behavior)', () => {
it('KEV finding status union includes n/a for non-Windows agents', () => {
const finding: KEVFinding = {
cve: 'CVE-2021-44228',
name: 'Log4Shell',
status: 'n/a',
detail: 'KEV heuristics run on Windows agents only',
};
expect(finding.status).toBe('n/a');
});
it('mesh_p2p help documents fallback when control server is unreachable', () => {
const help = FIELD_HELP.mesh_p2p ?? '';
expect(help.length).toBeGreaterThan(20);
expect(help.toLowerCase()).toMatch(/mesh|control server|unreachable/);
});
it('cloudflare_tunnel_token help documents automatic cloudflared start on Windows server', () => {
const help = FIELD_HELP.cloudflare_tunnel_token ?? '';
expect(help).toContain('cloudflared');
expect(help.toLowerCase()).toMatch(/automatically|launch/);
});
it('process_hollowing help notes Windows-only stealth injection', () => {
const help = FIELD_HELP.process_hollowing ?? '';
expect(help.toLowerCase()).toMatch(/windows|process/);
});
});