2026-05-12 18:11:18 +05:30
//go:build darwin
// +build darwin
package platform
import (
"testing"
"github.com/safedep/pmg/sandbox"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseSeatbeltLogPayload ( t * testing . T ) {
msg := `Sandbox: node(123) deny(1) file-write-data /Users/dev/project/.env ` +
seatbeltLogMessage ( "run-1" , "file-write" , "/Users/dev/project/.env" )
payload , ok := parseSeatbeltLogPayload ( msg )
require . True ( t , ok )
assert . Equal ( t , "run-1" , payload . RunID )
assert . Equal ( t , "file-write" , payload . Kind )
assert . Equal ( t , "/Users/dev/project/.env" , payload . Target )
}
func TestParseSeatbeltLogPayloadEmptyTarget ( t * testing . T ) {
msg := `Sandbox: node(123) deny(1) default ` + seatbeltLogMessage ( "run-1" , "default" , "" )
payload , ok := parseSeatbeltLogPayload ( msg )
require . True ( t , ok )
assert . Equal ( t , "run-1" , payload . RunID )
assert . Equal ( t , "default" , payload . Kind )
assert . Empty ( t , payload . Target )
}
func TestExtractSeatbeltDeniedPath ( t * testing . T ) {
payload := & seatbeltLogPayload {
RunID : "run-1" ,
Kind : "file-read" ,
Target : "**/.env" ,
}
raw := `Sandbox: node(123) deny(1) file-read-data ./.env ` + seatbeltLogMessage ( "run-1" , "file-read" , "**/.env" )
assert . Equal ( t , "./.env" , extractSeatbeltDeniedPath ( raw , payload ))
}
func TestDecodeSeatbeltLogEntries ( t * testing . T ) {
data := [] byte ( `[
{"eventMessage":"entry-1","process":"node"},
{"eventMessage":"entry-2","process":"npm"}
]` )
entries , err := decodeSeatbeltLogEntries ( data )
require . NoError ( t , err )
require . Len ( t , entries , 2 )
assert . Equal ( t , "entry-1" , entries [ 0 ]. EventMessage )
assert . Equal ( t , "npm" , entries [ 1 ]. Process )
}
func TestExtractSeatbeltViolations ( t * testing . T ) {
entries := [] seatbeltLogEntry {
{
EventMessage : "Sandbox deny " + seatbeltLogMessage ( "run-1" , "file-read" , "/tmp/.env" ),
Process : "node" ,
},
{
EventMessage : "Sandbox deny " + seatbeltLogMessage ( "run-2" , "file-write" , "/tmp/out" ),
Process : "npm" ,
},
}
violations := extractSeatbeltViolations ( entries , "run-1" )
require . Len ( t , violations , 1 )
assert . Equal ( t , sandbox . ViolationKindFSRead , violations [ 0 ]. Kind )
assert . Equal ( t , "file-read" , violations [ 0 ]. RawKind )
assert . Equal ( t , "/tmp/.env" , violations [ 0 ]. Target )
assert . Equal ( t , "/tmp/.env" , violations [ 0 ]. RuleTarget )
assert . Equal ( t , "node" , violations [ 0 ]. Process )
}
2026-05-19 14:40:54 +05:30
// Regression: a denial that hits the catch-all `(deny default ...)` rule
// must still be classified as the typed kind (and get a typed label) by
// recovering the verb from the sandbox-exec preamble. Without this, the
// actual failure-causing operation (e.g. a write) gets buried below
// incidental typed reads in primaryViolation ranking.
func TestExtractSeatbeltViolationsRecoversTypedKindFromDefaultDeny ( t * testing . T ) {
entries := [] seatbeltLogEntry {
{
EventMessage : `Sandbox: node(59437) deny(1) file-write-data /Users/dev/project/.astro/types.d.ts ` +
seatbeltLogMessage ( "run-1" , "default" , "" ),
Process : "node" ,
},
{
EventMessage : `Sandbox: node(59438) deny(1) network-outbound 1.2.3.4:443 ` +
seatbeltLogMessage ( "run-1" , "default" , "" ),
Process : "node" ,
},
}
violations := extractSeatbeltViolations ( entries , "run-1" )
require . Len ( t , violations , 2 )
// File write hit the catch-all deny — Kind is recovered, RawKind stays
// "default" to preserve the "catch-all match" signal, and the label
// reflects the recovered kind so users see "write access denied".
assert . Equal ( t , sandbox . ViolationKindFSWrite , violations [ 0 ]. Kind )
assert . Equal ( t , "default" , violations [ 0 ]. RawKind )
assert . Equal ( t , "/Users/dev/project/.astro/types.d.ts" , violations [ 0 ]. Target )
assert . Equal ( t , "write access denied: /Users/dev/project/.astro/types.d.ts" , violations [ 0 ]. RuleLabel )
2026-07-06 23:38:38 +05:30
// Network denial: kind recovered from the verb, and the operand is an
// address rather than a path, so it comes from the raw-token fallback.
assert . Equal ( t , sandbox . ViolationKindNetworkConnect , violations [ 1 ]. Kind )
2026-05-19 14:40:54 +05:30
assert . Equal ( t , "default" , violations [ 1 ]. RawKind )
2026-07-06 23:38:38 +05:30
assert . Equal ( t , "1.2.3.4:443" , violations [ 1 ]. Target )
assert . Equal ( t , "network connect denied: 1.2.3.4:443" , violations [ 1 ]. RuleLabel )
}
// Regression: a network-bind denial (e.g. httptest listeners under go test)
// must surface as a typed network violation with the denied address, not as a
// bare generic deny that loses primary ranking to incidental fs noise.
func TestExtractSeatbeltViolationsRecoversNetworkBind ( t * testing . T ) {
entries := [] seatbeltLogEntry {
{
EventMessage : `Sandbox: scratch(36119) deny(1) network-bind local:*:0 ` +
seatbeltLogMessage ( "run-1" , "default" , "" ),
Process : "scratch" ,
},
}
violations := extractSeatbeltViolations ( entries , "run-1" )
require . Len ( t , violations , 1 )
assert . Equal ( t , sandbox . ViolationKindNetworkBind , violations [ 0 ]. Kind )
assert . Equal ( t , "default" , violations [ 0 ]. RawKind )
assert . Equal ( t , "local:*:0" , violations [ 0 ]. Target )
assert . Equal ( t , "network bind denied: local:*:0" , violations [ 0 ]. RuleLabel )
2026-05-19 14:40:54 +05:30
}
func TestInferSeatbeltKindFromRawLog ( t * testing . T ) {
tests := [] struct {
name string
raw string
wantKind sandbox . ViolationKind
wantLabel string
wantOK bool
}{
{
name : "file-write-data maps to fs_write" ,
raw : `Sandbox: node(1) deny(1) file-write-data /tmp/x` ,
wantKind : sandbox . ViolationKindFSWrite ,
wantLabel : "file-write" ,
wantOK : true ,
},
{
name : "file-write-unlink maps to fs_delete_or_rename" ,
raw : `Sandbox: node(1) deny(1) file-write-unlink /tmp/x` ,
wantKind : sandbox . ViolationKindFSDeleteOrRename ,
wantLabel : "file-write-unlink" ,
wantOK : true ,
},
{
name : "file-rename maps to fs_delete_or_rename" ,
raw : `Sandbox: node(1) deny(1) file-rename /tmp/a` ,
wantKind : sandbox . ViolationKindFSDeleteOrRename ,
wantLabel : "file-write-unlink" ,
wantOK : true ,
},
{
name : "file-read-metadata maps to fs_read" ,
raw : `Sandbox: node(1) deny(1) file-read-metadata /tmp/x` ,
wantKind : sandbox . ViolationKindFSRead ,
wantLabel : "file-read" ,
wantOK : true ,
},
{
name : "process-exec maps to exec" ,
raw : `Sandbox: node(1) deny(1) process-exec /usr/bin/curl` ,
wantKind : sandbox . ViolationKindExec ,
wantLabel : "process-exec" ,
wantOK : true ,
},
{
name : "process-fork is not exec" ,
raw : `Sandbox: node(1) deny(1) process-fork` ,
wantKind : sandbox . ViolationKindGenericDeny ,
wantOK : false ,
},
{
2026-07-06 23:38:38 +05:30
name : "network-outbound maps to network_connect" ,
raw : `Sandbox: node(1) deny(1) network-outbound 1.2.3.4:443` ,
wantKind : sandbox . ViolationKindNetworkConnect ,
wantLabel : "network-outbound" ,
wantOK : true ,
},
{
name : "network-bind maps to network_bind" ,
raw : `Sandbox: scratch(1) deny(1) network-bind local:*:0` ,
wantKind : sandbox . ViolationKindNetworkBind ,
wantLabel : "network-bind" ,
wantOK : true ,
2026-05-19 14:40:54 +05:30
},
{
name : "log without deny prefix is not recognized" ,
raw : `something unrelated` ,
wantKind : sandbox . ViolationKindGenericDeny ,
wantOK : false ,
},
}
for _ , tc := range tests {
t . Run ( tc . name , func ( t * testing . T ) {
kind , label , ok := inferSeatbeltKindFromRawLog ( tc . raw )
assert . Equal ( t , tc . wantOK , ok )
assert . Equal ( t , tc . wantKind , kind )
if tc . wantOK {
assert . Equal ( t , tc . wantLabel , label )
}
})
}
}
2026-07-10 22:01:10 +05:30
// 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 )
}