mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
feat(sandbox): actionable violation message for lockdown network denials (#373)
This commit is contained in:
@@ -143,6 +143,14 @@ func extractSeatbeltViolations(entries []seatbeltLogEntry, runID string) []sandb
|
|||||||
target = extractSeatbeltDeniedToken(entry.EventMessage, payload)
|
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{
|
violations = append(violations, sandbox.Violation{
|
||||||
Kind: kind,
|
Kind: kind,
|
||||||
RawKind: payload.Kind,
|
RawKind: payload.Kind,
|
||||||
@@ -150,7 +158,7 @@ func extractSeatbeltViolations(entries []seatbeltLogEntry, runID string) []sandb
|
|||||||
RuleTarget: payload.Target,
|
RuleTarget: payload.Target,
|
||||||
Process: process,
|
Process: process,
|
||||||
RawLog: strings.TrimSpace(entry.EventMessage),
|
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
|
return sandbox.ViolationKindExec, "process-exec", true
|
||||||
case verb == "network-bind":
|
case verb == "network-bind":
|
||||||
return sandbox.ViolationKindNetworkBind, "network-bind", true
|
return sandbox.ViolationKindNetworkBind, "network-bind", true
|
||||||
case verb == "network-outbound":
|
case verb == seatbeltKindNetworkOutbound:
|
||||||
return sandbox.ViolationKindNetworkConnect, "network-outbound", true
|
return sandbox.ViolationKindNetworkConnect, seatbeltKindNetworkOutbound, true
|
||||||
}
|
}
|
||||||
|
|
||||||
return sandbox.ViolationKindGenericDeny, "", false
|
return sandbox.ViolationKindGenericDeny, "", false
|
||||||
@@ -323,7 +331,10 @@ func summarizeSeatbeltViolation(kind, target string) string {
|
|||||||
return "network bind denied"
|
return "network bind denied"
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("network bind denied: %s", target)
|
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 == "" {
|
if target == "" {
|
||||||
return "network connect denied"
|
return "network connect denied"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ func generateLogTag() string {
|
|||||||
return fmt.Sprintf("PMG_SBX_%s", randomStr[:12])
|
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 {
|
func seatbeltLogMessage(runID, kind, target string) string {
|
||||||
return fmt.Sprintf("PMG_SBX|run=%s|kind=%s|target=%s", runID, kind, url.QueryEscape(target))
|
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) {
|
if utils.SafelyGetValue(policy.NetworkViaProxyOnly) {
|
||||||
sb.WriteString(";; network_via_proxy_only: all outbound confined to the PMG proxy\n")
|
sb.WriteString(";; network_via_proxy_only: all outbound confined to the PMG proxy\n")
|
||||||
sb.WriteString("(deny network-outbound (with message \"")
|
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")
|
sb.WriteString("\"))\n")
|
||||||
|
|
||||||
// Without a running proxy (render/inspection, e.g. `pmg sandbox
|
// Without a running proxy (render/inspection, e.g. `pmg sandbox
|
||||||
|
|||||||
Reference in New Issue
Block a user