@namzu/sandbox 2.0.2 → 2.0.3

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/package.json +12 -2
  3. package/dist/backends/aci-standby-pool/__tests__/unenforceable-controls.test.d.ts +0 -2
  4. package/dist/backends/aci-standby-pool/__tests__/unenforceable-controls.test.d.ts.map +0 -1
  5. package/dist/backends/aci-standby-pool/__tests__/unenforceable-controls.test.js +0 -61
  6. package/dist/backends/aci-standby-pool/__tests__/unenforceable-controls.test.js.map +0 -1
  7. package/dist/backends/docker/__tests__/hardening.test.d.ts +0 -2
  8. package/dist/backends/docker/__tests__/hardening.test.d.ts.map +0 -1
  9. package/dist/backends/docker/__tests__/hardening.test.js +0 -32
  10. package/dist/backends/docker/__tests__/hardening.test.js.map +0 -1
  11. package/dist/backends/docker/__tests__/leaf-permissions.smoke.test.d.ts +0 -40
  12. package/dist/backends/docker/__tests__/leaf-permissions.smoke.test.d.ts.map +0 -1
  13. package/dist/backends/docker/__tests__/leaf-permissions.smoke.test.js +0 -157
  14. package/dist/backends/docker/__tests__/leaf-permissions.smoke.test.js.map +0 -1
  15. package/dist/backends/firecracker/__tests__/agent-timeout-clamp.test.d.ts +0 -16
  16. package/dist/backends/firecracker/__tests__/agent-timeout-clamp.test.d.ts.map +0 -1
  17. package/dist/backends/firecracker/__tests__/agent-timeout-clamp.test.js +0 -37
  18. package/dist/backends/firecracker/__tests__/agent-timeout-clamp.test.js.map +0 -1
  19. package/dist/backends/firecracker/__tests__/backend.test.d.ts +0 -13
  20. package/dist/backends/firecracker/__tests__/backend.test.d.ts.map +0 -1
  21. package/dist/backends/firecracker/__tests__/backend.test.js +0 -361
  22. package/dist/backends/firecracker/__tests__/backend.test.js.map +0 -1
  23. package/dist/backends/firecracker/__tests__/control-plane-mtls.test.d.ts +0 -19
  24. package/dist/backends/firecracker/__tests__/control-plane-mtls.test.d.ts.map +0 -1
  25. package/dist/backends/firecracker/__tests__/control-plane-mtls.test.js +0 -209
  26. package/dist/backends/firecracker/__tests__/control-plane-mtls.test.js.map +0 -1
  27. package/dist/backends/firecracker/__tests__/egress-policy.test.d.ts +0 -2
  28. package/dist/backends/firecracker/__tests__/egress-policy.test.d.ts.map +0 -1
  29. package/dist/backends/firecracker/__tests__/egress-policy.test.js +0 -67
  30. package/dist/backends/firecracker/__tests__/egress-policy.test.js.map +0 -1
  31. package/dist/backends/firecracker/__tests__/fixtures/ipc-path.d.ts +0 -21
  32. package/dist/backends/firecracker/__tests__/fixtures/ipc-path.d.ts.map +0 -1
  33. package/dist/backends/firecracker/__tests__/fixtures/ipc-path.js +0 -30
  34. package/dist/backends/firecracker/__tests__/fixtures/ipc-path.js.map +0 -1
  35. package/dist/backends/firecracker/__tests__/fixtures/mtls-pki.d.ts +0 -39
  36. package/dist/backends/firecracker/__tests__/fixtures/mtls-pki.d.ts.map +0 -1
  37. package/dist/backends/firecracker/__tests__/fixtures/mtls-pki.js +0 -149
  38. package/dist/backends/firecracker/__tests__/fixtures/mtls-pki.js.map +0 -1
  39. package/dist/backends/firecracker/__tests__/protocol.test.d.ts +0 -6
  40. package/dist/backends/firecracker/__tests__/protocol.test.d.ts.map +0 -1
  41. package/dist/backends/firecracker/__tests__/protocol.test.js +0 -67
  42. package/dist/backends/firecracker/__tests__/protocol.test.js.map +0 -1
  43. package/dist/backends/firecracker/__tests__/transport.test.d.ts +0 -20
  44. package/dist/backends/firecracker/__tests__/transport.test.d.ts.map +0 -1
  45. package/dist/backends/firecracker/__tests__/transport.test.js +0 -457
  46. package/dist/backends/firecracker/__tests__/transport.test.js.map +0 -1
  47. package/dist/egress/__tests__/allowlist.test.d.ts +0 -2
  48. package/dist/egress/__tests__/allowlist.test.d.ts.map +0 -1
  49. package/dist/egress/__tests__/allowlist.test.js +0 -85
  50. package/dist/egress/__tests__/allowlist.test.js.map +0 -1
  51. package/dist/egress/__tests__/proxy.test.d.ts +0 -2
  52. package/dist/egress/__tests__/proxy.test.d.ts.map +0 -1
  53. package/dist/egress/__tests__/proxy.test.js +0 -177
  54. package/dist/egress/__tests__/proxy.test.js.map +0 -1
  55. package/dist/index.test.d.ts +0 -28
  56. package/dist/index.test.d.ts.map +0 -1
  57. package/dist/index.test.js +0 -649
  58. package/dist/index.test.js.map +0 -1
  59. package/src/backends/aci-standby-pool/__tests__/unenforceable-controls.test.ts +0 -69
  60. package/src/backends/docker/__tests__/hardening.test.ts +0 -43
  61. package/src/backends/docker/__tests__/leaf-permissions.smoke.test.ts +0 -169
  62. package/src/backends/firecracker/__tests__/agent-timeout-clamp.test.ts +0 -48
  63. package/src/backends/firecracker/__tests__/backend.test.ts +0 -429
  64. package/src/backends/firecracker/__tests__/control-plane-mtls.test.ts +0 -261
  65. package/src/backends/firecracker/__tests__/egress-policy.test.ts +0 -91
  66. package/src/backends/firecracker/__tests__/fixtures/ipc-path.ts +0 -31
  67. package/src/backends/firecracker/__tests__/fixtures/mtls-pki.ts +0 -166
  68. package/src/backends/firecracker/__tests__/protocol.test.ts +0 -75
  69. package/src/backends/firecracker/__tests__/transport.test.ts +0 -534
  70. package/src/egress/__tests__/allowlist.test.ts +0 -103
  71. package/src/egress/__tests__/proxy.test.ts +0 -212
  72. package/src/index.test.ts +0 -709
