spectrum-cli 5.0.2 → 6.0.0

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.
@@ -0,0 +1,418 @@
1
+ const io = require("../automation");
2
+ const { isDryRun, reportPreflight } = require("../cli/command-executor");
3
+ const {
4
+ required,
5
+ credentialPair,
6
+ PreflightError,
7
+ } = require("../preflight/automation");
8
+ const {
9
+ fs,
10
+ path,
11
+ ensure,
12
+ sha,
13
+ json,
14
+ save,
15
+ temporary,
16
+ DIGEST,
17
+ SEMVER,
18
+ safePath,
19
+ exec,
20
+ } = io;
21
+ const {
22
+ GENERATION,
23
+ OCI_TYPE,
24
+ MAX_PAYLOAD,
25
+ canonicalRepository,
26
+ validateCatalog,
27
+ validateManifest,
28
+ encoded,
29
+ } = require("./schema");
30
+ const tools = require("./tools");
31
+ class MissingCatalog extends Error {}
32
+ function authenticated(command, env) {
33
+ return command[0] === "oras" && env.DOCKER_CONFIG
34
+ ? [
35
+ ...command,
36
+ "--registry-config",
37
+ path.join(env.DOCKER_CONFIG, "config.json"),
38
+ ]
39
+ : command;
40
+ }
41
+ function run(
42
+ command,
43
+ env,
44
+ { missing = false, timeout = 300000, binary = false } = {},
45
+ ) {
46
+ const result = exec(authenticated(command, env), {
47
+ env,
48
+ allowed: [0, 1, 2],
49
+ timeout,
50
+ binary,
51
+ maxBuffer: 32 * 1024 * 1024,
52
+ label: "Registry/scanner command",
53
+ });
54
+ if (result.status) {
55
+ const message = String(result.stderr).toLowerCase();
56
+ if (
57
+ missing &&
58
+ [
59
+ "manifest_unknown",
60
+ "manifest unknown",
61
+ "not found: not found",
62
+ "response status code 404",
63
+ ].some((marker) => message.includes(marker))
64
+ )
65
+ throw new MissingCatalog("Catalog not published");
66
+ throw new Error("Registry/scanner command failed");
67
+ }
68
+ return result.stdout;
69
+ }
70
+ function sourceDigests(sbom) {
71
+ const source = sbom.source || {},
72
+ target = source.metadata || {};
73
+ if (source.type !== "image" || !Array.isArray(target.repoDigests || []))
74
+ return new Set();
75
+ return new Set(
76
+ [target.manifestDigest || "", ...(target.repoDigests || [])]
77
+ .filter((v) => typeof v === "string")
78
+ .map((v) => v.split("@").at(-1))
79
+ .filter((v) => DIGEST.test(v)),
80
+ );
81
+ }
82
+ function discover(sbom) {
83
+ const found = new Map();
84
+ for (const pkg of sbom.artifacts || []) {
85
+ if (pkg.name !== "next" || pkg.type !== "npm") continue;
86
+ ensure(
87
+ typeof pkg.version === "string" && SEMVER.test(pkg.version),
88
+ "Next has no exact valid version",
89
+ );
90
+ for (const location of pkg.locations || []) {
91
+ const name = location.path;
92
+ if (safePath(name, true) && name.endsWith("/next/package.json")) {
93
+ const root = name.slice(0, -"/package.json".length);
94
+ found.set(pkg.version + "\0" + root, {
95
+ name: "next",
96
+ version: pkg.version,
97
+ root,
98
+ });
99
+ }
100
+ }
101
+ }
102
+ return [...found.entries()]
103
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
104
+ .map(([, v]) => v);
105
+ }
106
+ function catalogRef(repository, version, env, command = run) {
107
+ const shard = repository + "/next";
108
+ const tag = GENERATION + "-" + sha(version);
109
+ const descriptor = json(
110
+ command(
111
+ ["oras", "manifest", "fetch", "--descriptor", shard + ":" + tag],
112
+ env,
113
+ { missing: true },
114
+ ),
115
+ );
116
+ ensure(
117
+ DIGEST.test(descriptor.digest) && descriptor.mediaType === OCI_TYPE,
118
+ "Invalid catalog manifest descriptor",
119
+ );
120
+ const reference = shard + "@" + descriptor.digest;
121
+ const bytes = Buffer.from(
122
+ command(["oras", "manifest", "fetch", reference], env, {
123
+ binary: true,
124
+ timeout: 60000,
125
+ }),
126
+ );
127
+ ensure(
128
+ bytes.length <= MAX_PAYLOAD && "sha256:" + sha(bytes) === descriptor.digest,
129
+ "Catalog manifest integrity failed",
130
+ );
131
+ const layer = validateManifest(json(bytes));
132
+ // stdout avoids any archive extraction or registry-controlled filename.
133
+ const body = Buffer.from(
134
+ command(
135
+ ["oras", "blob", "fetch", "--output", "-", shard + "@" + layer.digest],
136
+ env,
137
+ { binary: true, timeout: 60000 },
138
+ ),
139
+ );
140
+ ensure(
141
+ body.length === layer.size && "sha256:" + sha(body) === layer.digest,
142
+ "Catalog payload integrity failed",
143
+ );
144
+ validateCatalog(json(body), version);
145
+ return reference;
146
+ }
147
+ async function trigger(versions, env, transport = io.request) {
148
+ ensure(
149
+ versions.length <= 10 &&
150
+ versions.every(
151
+ (version) => typeof version === "string" && SEMVER.test(version),
152
+ ),
153
+ "At most 10 exact Next versions per request",
154
+ );
155
+ const api = (env.CI_API_V4_URL || "").replace(/\/(?![\s\S])/, ""),
156
+ server = (env.CI_SERVER_URL || "").replace(/\/(?![\s\S])/, "");
157
+ const parsed = new URL(server);
158
+ ensure(
159
+ env.CI_JOB_TOKEN &&
160
+ api === server + "/api/v4" &&
161
+ parsed.protocol === "https:" &&
162
+ !parsed.username &&
163
+ !parsed.password &&
164
+ parsed.pathname === "/",
165
+ "Missing trusted GitLab API configuration",
166
+ );
167
+ required(env, ["SBOM_CATALOG_UPDATER_PROJECT"]);
168
+ const body = JSON.stringify({
169
+ token: env.CI_JOB_TOKEN,
170
+ ref: "main",
171
+ inputs: {
172
+ operation: "populate-next-catalog",
173
+ next_versions: JSON.stringify(versions),
174
+ },
175
+ });
176
+ // One POST. A transport timeout must not create another downstream pipeline.
177
+ const res = await transport(
178
+ api +
179
+ "/projects/" +
180
+ encoded(env.SBOM_CATALOG_UPDATER_PROJECT) +
181
+ "/trigger/pipeline",
182
+ {
183
+ method: "POST",
184
+ body,
185
+ headers: { "Content-Type": "application/json" },
186
+ timeout: 30000,
187
+ limit: 1024 * 1024,
188
+ },
189
+ );
190
+ ensure(res.status >= 200 && res.status < 300, "Catalog request failed");
191
+ const url = json(res.body).web_url;
192
+ ensure(
193
+ typeof url === "string" && url.startsWith(server + "/"),
194
+ "Invalid downstream pipeline URL",
195
+ );
196
+ return url;
197
+ }
198
+ async function execute(env, directory, deps = {}, { dry = false } = {}) {
199
+ const selection = {
200
+ schemaVersion: 1,
201
+ status: "error",
202
+ image: {},
203
+ catalogs: [],
204
+ missing: [],
205
+ request: { status: "not_needed" },
206
+ },
207
+ errors = [];
208
+ const command = deps.run || run;
209
+ try {
210
+ if (
211
+ env.SBOM_CATALOG_CLIENT_ENABLED !== "true" ||
212
+ env.SBOM_CATALOG_FRAMEWORK !== "nextjs"
213
+ ) {
214
+ selection.status = "skipped";
215
+ return { selection, errors };
216
+ }
217
+ required(env, ["SBOM_CATALOG_REPOSITORY", "DOCKER_IMAGE", "APP_VERSION"]);
218
+ const repository = canonicalRepository(env.SBOM_CATALOG_REPOSITORY);
219
+ credentialPair(env, "SBOM_CATALOG_USER", "SBOM_CATALOG_PASSWORD");
220
+ await (deps.bootstrap || tools.bootstrap)(directory);
221
+ const runtime = {
222
+ ...env,
223
+ PATH: directory + path.delimiter + (env.PATH || ""),
224
+ SYFT_CHECK_FOR_APP_UPDATE: "false",
225
+ };
226
+ const auths = {};
227
+ for (const [registry, user, password] of [
228
+ [env.CI_REGISTRY, "gitlab-ci-token", env.CI_JOB_TOKEN],
229
+ [
230
+ repository.split("/")[0],
231
+ env.SBOM_CATALOG_USER,
232
+ env.SBOM_CATALOG_PASSWORD,
233
+ ],
234
+ ]) {
235
+ if (registry && user && password)
236
+ auths[registry] = {
237
+ auth: Buffer.from(user + ":" + password).toString("base64"),
238
+ };
239
+ }
240
+ fs.writeFileSync(
241
+ path.join(directory, "config.json"),
242
+ JSON.stringify({ auths }),
243
+ { mode: 0o600 },
244
+ );
245
+ runtime.DOCKER_CONFIG = directory;
246
+ const image = canonicalRepository(env.DOCKER_IMAGE || "");
247
+ ensure(
248
+ /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}(?![\s\S])/.test(
249
+ env.APP_VERSION || "",
250
+ ),
251
+ "Missing valid application image tag",
252
+ );
253
+ const descriptor = json(
254
+ command(
255
+ [
256
+ "oras",
257
+ "manifest",
258
+ "fetch",
259
+ "--descriptor",
260
+ image + ":" + env.APP_VERSION,
261
+ ],
262
+ runtime,
263
+ ),
264
+ );
265
+ ensure(DIGEST.test(descriptor.digest), "Invalid application image digest");
266
+ if (dry) {
267
+ required(env, [
268
+ "SBOM_CATALOG_UPDATER_PROJECT",
269
+ "CI_JOB_TOKEN",
270
+ "CI_API_V4_URL",
271
+ "CI_SERVER_URL",
272
+ ]);
273
+ const server = new URL(env.CI_SERVER_URL);
274
+ ensure(
275
+ server.protocol === "https:" &&
276
+ !server.username &&
277
+ !server.password &&
278
+ server.pathname === "/" &&
279
+ env.CI_API_V4_URL.replace(/\/$/, "") ===
280
+ env.CI_SERVER_URL.replace(/\/$/, "") + "/api/v4",
281
+ "Missing trusted GitLab API configuration",
282
+ );
283
+ reportPreflight(
284
+ "sbom catalog resolve",
285
+ "image-registry-access",
286
+ "проверены настройки каталога и чтение immutable образа",
287
+ );
288
+ selection.status = "ready";
289
+ return { selection, errors };
290
+ }
291
+ const reference = image + "@" + descriptor.digest;
292
+ selection.image = { reference, digest: descriptor.digest };
293
+ const sbom = json(
294
+ command(
295
+ [
296
+ "syft",
297
+ "scan",
298
+ "registry:" + reference,
299
+ "--platform",
300
+ "linux/amd64",
301
+ "-o",
302
+ "syft-json",
303
+ ],
304
+ runtime,
305
+ ),
306
+ );
307
+ ensure(
308
+ sourceDigests(sbom).has(descriptor.digest),
309
+ "Scanner source digest mismatch",
310
+ );
311
+ const owners = discover(sbom);
312
+ ensure(owners.length, "No installed Next package manifest found");
313
+ const resolved = new Map(),
314
+ missing = new Set();
315
+ for (const owner of owners) {
316
+ if (!resolved.has(owner.version)) {
317
+ try {
318
+ resolved.set(owner.version, {
319
+ ref: (deps.catalogRef || catalogRef)(
320
+ repository,
321
+ owner.version,
322
+ runtime,
323
+ command,
324
+ ),
325
+ });
326
+ } catch (error) {
327
+ if (error instanceof MissingCatalog) {
328
+ resolved.set(owner.version, { reason: "catalog not published" });
329
+ missing.add(owner.version);
330
+ } else {
331
+ resolved.set(owner.version, {
332
+ reason: "catalog lookup failed: " + error.name,
333
+ });
334
+ errors.push(resolved.get(owner.version).reason);
335
+ }
336
+ }
337
+ }
338
+ const result = resolved.get(owner.version);
339
+ if (result.ref) selection.catalogs.push({ ref: result.ref, owner });
340
+ else selection.missing.push({ ...owner, reason: result.reason });
341
+ }
342
+ if (missing.size) {
343
+ try {
344
+ selection.request = {
345
+ status: "submitted",
346
+ pipelineUrl: await (deps.trigger || trigger)(
347
+ [...missing].sort(),
348
+ env,
349
+ ),
350
+ };
351
+ } catch (error) {
352
+ selection.request = { status: "failed" };
353
+ errors.push(
354
+ "catalog request failed: " +
355
+ (error instanceof PreflightError ? error.message : error.name),
356
+ );
357
+ }
358
+ }
359
+ selection.status =
360
+ selection.missing.length || errors.length ? "partial" : "complete";
361
+ } catch (error) {
362
+ errors.push(
363
+ "catalog discovery failed: " +
364
+ (error instanceof PreflightError ? error.message : error.name),
365
+ );
366
+ }
367
+ return { selection, errors };
368
+ }
369
+ async function resolve(options = {}) {
370
+ const dry = options.dry ?? isDryRun();
371
+ const env = { ...process.env, ...(options.env || {}) },
372
+ prefix = env.SBOM_CATALOG_PREFIX ?? "site:";
373
+ ensure(
374
+ /^[A-Za-z0-9_.:-]*(?![\s\S])/.test(prefix) && ![".", ".."].includes(prefix),
375
+ "Invalid catalog artifact prefix",
376
+ );
377
+ const output = path.join(
378
+ options.cwd || process.cwd(),
379
+ "sbom-catalog",
380
+ prefix,
381
+ );
382
+ const { selection, errors } = await temporary("sbom-resolve-", (dir) =>
383
+ execute(env, dir, options.deps, { dry }),
384
+ );
385
+ if (dry) {
386
+ if (errors.length)
387
+ console.error("SBOM catalog preflight: " + errors.join("; "));
388
+ return ["ready", "skipped"].includes(selection.status) ? 0 : 2;
389
+ }
390
+ save(path.join(output, "selection.json"), selection);
391
+ save(path.join(output, "report.json"), { ...selection, errors });
392
+ const lines = [
393
+ "# SBOM catalog discovery",
394
+ "",
395
+ "Status: " + selection.status,
396
+ "",
397
+ "Available: " + selection.catalogs.length,
398
+ "Missing: " + selection.missing.length,
399
+ "Request: " + selection.request.status,
400
+ ];
401
+ if (selection.request.pipelineUrl)
402
+ lines.push("", selection.request.pipelineUrl);
403
+ lines.push(...errors.map((error) => "\n- " + error));
404
+ fs.writeFileSync(path.join(output, "report.md"), lines.join("\n") + "\n");
405
+ console.log("SBOM catalog discovery: " + selection.status);
406
+ return ["complete", "skipped"].includes(selection.status) ? 0 : 2;
407
+ }
408
+ module.exports = {
409
+ MissingCatalog,
410
+ authenticated,
411
+ run,
412
+ sourceDigests,
413
+ discover,
414
+ catalogRef,
415
+ trigger,
416
+ execute,
417
+ resolve,
418
+ };
@@ -0,0 +1,170 @@
1
+ const {
2
+ ensure,
3
+ safePath,
4
+ SEMVER,
5
+ HEX64,
6
+ COMMIT,
7
+ DIGEST,
8
+ } = require("../automation");
9
+ const GENERATION = "r2";
10
+ const CONFIG_TYPE = "application/vnd.spectrum.sbom-catalog.v1+json";
11
+ const OCI_TYPE = "application/vnd.oci.image.manifest.v1+json";
12
+ const MAX_PAYLOAD = 5 * 1024 * 1024;
13
+ const encoded = (value) =>
14
+ encodeURIComponent(value).replace(
15
+ /[!'()*]/g,
16
+ (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(),
17
+ );
18
+ const purl = (name, version) =>
19
+ "pkg:npm/" + name.split("/").map(encoded).join("/") + "@" + encoded(version);
20
+ function canonicalRepository(value) {
21
+ ensure(
22
+ typeof value === "string" &&
23
+ /^[a-z0-9][a-z0-9.-]*(?::[0-9]+)?\/[a-z0-9][a-z0-9._/-]*(?![\s\S])/.test(
24
+ value,
25
+ ) &&
26
+ value
27
+ .split("/")
28
+ .slice(1)
29
+ .every((p) => p && p !== "." && p !== ".."),
30
+ "Invalid repository",
31
+ );
32
+ return value;
33
+ }
34
+ function validateCatalog(payload, version) {
35
+ ensure(
36
+ payload?.schemaVersion === 1 &&
37
+ payload.kind === "vendored-package-catalog" &&
38
+ payload.generation === GENERATION,
39
+ "Unsupported catalog",
40
+ );
41
+ const owner = payload.owner || {};
42
+ ensure(
43
+ owner.name === "next" &&
44
+ owner.version === version &&
45
+ owner.purl === purl("next", version),
46
+ "Catalog owner mismatch",
47
+ );
48
+ ensure(
49
+ typeof owner.manifestSha256 === "string" &&
50
+ HEX64.test(owner.manifestSha256) &&
51
+ typeof owner.sourceCommit === "string" &&
52
+ COMMIT.test(owner.sourceCommit),
53
+ "Missing catalog provenance",
54
+ );
55
+ ensure(
56
+ typeof owner.integrity === "string" &&
57
+ /^sha512-[A-Za-z0-9+/]+={0,2}(?![\s\S])/.test(owner.integrity) &&
58
+ Buffer.from(owner.integrity.slice(7), "base64").length === 64 &&
59
+ Buffer.from(owner.integrity.slice(7), "base64").toString("base64") ===
60
+ owner.integrity.slice(7),
61
+ "Invalid npm integrity",
62
+ );
63
+ ensure(
64
+ Array.isArray(payload.components) && Array.isArray(payload.unresolved),
65
+ "Invalid catalog entries",
66
+ );
67
+ const paths = [];
68
+ for (const component of payload.components) {
69
+ const name = component.path;
70
+ ensure(
71
+ safePath(name) &&
72
+ name.startsWith("dist/compiled/") &&
73
+ !paths.some(
74
+ (p) =>
75
+ p === name || p.startsWith(name + "/") || name.startsWith(p + "/"),
76
+ ),
77
+ "Invalid component path",
78
+ );
79
+ paths.push(name);
80
+ ensure(
81
+ typeof component.version === "string" &&
82
+ SEMVER.test(component.version) &&
83
+ typeof component.name === "string" &&
84
+ component.name &&
85
+ component.evidence,
86
+ "Invalid catalog component",
87
+ );
88
+ const evidence = component.evidence;
89
+ ensure(
90
+ component.purl === purl(component.name, component.version) &&
91
+ typeof evidence.commit === "string" &&
92
+ COMMIT.test(evidence.commit) &&
93
+ safePath(evidence.sourcePath) &&
94
+ safePath(evidence.lockfile) &&
95
+ typeof evidence.resolution === "string" &&
96
+ evidence.resolution,
97
+ "Invalid component provenance",
98
+ );
99
+ const files = component.files;
100
+ ensure(
101
+ files &&
102
+ typeof files === "object" &&
103
+ !Array.isArray(files) &&
104
+ Object.hasOwn(files, "package.json") &&
105
+ Object.keys(files).some((p) =>
106
+ /\.(js|mjs|cjs|wasm|node)(?![\s\S])/.test(p),
107
+ ),
108
+ "Incomplete file inventory",
109
+ );
110
+ for (const [name, hash] of Object.entries(files))
111
+ ensure(
112
+ safePath(name) && typeof hash === "string" && HEX64.test(hash),
113
+ "Unsafe catalog file",
114
+ );
115
+ }
116
+ for (const entry of payload.unresolved)
117
+ ensure(
118
+ entry &&
119
+ safePath(entry.path) &&
120
+ typeof entry.reason === "string" &&
121
+ entry.reason,
122
+ "Invalid unresolved catalog entry",
123
+ );
124
+ return payload;
125
+ }
126
+ function validateManifest(manifest) {
127
+ ensure(
128
+ manifest?.schemaVersion === 2 &&
129
+ manifest.mediaType === OCI_TYPE &&
130
+ manifest.config?.mediaType === CONFIG_TYPE,
131
+ "Unsupported OCI catalog manifest",
132
+ );
133
+ const config = manifest.config;
134
+ ensure(
135
+ typeof config.digest === "string" &&
136
+ DIGEST.test(config.digest) &&
137
+ Number.isInteger(config.size) &&
138
+ config.size >= 0 &&
139
+ config.size <= MAX_PAYLOAD,
140
+ "Invalid catalog config descriptor",
141
+ );
142
+ ensure(
143
+ Array.isArray(manifest.layers) && manifest.layers.length === 1,
144
+ "Catalog must contain one JSON layer",
145
+ );
146
+ const layer = manifest.layers[0];
147
+ ensure(
148
+ layer.mediaType === "application/json" &&
149
+ layer.annotations?.["org.opencontainers.image.title"] ===
150
+ "catalog.json" &&
151
+ typeof layer.digest === "string" &&
152
+ DIGEST.test(layer.digest) &&
153
+ Number.isInteger(layer.size) &&
154
+ layer.size > 0 &&
155
+ layer.size <= MAX_PAYLOAD,
156
+ "Unsafe catalog layer",
157
+ );
158
+ return layer;
159
+ }
160
+ module.exports = {
161
+ GENERATION,
162
+ CONFIG_TYPE,
163
+ OCI_TYPE,
164
+ MAX_PAYLOAD,
165
+ encoded,
166
+ purl,
167
+ canonicalRepository,
168
+ validateCatalog,
169
+ validateManifest,
170
+ };
@@ -0,0 +1,74 @@
1
+ const {
2
+ fs,
3
+ path,
4
+ ensure,
5
+ sha,
6
+ request,
7
+ tarEntries,
8
+ sleep,
9
+ } = require("../automation");
10
+ const TOOLS = Object.freeze({
11
+ oras: [
12
+ "https://github.com/oras-project/oras/releases/download/v1.3.0/oras_1.3.0_linux_amd64.tar.gz",
13
+ "6cdc692f929100feb08aa8de584d02f7bcc30ec7d88bc2adc2054d782db57c64",
14
+ ],
15
+ syft: [
16
+ "https://github.com/anchore/syft/releases/download/v1.44.0/syft_1.44.0_linux_amd64.tar.gz",
17
+ "0e91737aee2b5baf1d255b959630194a302335d848ff97bb07921eb6205b5f5a",
18
+ ],
19
+ });
20
+ async function toolDownload(url, transport = request) {
21
+ const deadline = Date.now() + 600000;
22
+ for (let attempt = 0; attempt < 4; attempt++) {
23
+ try {
24
+ let current = url;
25
+ for (let hop = 0; hop < 6; hop++) {
26
+ const parsed = new URL(current);
27
+ ensure(
28
+ parsed.protocol === "https:" && !parsed.username && !parsed.password,
29
+ "Unsafe tool URL",
30
+ );
31
+ const res = await transport(current, {
32
+ limit: 100 * 1024 * 1024,
33
+ timeout: Math.min(180000, deadline - Date.now()),
34
+ });
35
+ if (
36
+ [301, 302, 303, 307, 308].includes(res.status) &&
37
+ res.headers.get("location")
38
+ ) {
39
+ current = new URL(res.headers.get("location"), current).href;
40
+ continue;
41
+ }
42
+ ensure(res.status === 200, "Tool download failed");
43
+ return res.body;
44
+ }
45
+ throw new Error("Too many tool redirects");
46
+ } catch (error) {
47
+ if (attempt === 3 || Date.now() + 5000 >= deadline) throw error;
48
+ await sleep(5000);
49
+ }
50
+ }
51
+ }
52
+ async function bootstrap(directory, names = ["oras", "syft"], transport) {
53
+ ensure(
54
+ process.platform === "linux" && process.arch === "x64",
55
+ "Catalog runtime requires linux/amd64",
56
+ );
57
+ for (const name of names) {
58
+ const [url, checksum] = TOOLS[name];
59
+ const archive = await toolDownload(url, transport);
60
+ ensure(sha(archive) === checksum, "Tool checksum mismatch: " + name);
61
+ const entries = tarEntries(archive, {
62
+ maxFile: 200 * 1024 * 1024,
63
+ maxEntries: 100,
64
+ });
65
+ const binaries = entries.filter(
66
+ (entry) => entry.name === name && !entry.directory,
67
+ );
68
+ ensure(binaries.length === 1, "Invalid tool archive");
69
+ fs.writeFileSync(path.join(directory, name), binaries[0].data, {
70
+ mode: 0o700,
71
+ });
72
+ }
73
+ }
74
+ module.exports = { TOOLS, toolDownload, bootstrap };