//go:build darwin // +build darwin package seatbelt import ( "fmt" "path/filepath" "strings" "github.com/safedep/pmg/sandbox/util" ) // PolicyTranslator translates PMG sandbox policies to Seatbelt Profile Language (.sb). type PolicyTranslator struct{} // NewPolicyTranslator creates a new policy translator. func NewPolicyTranslator() *PolicyTranslator { return &PolicyTranslator{} } // SandboxPolicy represents a parsed sandbox policy (defined here to avoid import cycle). type SandboxPolicy struct { Name string Description string PackageManagers []string ViolationMode string Filesystem FilesystemPolicy Network NetworkPolicy Process ProcessPolicy } type FilesystemPolicy struct { AllowRead []string AllowWrite []string DenyRead []string DenyWrite []string } type NetworkPolicy struct { AllowOutbound []string DenyOutbound []string } type ProcessPolicy struct { AllowExec []string DenyExec []string } // Translate converts a PMG SandboxPolicy to Seatbelt Profile Language. func (t *PolicyTranslator) Translate(policy *SandboxPolicy) (string, error) { var sb strings.Builder // Header sb.WriteString("(version 1)\n") sb.WriteString(fmt.Sprintf(";; PMG Sandbox Policy: %s\n", policy.Name)) sb.WriteString(fmt.Sprintf(";; %s\n", policy.Description)) sb.WriteString(";; Generated by PMG sandbox system\n\n") // Default policy: deny by default for maximum security sb.WriteString("(deny default)\n\n") // Allow basic system operations required for any process sb.WriteString(";; Basic system access\n") sb.WriteString("(allow process-fork)\n") sb.WriteString("(allow process-exec-interpreter)\n") sb.WriteString("(allow sysctl-read)\n") sb.WriteString("(allow mach-lookup)\n") sb.WriteString("(allow mach-register)\n") sb.WriteString("(allow ipc-posix-shm)\n") sb.WriteString("(allow signal)\n\n") // Filesystem rules if err := t.translateFilesystem(policy, &sb); err != nil { return "", fmt.Errorf("failed to translate filesystem rules: %w", err) } // Network rules if err := t.translateNetwork(policy, &sb); err != nil { return "", fmt.Errorf("failed to translate network rules: %w", err) } // Process execution rules if err := t.translateProcess(policy, &sb); err != nil { return "", fmt.Errorf("failed to translate process rules: %w", err) } return sb.String(), nil } // translateFilesystem translates filesystem access rules. func (t *PolicyTranslator) translateFilesystem(policy *SandboxPolicy, sb *strings.Builder) error { sb.WriteString(";; Filesystem access\n") // Expand and add allow read rules for _, pattern := range policy.Filesystem.AllowRead { expanded, err := util.ExpandVariables(pattern) if err != nil { return fmt.Errorf("failed to expand pattern %s: %w", pattern, err) } // Handle glob patterns vs literal paths if util.ContainsGlob(expanded) { // For glob patterns, use subpath with the base directory baseDir := filepath.Dir(strings.TrimSuffix(expanded, "/**")) sb.WriteString(fmt.Sprintf("(allow file-read* (subpath \"%s\"))\n", baseDir)) } else { sb.WriteString(fmt.Sprintf("(allow file-read* (subpath \"%s\"))\n", expanded)) } } sb.WriteString("\n") // Expand and add allow write rules for _, pattern := range policy.Filesystem.AllowWrite { expanded, err := util.ExpandVariables(pattern) if err != nil { return fmt.Errorf("failed to expand pattern %s: %w", pattern, err) } if util.ContainsGlob(expanded) { baseDir := filepath.Dir(strings.TrimSuffix(expanded, "/**")) sb.WriteString(fmt.Sprintf("(allow file-write* (subpath \"%s\"))\n", baseDir)) } else { sb.WriteString(fmt.Sprintf("(allow file-write* (subpath \"%s\"))\n", expanded)) } } sb.WriteString("\n") // Deny rules have higher priority (applied after allow) // Note: Seatbelt evaluates rules in order, so denies after allows will override for _, pattern := range policy.Filesystem.DenyRead { expanded, err := util.ExpandVariables(pattern) if err != nil { return fmt.Errorf("failed to expand pattern %s: %w", pattern, err) } if util.ContainsGlob(expanded) { baseDir := filepath.Dir(strings.TrimSuffix(expanded, "/**")) sb.WriteString(fmt.Sprintf("(deny file-read* (subpath \"%s\"))\n", baseDir)) } else { sb.WriteString(fmt.Sprintf("(deny file-read* (subpath \"%s\"))\n", expanded)) } } for _, pattern := range policy.Filesystem.DenyWrite { expanded, err := util.ExpandVariables(pattern) if err != nil { return fmt.Errorf("failed to expand pattern %s: %w", pattern, err) } if util.ContainsGlob(expanded) { baseDir := filepath.Dir(strings.TrimSuffix(expanded, "/**")) sb.WriteString(fmt.Sprintf("(deny file-write* (subpath \"%s\"))\n", baseDir)) } else { sb.WriteString(fmt.Sprintf("(deny file-write* (subpath \"%s\"))\n", expanded)) } } sb.WriteString("\n") return nil } // translateNetwork translates network access rules. func (t *PolicyTranslator) translateNetwork(policy *SandboxPolicy, sb *strings.Builder) error { sb.WriteString(";; Network access\n") // If there are allow outbound rules, allow network-outbound generally // (Seatbelt doesn't support fine-grained host:port filtering in all cases) // Note: This is a limitation of Seatbelt - for more fine-grained control, // consider using a network filtering solution or firewall rules if len(policy.Network.AllowOutbound) > 0 { sb.WriteString(";; Network outbound allowed to specific hosts\n") sb.WriteString(";; Note: Seatbelt has limited host-based filtering, consider using firewall rules for strict control\n") sb.WriteString("(allow network-outbound)\n") } // If deny outbound includes "*:*", block all network for _, pattern := range policy.Network.DenyOutbound { if pattern == "*:*" { sb.WriteString(";; Network outbound denied\n") sb.WriteString("(deny network-outbound)\n") break } } sb.WriteString("\n") return nil } // translateProcess translates process execution rules. func (t *PolicyTranslator) translateProcess(policy *SandboxPolicy, sb *strings.Builder) error { sb.WriteString(";; Process execution\n") // Add allow exec rules for _, exePath := range policy.Process.AllowExec { expanded, err := util.ExpandVariables(exePath) if err != nil { return fmt.Errorf("failed to expand exec path %s: %w", exePath, err) } if util.ContainsGlob(expanded) { // For glob patterns, use subpath to allow anything under that directory baseDir := filepath.Dir(strings.TrimSuffix(expanded, "/**")) sb.WriteString(fmt.Sprintf("(allow process-exec* (subpath \"%s\"))\n", baseDir)) } else { sb.WriteString(fmt.Sprintf("(allow process-exec* (literal \"%s\"))\n", expanded)) } } sb.WriteString("\n") // Add deny exec rules for _, exePath := range policy.Process.DenyExec { expanded, err := util.ExpandVariables(exePath) if err != nil { return fmt.Errorf("failed to expand exec path %s: %w", exePath, err) } if util.ContainsGlob(expanded) { baseDir := filepath.Dir(strings.TrimSuffix(expanded, "/**")) sb.WriteString(fmt.Sprintf("(deny process-exec* (subpath \"%s\"))\n", baseDir)) } else { sb.WriteString(fmt.Sprintf("(deny process-exec* (literal \"%s\"))\n", expanded)) } } sb.WriteString("\n") return nil }