@@ -1,212 +0,0 @@
1
- import { createServer, request as nodeRequest } from 'node:http'
2
- import type { IncomingMessage, Server, ServerResponse } from 'node:http'
3
- import { afterEach, beforeEach, describe, expect, it } from 'vitest'
4
-
5
- import { EgressProxy } from '../proxy.js'
6
- import type { RunningEgressProxy } from '../proxy.js'
7
-
8
- /**
9
- * Until this existed, an egress policy could be declared and only two of
10
- * its four shapes could be honoured anywhere: the container backend
11
- * refused a host allowlist outright because it had no proxy to filter
12
- * through. `deny-all` and `allow-all` were the whole spectrum — all or
13
- * nothing.
14
- *
15
- * It also settles where credentials live. Any token the agent needed to
16
- * reach an allowed host had to be INSIDE the sandbox, in the environment,
17
- * readable by the untrusted code it is meant to be isolated from — via
18
- * `/proc/self/environ`, or via a prompt injection that exfiltrates it over
19
- * the very egress the policy permits.
20
- */
21
-
22
- /** A stand-in upstream that reports what it was sent. */
23
- function upstream(): Promise<{ server: Server; port: number; seen: IncomingMessage[] }> {
24
- const seen: IncomingMessage[] = []
25
- const server = createServer((req: IncomingMessage, res: ServerResponse) => {
26
- seen.push(req)
27
- res.writeHead(200, { 'content-type': 'text/plain' })
28
- res.end('upstream ok')
29
- })
30
- return new Promise((resolve) => {
31
- server.listen(0, '127.0.0.1', () => {
32
- const address = server.address()
33
- const port = typeof address === 'object' && address !== null ? address.port : 0
34
- resolve({ server, port, seen })
35
- })
36
- })
37
- }
38
-
39
- /**
40
- * Issue a request THROUGH the proxy, the way a proxied client does: the
41
- * absolute URL goes in the request line.
42
- *
43
- * `fetch` cannot express this — it owns the `Host` header and refuses an
44
- * override, so a request built with it names the proxy as its own target.
45
- * The first version of this helper did exactly that, and the proxy
46
- * forwarded to itself and hung. That hang is now a 400, and this helper
47
- * speaks the protocol properly.
48
- */
49
- function viaProxy(
50
- proxy: RunningEgressProxy,
51
- target: string,
52
- init: { headers?: Record<string, string> } = {},
53
- ): Promise<{ status: number; body: string }> {
54
- const proxyUrl = new URL(proxy.url)
55
- return new Promise((resolve, reject) => {
56
- const req = nodeRequest(
57
- {
58
- host: proxyUrl.hostname,
59
- port: Number(proxyUrl.port),
60
- method: 'GET',
61
- // Absolute-URI form: what distinguishes a proxied request.
62
- path: target,
63
- headers: init.headers ?? {},
64
- },
65
- (res) => {
66
- let body = ''
67
- res.setEncoding('utf-8')
68
- res.on('data', (chunk: string) => {
69
- body += chunk
70
- })
71
- res.on('end', () => resolve({ status: res.statusCode ?? 0, body }))
72
- },
73
- )
74
- req.on('error', reject)
75
- req.end()
76
- })
77
- }
78
-
79
- describe('the boundary', () => {
80
- let proxy: RunningEgressProxy
81
- let allowed: string[]
82
- let denied: Array<{ host: string; reason: string }>
83
- let target: Awaited<ReturnType<typeof upstream>>
84
-
85
- beforeEach(async () => {
86
- target = await upstream()
87
- allowed = ['127.0.0.1']
88
- denied = []
89
- proxy = await new EgressProxy({
90
- allowedHosts: async () => allowed,
91
- // The stand-in upstream speaks plain HTTP.
92
- upgradeToHttps: false,
93
- onDenied: (host, reason) => denied.push({ host, reason }),
94
- }).listen()
95
- })
96
-
97
- afterEach(async () => {
98
- await proxy.close()
99
- await new Promise<void>((resolve) => target.server.close(() => resolve()))
100
- })
101
-
102
- it('lets an allowed host through', async () => {
103
- const res = await viaProxy(proxy, `http://127.0.0.1:${target.port}/thing`)
104
- expect(res.status).toBe(200)
105
- expect(res.body).toBe('upstream ok')
106
- })
107
-
108
- it('refuses a host that is not on the list', async () => {
109
- allowed = ['example.com']
110
- const res = await viaProxy(proxy, `http://127.0.0.1:${target.port}/thing`)
111
-
112
- expect(res.status).toBe(403)
113
- // Never reached the upstream at all — the refusal is the point, not
114
- // the response code.
115
- expect(target.seen).toHaveLength(0)
116
- })
117
-
118
- it('names the host it refused', async () => {
119
- allowed = []
120
- const res = await viaProxy(proxy, `http://127.0.0.1:${target.port}/thing`)
121
-
122
- // An agent that cannot tell "not permitted" from "network down"
123
- // retries forever, and a human debugging it has nothing to go on.
124
- expect(res.body).toContain('127.0.0.1')
125
- expect(denied[0]?.host).toBe('127.0.0.1')
126
- })
127
-
128
- it('denies when the policy cannot be read', async () => {
129
- // An allowlist that fails open is not an allowlist.
130
- const failing = await new EgressProxy({
131
- allowedHosts: async () => {
132
- throw new Error('policy service unavailable')
133
- },
134
- upgradeToHttps: false,
135
- }).listen()
136
-
137
- try {
138
- const res = await viaProxy(failing, `http://127.0.0.1:${target.port}/thing`)
139
- expect(res.status).toBe(403)
140
- } finally {
141
- await failing.close()
142
- }
143
- })
144
-
145
- it('re-reads the policy per request, so a rotating allowlist is honoured', async () => {
146
- allowed = []
147
- expect((await viaProxy(proxy, `http://127.0.0.1:${target.port}/a`)).status).toBe(403)
148
-
149
- allowed = ['127.0.0.1']
150
- expect((await viaProxy(proxy, `http://127.0.0.1:${target.port}/b`)).status).toBe(200)
151
- })
152
-
153
- it('can be narrowed on a live proxy', async () => {
154
- // "Clone with a token, then drop to deny-all before running
155
- // untrusted build scripts" was not expressible at all: the policy
156
- // was frozen at provider construction.
157
- expect((await viaProxy(proxy, `http://127.0.0.1:${target.port}/a`)).status).toBe(200)
158
-
159
- proxy.setAllowedHosts(async () => [])
160
- expect((await viaProxy(proxy, `http://127.0.0.1:${target.port}/b`)).status).toBe(403)
161
- })
162
- })
163
-
164
- describe('brokering a credential', () => {
165
- let proxy: RunningEgressProxy
166
- let target: Awaited<ReturnType<typeof upstream>>
167
-
168
- beforeEach(async () => {
169
- target = await upstream()
170
- })
171
-
172
- afterEach(async () => {
173
- await proxy.close()
174
- await new Promise<void>((resolve) => target.server.close(() => resolve()))
175
- })
176
-
177
- const withCredentials = async (host: string) => {
178
- proxy = await new EgressProxy({
179
- allowedHosts: async () => ['127.0.0.1'],
180
- upgradeToHttps: false,
181
- credentials: [{ host, header: 'authorization', value: 'Bearer real-secret' }],
182
- }).listen()
183
- return proxy
184
- }
185
-
186
- it('stamps the real value on at the boundary', async () => {
187
- const p = await withCredentials('127.0.0.1')
188
- await viaProxy(p, `http://127.0.0.1:${target.port}/thing`)
189
-
190
- // The token never entered the sandbox — a placeholder did, and the
191
- // value was applied here.
192
- expect(target.seen[0]?.headers.authorization).toBe('Bearer real-secret')
193
- })
194
-
195
- it('sends it only to the host it belongs to', async () => {
196
- // A credential attached to every request is a credential handed to
197
- // whichever host the agent was talked into contacting.
198
- const p = await withCredentials('other.example.com')
199
- await viaProxy(p, `http://127.0.0.1:${target.port}/thing`)
200
-
201
- expect(target.seen[0]?.headers.authorization).toBeUndefined()
202
- })
203
-
204
- it('does not forward the hop-by-hop proxy header onward', async () => {
205
- const p = await withCredentials('127.0.0.1')
206
- await viaProxy(p, `http://127.0.0.1:${target.port}/thing`, {
207
- headers: { 'proxy-authorization': 'Basic should-not-travel' },
208
- })
209
-
210
- expect(target.seen[0]?.headers['proxy-authorization']).toBeUndefined()
211
- })
212
- })