This repository has been archived on 2026-03-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
greywall/internal/sandbox/monitor.go
Jose B 6be1cf5620
Some checks failed
Build and test / Lint (pull_request) Failing after 1m3s
Build and test / Test (Linux) (pull_request) Failing after 39s
Build and test / Build (pull_request) Successful in 19s
feat: add domain-based outbound filtering with allowedDomains/deniedDomains
Add NetworkConfig.AllowedDomains and DeniedDomains fields for controlling
outbound connections by hostname. Deny rules are checked first (deny wins).
When AllowedDomains is set, only matching domains are permitted. When only
DeniedDomains is set, all domains except denied ones are allowed.

Implement FilteringProxy that wraps gost HTTP proxy with domain enforcement
via AllowConnect callback. Skip GreyHaven proxy/DNS defaults
2026-02-17 11:52:43 -05:00

187 lines
4.4 KiB
Go

package sandbox
import (
"bufio"
"context"
"fmt"
"os"
"os/exec"
"regexp"
"strings"
"time"
"gitea.app.monadical.io/monadical/greywall/internal/platform"
)
// LogMonitor monitors sandbox violations via macOS log stream.
type LogMonitor struct {
sessionSuffix string
cmd *exec.Cmd
cancel context.CancelFunc
running bool
}
// NewLogMonitor creates a new log monitor for the given session suffix.
// Returns nil on non-macOS platforms.
func NewLogMonitor(sessionSuffix string) *LogMonitor {
if platform.Detect() != platform.MacOS {
return nil
}
return &LogMonitor{
sessionSuffix: sessionSuffix,
}
}
// Start begins monitoring the macOS unified log for sandbox violations.
func (m *LogMonitor) Start() error {
if m == nil {
return nil
}
ctx, cancel := context.WithCancel(context.Background())
m.cancel = cancel
// Build predicate to filter for this session's violations only
predicate := fmt.Sprintf(`eventMessage ENDSWITH "%s"`, m.sessionSuffix)
m.cmd = exec.CommandContext(ctx, "log", "stream", //nolint:gosec // predicate is constructed from trusted session suffix
"--predicate", predicate,
"--style", "compact",
)
stdout, err := m.cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("failed to create stdout pipe: %w", err)
}
if err := m.cmd.Start(); err != nil {
return fmt.Errorf("failed to start log stream: %w", err)
}
m.running = true
// Parse log output in background
go func() {
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
line := scanner.Text()
if violation := parseViolation(line); violation != "" {
fmt.Fprintf(os.Stderr, "%s\n", violation)
}
}
}()
// Give log stream a moment to initialize
time.Sleep(100 * time.Millisecond)
return nil
}
// Stop stops the log monitor.
func (m *LogMonitor) Stop() {
if m == nil || !m.running {
return
}
// Give a moment for any pending events to be processed
time.Sleep(500 * time.Millisecond)
if m.cancel != nil {
m.cancel()
}
if m.cmd != nil && m.cmd.Process != nil {
_ = m.cmd.Process.Kill()
_ = m.cmd.Wait()
}
m.running = false
}
// violationPattern matches sandbox denial log entries
var violationPattern = regexp.MustCompile(`Sandbox: (\w+)\((\d+)\) deny\(\d+\) (\S+)(.*)`)
// parseViolation extracts and formats a sandbox violation from a log line.
// Returns empty string if the line should be filtered out.
func parseViolation(line string) string {
if strings.HasPrefix(line, "Filtering") || strings.HasPrefix(line, "Timestamp") {
return ""
}
if strings.Contains(line, "duplicate report") {
return ""
}
if strings.HasPrefix(line, "CMD64_") {
return ""
}
// Match violation pattern
matches := violationPattern.FindStringSubmatch(line)
if matches == nil {
return ""
}
process := matches[1]
pid := matches[2]
operation := matches[3]
details := strings.TrimSpace(matches[4])
if !shouldShowViolation(operation) {
return ""
}
if isNoisyViolation(details) {
return ""
}
timestamp := time.Now().Format("15:04:05")
if details != "" {
return fmt.Sprintf("\033[31m[greywall:logstream] %s ✗ %s %s (%s:%s)\033[0m", timestamp, operation, details, process, pid)
}
return fmt.Sprintf("\033[31m[greywall:logstream] %s ✗ %s (%s:%s)\033[0m", timestamp, operation, process, pid)
}
// shouldShowViolation returns true if this violation type should be displayed.
func shouldShowViolation(operation string) bool {
if strings.HasPrefix(operation, "network-") {
return true
}
if strings.HasPrefix(operation, "file-read") ||
strings.HasPrefix(operation, "file-write") {
return true
}
// Filter out everything else (mach-lookup, file-ioctl, etc.)
return false
}
// isNoisyViolation returns true if this violation is system noise that should be filtered.
func isNoisyViolation(details string) bool {
// Filter out TTY/terminal writes (very noisy from any process that prints output)
if strings.HasPrefix(details, "/dev/tty") ||
strings.HasPrefix(details, "/dev/pts") {
return true
}
// Filter out mDNSResponder (system DNS resolution socket)
if strings.Contains(details, "mDNSResponder") {
return true
}
// Filter out other system sockets that are typically noise
if strings.HasPrefix(details, "/private/var/run/syslog") {
return true
}
return false
}
// GetSessionSuffix returns the session suffix used for filtering.
// This is the same suffix used in macOS sandbox-exec profiles.
func GetSessionSuffix() string {
return sessionSuffix
}