//go:build darwin // +build darwin package platform import ( "fmt" "path/filepath" "strings" "github.com/safedep/pmg/sandbox" "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{} } // translate converts a PMG SandboxPolicy to Seatbelt Profile Language. func (t *policyTranslator) translate(policy *sandbox.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 *sandbox.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 *sandbox.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 *sandbox.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 }