@landstrip/landstrip-api 0.19.12 → 0.19.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/lib/index.d.ts +4 -1
- package/lib/shared.d.ts +7 -2
- package/lib/shared.js +138 -21
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -40,6 +40,15 @@ Descriptor lifetimes and the reply protocol are covered in the manual.
|
|
|
40
40
|
Integration helpers are exported through [`/shared`](lib/shared.d.ts) and
|
|
41
41
|
[`/proxy`](lib/proxy.d.ts).
|
|
42
42
|
|
|
43
|
+
`decodeLandstripTrap(value)` and `parseTrapLine(line)` from `/shared` return a
|
|
44
|
+
validated `LandstripTrap`, or `null` for malformed input. Extra fields are
|
|
45
|
+
preserved. Missing or unknown denial states are normalized to `info`, never
|
|
46
|
+
queries; query IDs must be decimal `u64` strings and nonzero for queries.
|
|
47
|
+
`isQueryTrap(trap)` narrows validated events to `LandstripQueryTrap` with
|
|
48
|
+
`state: 'query'`. The declarations include all five native mechanisms and the
|
|
49
|
+
current Windows error codes; the previously declared `POLICY_TCP_BIND_UNSUPPORTED`
|
|
50
|
+
code remains accepted alongside `POLICY_LOCAL_BINDING_UNSUPPORTED`.
|
|
51
|
+
|
|
43
52
|
`writeLandstripPolicyFile(policy, prefix = 'landstrip-')` from `/shared` writes
|
|
44
53
|
`policy.json` and returns `{ dir, path }`. The caller owns `dir` and must remove it
|
|
45
54
|
with `rmSync(dir, { recursive: true, force: true })` after use. Serialization
|
package/lib/index.d.ts
CHANGED
|
@@ -5,7 +5,8 @@ export type LandstripMechanism =
|
|
|
5
5
|
| 'landlock'
|
|
6
6
|
| 'seccomp'
|
|
7
7
|
| 'seatbelt'
|
|
8
|
-
| 'appcontainer'
|
|
8
|
+
| 'appcontainer'
|
|
9
|
+
| 'windowsuser';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* `query` holds the syscall until the launcher answers with a
|
|
@@ -86,6 +87,7 @@ export type LandstripPolicyErrorCode =
|
|
|
86
87
|
| 'POLICY_DENY_READ_ALWAYS_UNSUPPORTED'
|
|
87
88
|
| 'POLICY_DENY_WRITE_ALWAYS_UNSUPPORTED'
|
|
88
89
|
| 'POLICY_TCP_BIND_UNSUPPORTED'
|
|
90
|
+
| 'POLICY_LOCAL_BINDING_UNSUPPORTED'
|
|
89
91
|
| 'POLICY_UNIX_SOCKET_UNSUPPORTED'
|
|
90
92
|
| 'POLICY_UNIX_SOCKET_PATH'
|
|
91
93
|
| 'POLICY_DENY_WRITE_SYMLINK_ANCESTOR'
|
|
@@ -99,6 +101,7 @@ export type LandstripInternalCode =
|
|
|
99
101
|
| LandstripPolicyErrorCode
|
|
100
102
|
| 'SANDBOX_SETUP_FAILED'
|
|
101
103
|
| 'SUPERVISE_FAILED'
|
|
104
|
+
| 'HOST_JOB_INCOMPATIBLE'
|
|
102
105
|
| 'PLATFORM_UNSUPPORTED'
|
|
103
106
|
| 'INTEGER_TOO_LARGE'
|
|
104
107
|
| 'INTERNAL_ERROR';
|
package/lib/shared.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
} from './index.js';
|
|
25
25
|
|
|
26
26
|
export type LandstripDenialTrap = LandstripFilesystemTrap | LandstripNetworkTrap;
|
|
27
|
+
export type LandstripQueryTrap = LandstripDenialTrap & { state: 'query' };
|
|
27
28
|
|
|
28
29
|
export type {
|
|
29
30
|
SandboxConfig,
|
|
@@ -78,7 +79,7 @@ export function isFilesystemTrap(trap: LandstripTrap): trap is LandstripFilesyst
|
|
|
78
79
|
|
|
79
80
|
export function isDenialTrap(trap: LandstripTrap): trap is LandstripDenialTrap;
|
|
80
81
|
|
|
81
|
-
export function isQueryTrap(trap: LandstripTrap): trap is
|
|
82
|
+
export function isQueryTrap(trap: LandstripTrap): trap is LandstripQueryTrap;
|
|
82
83
|
|
|
83
84
|
export function formatLandstripTrap(trap: LandstripTrap): string;
|
|
84
85
|
|
|
@@ -222,7 +223,11 @@ export function extractCandidatePaths(command: string): string[];
|
|
|
222
223
|
|
|
223
224
|
export function extractNativeDeniedPath(output: string, cwd?: string): string | null;
|
|
224
225
|
|
|
225
|
-
export function extractNativeWriteDeniedPath(
|
|
226
|
+
export function extractNativeWriteDeniedPath(
|
|
227
|
+
output: string,
|
|
228
|
+
cwd?: string,
|
|
229
|
+
command?: string,
|
|
230
|
+
): string | null;
|
|
226
231
|
|
|
227
232
|
export interface ExtractDeniedPathOptions {
|
|
228
233
|
cwd?: string;
|
package/lib/shared.js
CHANGED
|
@@ -122,51 +122,115 @@ function allowsAllDomains(allowedDomains) {
|
|
|
122
122
|
return allowedDomains.includes('*');
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
125
|
+
const LANDSTRIP_MECHANISMS = new Set([
|
|
126
|
+
'landlock',
|
|
127
|
+
'seccomp',
|
|
128
|
+
'seatbelt',
|
|
129
|
+
'appcontainer',
|
|
130
|
+
'windowsuser',
|
|
131
|
+
]);
|
|
132
|
+
const LANDSTRIP_INTERNAL_CODES = new Set([
|
|
133
|
+
'POLICY_PARSE_FAILED',
|
|
134
|
+
'POLICY_IO_FAILED',
|
|
135
|
+
'POLICY_UNRESTRICTED_READ',
|
|
136
|
+
'POLICY_DENY_READ_ALWAYS_UNSUPPORTED',
|
|
137
|
+
'POLICY_DENY_WRITE_ALWAYS_UNSUPPORTED',
|
|
138
|
+
'POLICY_TCP_BIND_UNSUPPORTED',
|
|
139
|
+
'POLICY_LOCAL_BINDING_UNSUPPORTED',
|
|
140
|
+
'POLICY_UNIX_SOCKET_UNSUPPORTED',
|
|
141
|
+
'POLICY_UNIX_SOCKET_PATH',
|
|
142
|
+
'POLICY_DENY_WRITE_SYMLINK_ANCESTOR',
|
|
143
|
+
'POLICY_INVALID_PORT',
|
|
144
|
+
'POLICY_EMPTY_PATH',
|
|
145
|
+
'POLICY_HOME_UNAVAILABLE',
|
|
146
|
+
'POLICY_TRAVERSAL_DEPTH',
|
|
147
|
+
'SUPERVISE_FAILED',
|
|
148
|
+
'HOST_JOB_INCOMPATIBLE',
|
|
149
|
+
'PLATFORM_UNSUPPORTED',
|
|
150
|
+
'INTEGER_TOO_LARGE',
|
|
151
|
+
'INTERNAL_ERROR',
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
function isDenialRecord(value) {
|
|
155
|
+
return (
|
|
156
|
+
typeof value.query_id === 'string' &&
|
|
157
|
+
/^(0|[1-9][0-9]{0,19})$/.test(value.query_id) &&
|
|
158
|
+
BigInt(value.query_id) <= 18446744073709551615n &&
|
|
159
|
+
(value.state !== 'query' || value.query_id !== '0') &&
|
|
160
|
+
typeof value.syscall === 'string' &&
|
|
161
|
+
typeof value.errno === 'string' &&
|
|
162
|
+
value.mechanism === 'seccomp' &&
|
|
163
|
+
isRecord(value.process) &&
|
|
164
|
+
Number.isInteger(value.process.pid) &&
|
|
165
|
+
value.process.pid >= 0 &&
|
|
166
|
+
value.process.pid <= 4294967295 &&
|
|
167
|
+
(value.process.exe === null || typeof value.process.exe === 'string') &&
|
|
168
|
+
(value.process.cwd === null || typeof value.process.cwd === 'string')
|
|
169
|
+
);
|
|
170
|
+
}
|
|
134
171
|
|
|
135
172
|
function isLandstripTrap(value) {
|
|
136
173
|
if (!isRecord(value)) return false;
|
|
137
174
|
|
|
138
175
|
switch (value.kind) {
|
|
139
|
-
case 'filesystem':
|
|
176
|
+
case 'filesystem': {
|
|
177
|
+
if (!Array.isArray(value.flags)) return false;
|
|
178
|
+
for (const flag of value.flags) {
|
|
179
|
+
if (typeof flag !== 'string') return false;
|
|
180
|
+
}
|
|
181
|
+
const grant = value.operation === 'read' ? 'allowRead' : 'allowWrite';
|
|
182
|
+
const otherGrant = value.operation === 'read' ? 'allowWrite' : 'allowRead';
|
|
140
183
|
return (
|
|
141
|
-
|
|
184
|
+
value.code === 'FILESYSTEM_DENIED' &&
|
|
185
|
+
(value.operation === 'read' || value.operation === 'write') &&
|
|
142
186
|
typeof value.path === 'string' &&
|
|
143
|
-
typeof value.
|
|
187
|
+
typeof value.requested_path === 'string' &&
|
|
188
|
+
(value.reason === 'allow_miss' || value.reason === 'deny_match') &&
|
|
189
|
+
isRecord(value.suggested_grant) &&
|
|
190
|
+
typeof value.suggested_grant[grant] === 'string' &&
|
|
191
|
+
value.suggested_grant[otherGrant] === undefined &&
|
|
192
|
+
isDenialRecord(value)
|
|
144
193
|
);
|
|
194
|
+
}
|
|
145
195
|
case 'network':
|
|
146
196
|
return (
|
|
147
|
-
|
|
197
|
+
value.code === 'NETWORK_DENIED' &&
|
|
198
|
+
(value.operation === 'connect' || value.operation === 'bind') &&
|
|
148
199
|
typeof value.target === 'string' &&
|
|
149
|
-
|
|
200
|
+
isDenialRecord(value)
|
|
150
201
|
);
|
|
151
202
|
case 'launch':
|
|
152
|
-
return
|
|
203
|
+
return (
|
|
204
|
+
value.code === 'LAUNCH_FAILED' &&
|
|
205
|
+
typeof value.program === 'string' &&
|
|
206
|
+
typeof value.message === 'string' &&
|
|
207
|
+
(value.errno === undefined || typeof value.errno === 'string')
|
|
208
|
+
);
|
|
153
209
|
case 'usage':
|
|
154
|
-
return typeof value.message === 'string';
|
|
210
|
+
return value.code === 'USAGE_ERROR' && typeof value.message === 'string';
|
|
155
211
|
case 'internal':
|
|
156
|
-
return
|
|
212
|
+
return (
|
|
213
|
+
typeof value.message === 'string' &&
|
|
214
|
+
(value.code === 'SANDBOX_SETUP_FAILED'
|
|
215
|
+
? LANDSTRIP_MECHANISMS.has(value.mechanism)
|
|
216
|
+
: LANDSTRIP_INTERNAL_CODES.has(value.code) && value.mechanism === undefined)
|
|
217
|
+
);
|
|
157
218
|
default:
|
|
158
219
|
return false;
|
|
159
220
|
}
|
|
160
221
|
}
|
|
161
222
|
|
|
162
223
|
function decodeLandstripTrap(value) {
|
|
163
|
-
|
|
224
|
+
if (!isLandstripTrap(value)) return null;
|
|
225
|
+
if (isDenialTrap(value) && value.state !== 'query' && value.state !== 'info') {
|
|
226
|
+
return { ...value, state: 'info' };
|
|
227
|
+
}
|
|
228
|
+
return value;
|
|
164
229
|
}
|
|
165
230
|
|
|
166
231
|
function parseTrapLine(line) {
|
|
167
232
|
try {
|
|
168
|
-
|
|
169
|
-
return isLandstripTrap(parsed) ? parsed : null;
|
|
233
|
+
return decodeLandstripTrap(JSON.parse(line));
|
|
170
234
|
} catch {
|
|
171
235
|
return null;
|
|
172
236
|
}
|
|
@@ -928,7 +992,37 @@ function extractNativeDeniedPath(output, cwd = process.cwd()) {
|
|
|
928
992
|
return null;
|
|
929
993
|
}
|
|
930
994
|
|
|
931
|
-
|
|
995
|
+
const nativeWriteTools = new Set(['mkdir', 'rm', 'rmdir', 'tee', 'touch', 'truncate', 'unlink']);
|
|
996
|
+
const nativeShellTools = new Set(['bash', 'dash', 'fish', 'ksh', 'sh', 'zsh']);
|
|
997
|
+
const nativeWriteSyscalls = new Set([
|
|
998
|
+
'chmod',
|
|
999
|
+
'chown',
|
|
1000
|
+
'copyfile',
|
|
1001
|
+
'link',
|
|
1002
|
+
'mkdir',
|
|
1003
|
+
'mkdtemp',
|
|
1004
|
+
'rename',
|
|
1005
|
+
'rmdir',
|
|
1006
|
+
'symlink',
|
|
1007
|
+
'truncate',
|
|
1008
|
+
'unlink',
|
|
1009
|
+
'utimes',
|
|
1010
|
+
'write',
|
|
1011
|
+
]);
|
|
1012
|
+
|
|
1013
|
+
function commandRedirectsToPath(command, path, cwd) {
|
|
1014
|
+
if (typeof command !== 'string') return false;
|
|
1015
|
+
const redirects = command.matchAll(
|
|
1016
|
+
/(?:[0-9]*>>?|&>>?|>\|)\s*(?:'([^'\r\n]+)'|"([^"\r\n]+)"|\x60([^\x60\r\n]+)\x60|([^\s'"\x60;&|<>]+))/g,
|
|
1017
|
+
);
|
|
1018
|
+
for (const redirect of redirects) {
|
|
1019
|
+
const target = redirect[1] ?? redirect[2] ?? redirect[3] ?? redirect[4];
|
|
1020
|
+
if (target !== undefined && normalizePathMatch(target, cwd) === path) return true;
|
|
1021
|
+
}
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function extractNativeWriteDeniedPath(output, cwd = process.cwd(), command) {
|
|
932
1026
|
if (typeof output !== 'string') return null;
|
|
933
1027
|
const denial = String.raw`(?:Access is denied\.?|Operation not permitted|Permission denied)`;
|
|
934
1028
|
let match = output.match(
|
|
@@ -956,6 +1050,29 @@ function extractNativeWriteDeniedPath(output, cwd = process.cwd()) {
|
|
|
956
1050
|
);
|
|
957
1051
|
if (match) return normalizePathMatch(match[1], cwd);
|
|
958
1052
|
|
|
1053
|
+
match = output.match(
|
|
1054
|
+
new RegExp(
|
|
1055
|
+
String.raw`^([^\s:]+):\s+((?:[a-zA-Z]:[\\/]|\\\\|\/|\.{1,2}[\\/])[^:\r\n]*?):\s+${denial}$`,
|
|
1056
|
+
'im',
|
|
1057
|
+
),
|
|
1058
|
+
);
|
|
1059
|
+
if (match) {
|
|
1060
|
+
const tool = basename(match[1]);
|
|
1061
|
+
const path = normalizePathMatch(match[2], cwd);
|
|
1062
|
+
if (path && nativeWriteTools.has(tool)) return path;
|
|
1063
|
+
if (path && nativeShellTools.has(tool) && commandRedirectsToPath(command, path, cwd)) return path;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
match = output.match(
|
|
1067
|
+
new RegExp(
|
|
1068
|
+
String.raw`(?:EPERM|EACCES):\s*(?:operation not permitted|permission denied),\s*([a-z]+)\s+['"\x60]([^'"\x60\r\n]+)['"\x60]`,
|
|
1069
|
+
'i',
|
|
1070
|
+
),
|
|
1071
|
+
);
|
|
1072
|
+
if (match && nativeWriteSyscalls.has(match[1].toLowerCase())) {
|
|
1073
|
+
return normalizePathMatch(match[2], cwd);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
959
1076
|
return null;
|
|
960
1077
|
}
|
|
961
1078
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@landstrip/landstrip-api",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.13",
|
|
4
4
|
"description": "Sandbox runner using Landlock, Seatbelt, and AppContainer",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/landstrip/landstrip#readme",
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
"ipaddr.js": "^2.5.0"
|
|
41
41
|
},
|
|
42
42
|
"optionalDependencies": {
|
|
43
|
-
"@landstrip/landstrip-darwin-arm64": "0.19.
|
|
44
|
-
"@landstrip/landstrip-darwin-x64": "0.19.
|
|
45
|
-
"@landstrip/landstrip-linux-x64": "0.19.
|
|
46
|
-
"@landstrip/landstrip-linux-arm64": "0.19.
|
|
47
|
-
"@landstrip/landstrip-win32-x64": "0.19.
|
|
48
|
-
"@landstrip/landstrip-win32-arm64": "0.19.
|
|
43
|
+
"@landstrip/landstrip-darwin-arm64": "0.19.13",
|
|
44
|
+
"@landstrip/landstrip-darwin-x64": "0.19.13",
|
|
45
|
+
"@landstrip/landstrip-linux-x64": "0.19.13",
|
|
46
|
+
"@landstrip/landstrip-linux-arm64": "0.19.13",
|
|
47
|
+
"@landstrip/landstrip-win32-x64": "0.19.13",
|
|
48
|
+
"@landstrip/landstrip-win32-arm64": "0.19.13"
|
|
49
49
|
},
|
|
50
50
|
"publishConfig": {
|
|
51
51
|
"access": "public"
|