lib/sandbox/src/policy.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const audit_data = @import("audit.zig");
3
4 pub const Preference = enum {
5 allow_inherited,
6 prefer_isolated,
7 require_isolated,
8 };
9
10 /// States what isolation a run has to have before the run starts: the policy
11 /// records what the caller asks of the filesystem, process, and network
12 /// dimensions, each judged on its own. A required dimension rejects an audit
13 /// weaker than isolated, returning `error.UnmetFilesystemIsolation` ,
14 /// `error.UnmetProcessIsolation` , or `error.UnmetNetworkIsolation` before the
15 /// child runs. By contrast, a preferred dimension records what the caller wants
16 /// and admits every audit, which backend selection reads to pick a runner. Both
17 /// filesystem values count as isolated, so requiring the filesystem dimension
18 /// admits every audit the package can produce. The environment dimension
19 /// appears in the audit and has no entry here, so a policy constrains it in no
20 /// way.
21 pub const Policy = struct {
22 filesystem: Preference = .require_isolated,
23 process: Preference = .allow_inherited,
24 network: Preference = .allow_inherited,
25
26 pub fn validate(self: Policy, audit: audit_data.Audit) !void {
27 if (self.filesystem == .require_isolated and !filesystemIsolated(audit.filesystem)) return error.UnmetFilesystemIsolation;
28 if (self.process == .require_isolated and !processIsolated(audit.process)) return error.UnmetProcessIsolation;
29 if (self.network == .require_isolated and !networkIsolated(audit.network)) return error.UnmetNetworkIsolation;
30 }
31
32 pub fn prefersFilesystemIsolation(self: Policy) bool {
33 return self.filesystem != .allow_inherited;
34 }
35
36 pub fn prefersProcessIsolation(self: Policy) bool {
37 return self.process != .allow_inherited;
38 }
39
40 pub fn prefersNetworkIsolation(self: Policy) bool {
41 return self.network != .allow_inherited;
42 }
43 };
44
45 pub fn filesystemIsolated(value: audit_data.Filesystem) bool {
46 return switch (value) {
47 .copied, .overlay => true,
48 };
49 }
50
51 pub fn processIsolated(value: audit_data.Process) bool {
52 return switch (value) {
53 .host => false,
54 .bubblewrap_namespace => true,
55 };
56 }
57
58 pub fn networkIsolated(value: audit_data.Network) bool {
59 return switch (value) {
60 .inherited => false,
61 .bubblewrap_namespace => true,
62 };
63 }
64
65 test "policy validates required isolation dimensions" {
66 const inherited = audit_data.Audit{};
67 try (Policy{}).validate(inherited);
68 try std.testing.expectError(error.UnmetProcessIsolation, (Policy{ .process = .require_isolated }).validate(inherited));
69 try std.testing.expectError(error.UnmetNetworkIsolation, (Policy{ .network = .require_isolated }).validate(inherited));
70
71 const inherited_bwrap = audit_data.Audit{
72 .runner = .bubblewrap_overlay,
73 .filesystem = .overlay,
74 .process = .bubblewrap_namespace,
75 .network = .inherited,
76 };
77 try (Policy{ .process = .require_isolated }).validate(inherited_bwrap);
78 try std.testing.expectError(
79 error.UnmetNetworkIsolation,
80 (Policy{ .network = .require_isolated }).validate(inherited_bwrap),
81 );
82
83 var isolated_bwrap = inherited_bwrap;
84 isolated_bwrap.network = .bubblewrap_namespace;
85 try (Policy{ .network = .require_isolated }).validate(isolated_bwrap);
86 }
87
88 test "policy exposes preferences without enforcing them" {
89 const policy = Policy{
90 .filesystem = .prefer_isolated,
91 .process = .prefer_isolated,
92 .network = .allow_inherited,
93 };
94 try policy.validate(.{});
95 try std.testing.expect(policy.prefersFilesystemIsolation());
96 try std.testing.expect(policy.prefersProcessIsolation());
97 try std.testing.expect(!policy.prefersNetworkIsolation());
98 }