feat: switch macOS learning mode from fs_usage to eslogger

Replace fs_usage (reports Mach thread IDs, requiring process name matching
with false positives) with eslogger (Endpoint Security framework, reports
real Unix PIDs via audit_token.pid plus fork events for process tree tracking).

Key changes:
- Daemon starts eslogger instead of fs_usage, with early-exit detection
  and clear Full Disk Access error messaging
- New two-pass eslogger JSON parser: pass 1 builds PID tree from fork
  events, pass 2 filters filesystem events by PID set
- Remove runtime PID polling (StartPIDTracking, pollDescendantPIDs) —
  process tree is now built post-hoc from the eslogger log
- Platform-specific generateLearnedTemplatePlatform() for darwin/linux/stub
- Refactor TraceResult and GenerateLearnedTemplate to be platform-agnostic
This commit is contained in:
2026-02-26 17:23:43 -06:00
parent e05b54ec1b
commit 9d5d852860
14 changed files with 1434 additions and 70 deletions

View File

@@ -30,12 +30,16 @@ type Manager struct {
debug bool
monitor bool
initialized bool
learning bool // learning mode: permissive sandbox with strace
straceLogPath string // host-side temp file for strace output
learning bool // learning mode: permissive sandbox with strace/eslogger
straceLogPath string // host-side temp file for strace output (Linux)
commandName string // name of the command being learned
// macOS daemon session fields
daemonClient *daemon.Client
daemonSession *DaemonSession
// macOS learning mode fields
learningID string // daemon learning session ID
learningLog string // eslogger log file path
learningRootPID int // root PID of the command being learned
}
// NewManager creates a new sandbox manager.
@@ -77,6 +81,28 @@ func (m *Manager) Initialize() error {
return fmt.Errorf("sandbox is not supported on platform: %s", platform.Detect())
}
// On macOS in learning mode, use the daemon for eslogger tracing only.
// No TUN/pf/DNS session needed — the command runs unsandboxed.
if platform.Detect() == platform.MacOS && m.learning {
client := daemon.NewClient(daemon.DefaultSocketPath, m.debug)
if !client.IsRunning() {
return fmt.Errorf("greywall daemon is not running (required for macOS learning mode)\n\n" +
" Install and start: sudo greywall daemon install\n" +
" Check status: greywall daemon status")
}
m.logDebug("Daemon is running, requesting learning session")
resp, err := client.StartLearning()
if err != nil {
return fmt.Errorf("failed to start learning session: %w", err)
}
m.daemonClient = client
m.learningID = resp.LearningID
m.learningLog = resp.LearningLog
m.logDebug("Learning session started: id=%s log=%s", m.learningID, m.learningLog)
m.initialized = true
return nil
}
// On macOS, the daemon is required for transparent proxying.
// Without it, env-var proxying is unreliable (only works for tools that
// honor HTTP_PROXY) and gives users a false sense of security.
@@ -187,6 +213,10 @@ func (m *Manager) WrapCommand(command string) (string, error) {
plat := platform.Detect()
switch plat {
case platform.MacOS:
if m.learning {
// In learning mode, run command directly (no sandbox-exec wrapping)
return command, nil
}
return WrapCommandMacOS(m.config, command, m.exposedPorts, m.daemonSession, m.debug)
case platform.Linux:
if m.learning {
@@ -220,26 +250,30 @@ func (m *Manager) wrapCommandLearning(command string) (string, error) {
})
}
// GenerateLearnedTemplate generates a config template from the strace log collected during learning.
// GenerateLearnedTemplate generates a config template from the trace log collected during learning.
// Platform-specific implementation in manager_linux.go / manager_darwin.go.
func (m *Manager) GenerateLearnedTemplate(cmdName string) (string, error) {
if m.straceLogPath == "" {
return "", fmt.Errorf("no strace log available (was learning mode enabled?)")
}
return m.generateLearnedTemplatePlatform(cmdName)
}
templatePath, err := GenerateLearnedTemplate(m.straceLogPath, cmdName, m.debug)
if err != nil {
return "", err
}
// Clean up strace log since we've processed it
_ = os.Remove(m.straceLogPath)
m.straceLogPath = ""
return templatePath, nil
// SetLearningRootPID records the root PID of the command being learned.
// The eslogger log parser uses this to build the process tree from fork events.
func (m *Manager) SetLearningRootPID(pid int) {
m.learningRootPID = pid
m.logDebug("Set learning root PID: %d", pid)
}
// Cleanup stops the proxies and cleans up resources.
func (m *Manager) Cleanup() {
// Stop macOS learning session if active
if m.daemonClient != nil && m.learningID != "" {
m.logDebug("Stopping learning session %s", m.learningID)
if err := m.daemonClient.StopLearning(m.learningID); err != nil {
m.logDebug("Warning: failed to stop learning session: %v", err)
}
m.learningID = ""
}
// Destroy macOS daemon session if active.
if m.daemonClient != nil && m.daemonSession != nil {
m.logDebug("Destroying daemon session %s", m.daemonSession.SessionID)
@@ -247,9 +281,11 @@ func (m *Manager) Cleanup() {
m.logDebug("Warning: failed to destroy daemon session: %v", err)
}
m.daemonSession = nil
m.daemonClient = nil
}
// Clear daemon client after all daemon interactions
m.daemonClient = nil
if m.reverseBridge != nil {
m.reverseBridge.Cleanup()
}
@@ -266,6 +302,10 @@ func (m *Manager) Cleanup() {
_ = os.Remove(m.straceLogPath)
m.straceLogPath = ""
}
if m.learningLog != "" {
_ = os.Remove(m.learningLog)
m.learningLog = ""
}
m.logDebug("Sandbox manager cleaned up")
}