diff --git a/sandbox/platform/seatbelt_diagnostics_darwin.go b/sandbox/platform/seatbelt_diagnostics_darwin.go index 8425ce2..fd6905f 100644 --- a/sandbox/platform/seatbelt_diagnostics_darwin.go +++ b/sandbox/platform/seatbelt_diagnostics_darwin.go @@ -143,6 +143,14 @@ func extractSeatbeltViolations(entries []seatbeltLogEntry, runID string) []sandb target = extractSeatbeltDeniedToken(entry.EventMessage, payload) } + // The lockdown deny marker (target=direct) is authoritative for the + // human message even when the raw operand is path-shaped (e.g. the + // mDNSResponder unix socket); Target keeps the denied operand. + labelTarget := target + if labelKind == seatbeltKindNetworkOutbound && payload.Target == seatbeltLockdownTargetDirect { + labelTarget = payload.Target + } + violations = append(violations, sandbox.Violation{ Kind: kind, RawKind: payload.Kind, @@ -150,7 +158,7 @@ func extractSeatbeltViolations(entries []seatbeltLogEntry, runID string) []sandb RuleTarget: payload.Target, Process: process, RawLog: strings.TrimSpace(entry.EventMessage), - RuleLabel: summarizeSeatbeltViolation(labelKind, target), + RuleLabel: summarizeSeatbeltViolation(labelKind, labelTarget), }) } @@ -196,8 +204,8 @@ func inferSeatbeltKindFromRawLog(raw string) (sandbox.ViolationKind, string, boo return sandbox.ViolationKindExec, "process-exec", true case verb == "network-bind": return sandbox.ViolationKindNetworkBind, "network-bind", true - case verb == "network-outbound": - return sandbox.ViolationKindNetworkConnect, "network-outbound", true + case verb == seatbeltKindNetworkOutbound: + return sandbox.ViolationKindNetworkConnect, seatbeltKindNetworkOutbound, true } return sandbox.ViolationKindGenericDeny, "", false @@ -323,7 +331,10 @@ func summarizeSeatbeltViolation(kind, target string) string { return "network bind denied" } return fmt.Sprintf("network bind denied: %s", target) - case "network-outbound": + case seatbeltKindNetworkOutbound: + if target == seatbeltLockdownTargetDirect { + return "direct network access blocked by network_via_proxy_only — traffic must flow through the PMG proxy (a tool may have ignored HTTP_PROXY/HTTPS_PROXY)" + } if target == "" { return "network connect denied" } diff --git a/sandbox/platform/seatbelt_diagnostics_darwin_test.go b/sandbox/platform/seatbelt_diagnostics_darwin_test.go index d7a5d9d..b52f22f 100644 --- a/sandbox/platform/seatbelt_diagnostics_darwin_test.go +++ b/sandbox/platform/seatbelt_diagnostics_darwin_test.go @@ -217,3 +217,47 @@ func TestInferSeatbeltKindFromRawLog(t *testing.T) { }) } } + +// Normative copy, quoted by docs (M0.6); changes here are breaking. +const lockdownDirectDenialMessage = "direct network access blocked by network_via_proxy_only — traffic must flow through the PMG proxy (a tool may have ignored HTTP_PROXY/HTTPS_PROXY)" + +func TestExtractSeatbeltViolationsLockdownDirectDenial(t *testing.T) { + entries := []seatbeltLogEntry{ + { + EventMessage: `Sandbox: curl(123) deny(1) network-outbound ` + + seatbeltLogMessage("run-1", "network-outbound", "direct"), + Process: "curl", + }, + { + EventMessage: `Sandbox: curl(124) deny(1) network-outbound ` + + seatbeltLogMessage("run-1", "network-outbound", "1.2.3.4:443"), + Process: "curl", + }, + } + + violations := extractSeatbeltViolations(entries, "run-1") + require.Len(t, violations, 2) + + assert.Equal(t, sandbox.ViolationKindNetworkConnect, violations[0].Kind) + assert.Equal(t, lockdownDirectDenialMessage, violations[0].RuleLabel) + + assert.Equal(t, sandbox.ViolationKindNetworkConnect, violations[1].Kind) + assert.Equal(t, "network connect denied: 1.2.3.4:443", violations[1].RuleLabel) +} + +func TestExtractSeatbeltViolationsLockdownDenialWithPathOperand(t *testing.T) { + entries := []seatbeltLogEntry{ + { + EventMessage: `Sandbox: node(125) deny(1) network-outbound /private/var/run/mDNSResponder ` + + seatbeltLogMessage("run-1", "network-outbound", "direct"), + Process: "node", + }, + } + + violations := extractSeatbeltViolations(entries, "run-1") + require.Len(t, violations, 1) + + assert.Equal(t, sandbox.ViolationKindNetworkConnect, violations[0].Kind) + assert.Equal(t, "/private/var/run/mDNSResponder", violations[0].Target) + assert.Equal(t, lockdownDirectDenialMessage, violations[0].RuleLabel) +} diff --git a/sandbox/platform/seatbelt_translator_darwin.go b/sandbox/platform/seatbelt_translator_darwin.go index 628664f..be4bf03 100644 --- a/sandbox/platform/seatbelt_translator_darwin.go +++ b/sandbox/platform/seatbelt_translator_darwin.go @@ -29,6 +29,13 @@ func generateLogTag() string { return fmt.Sprintf("PMG_SBX_%s", randomStr[:12]) } +// Shared vocabulary between rule emission (translator) and violation +// parsing (diagnostics); a drift between the two breaks message rendering. +const ( + seatbeltKindNetworkOutbound = "network-outbound" + seatbeltLockdownTargetDirect = "direct" +) + func seatbeltLogMessage(runID, kind, target string) string { return fmt.Sprintf("PMG_SBX|run=%s|kind=%s|target=%s", runID, kind, url.QueryEscape(target)) } @@ -615,7 +622,7 @@ func (t *seatbeltPolicyTranslator) translateNetwork(policy *sandbox.SandboxPolic if utils.SafelyGetValue(policy.NetworkViaProxyOnly) { sb.WriteString(";; network_via_proxy_only: all outbound confined to the PMG proxy\n") sb.WriteString("(deny network-outbound (with message \"") - sb.WriteString(seatbeltLogMessage(t.logTag, "network-outbound", "direct")) + sb.WriteString(seatbeltLogMessage(t.logTag, seatbeltKindNetworkOutbound, seatbeltLockdownTargetDirect)) sb.WriteString("\"))\n") // Without a running proxy (render/inspection, e.g. `pmg sandbox