@cyclonedx/cdxgen 8.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.
- package/.eslintrc.js +15 -0
- package/LICENSE +201 -0
- package/README.md +354 -0
- package/analyzer.js +189 -0
- package/bin/cdxgen +316 -0
- package/binary.js +507 -0
- package/docker.js +769 -0
- package/docker.test.js +72 -0
- package/index.js +4292 -0
- package/jest.config.js +181 -0
- package/known-licenses.json +27 -0
- package/lic-mapping.json +294 -0
- package/package.json +94 -0
- package/queries.json +68 -0
- package/server.js +110 -0
- package/spdx-licenses.json +500 -0
- package/utils.js +4284 -0
- package/utils.test.js +1660 -0
- package/vendor-alias.json +10 -0
package/docker.js
ADDED
|
@@ -0,0 +1,769 @@
|
|
|
1
|
+
const isWin = require("os").platform() === "win32";
|
|
2
|
+
const got = require("got");
|
|
3
|
+
const glob = require("glob");
|
|
4
|
+
const url = require("url");
|
|
5
|
+
const util = require("util");
|
|
6
|
+
const stream = require("stream");
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const os = require("os");
|
|
10
|
+
const tar = require("tar");
|
|
11
|
+
const { spawnSync } = require("child_process");
|
|
12
|
+
|
|
13
|
+
const pipeline = util.promisify(stream.pipeline);
|
|
14
|
+
|
|
15
|
+
let dockerConn = undefined;
|
|
16
|
+
let isPodman = false;
|
|
17
|
+
let isPodmanRootless = true;
|
|
18
|
+
let isDockerRootless = false;
|
|
19
|
+
const WIN_LOCAL_TLS = "http://localhost:2375";
|
|
20
|
+
let isWinLocalTLS = false;
|
|
21
|
+
|
|
22
|
+
// Debug mode flag
|
|
23
|
+
const DEBUG_MODE =
|
|
24
|
+
process.env.SCAN_DEBUG_MODE === "debug" ||
|
|
25
|
+
process.env.SHIFTLEFT_LOGGING_LEVEL === "debug";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Method to get all dirs matching a name
|
|
29
|
+
*
|
|
30
|
+
* @param {string} dirPath Root directory for search
|
|
31
|
+
* @param {string} dirName Directory name
|
|
32
|
+
*/
|
|
33
|
+
const getDirs = (dirPath, dirName, hidden = false, recurse = true) => {
|
|
34
|
+
try {
|
|
35
|
+
return glob.sync(recurse ? "**/" : "" + dirName, {
|
|
36
|
+
cwd: dirPath,
|
|
37
|
+
silent: true,
|
|
38
|
+
absolute: true,
|
|
39
|
+
nocase: true,
|
|
40
|
+
nodir: false,
|
|
41
|
+
follow: false,
|
|
42
|
+
dot: hidden
|
|
43
|
+
});
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
exports.getDirs = getDirs;
|
|
49
|
+
|
|
50
|
+
function flatten(lists) {
|
|
51
|
+
return lists.reduce((a, b) => a.concat(b), []);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getDirectories(srcpath) {
|
|
55
|
+
if (fs.existsSync(srcpath)) {
|
|
56
|
+
return fs
|
|
57
|
+
.readdirSync(srcpath)
|
|
58
|
+
.map((file) => path.join(srcpath, file))
|
|
59
|
+
.filter((path) => {
|
|
60
|
+
try {
|
|
61
|
+
return fs.statSync(path).isDirectory();
|
|
62
|
+
} catch (e) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const getOnlyDirs = (srcpath, dirName) => {
|
|
71
|
+
return [
|
|
72
|
+
srcpath,
|
|
73
|
+
...flatten(
|
|
74
|
+
getDirectories(srcpath)
|
|
75
|
+
.map((p) => {
|
|
76
|
+
try {
|
|
77
|
+
if (fs.existsSync(p)) {
|
|
78
|
+
if (fs.lstatSync(p).isDirectory()) {
|
|
79
|
+
return getOnlyDirs(p, dirName);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error(err);
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
.filter((p) => p !== undefined)
|
|
87
|
+
)
|
|
88
|
+
].filter((d) => d.endsWith(dirName));
|
|
89
|
+
};
|
|
90
|
+
exports.getOnlyDirs = getOnlyDirs;
|
|
91
|
+
|
|
92
|
+
const getDefaultOptions = () => {
|
|
93
|
+
let opts = {
|
|
94
|
+
throwHttpErrors: true,
|
|
95
|
+
"hooks.beforeError": [],
|
|
96
|
+
method: "GET",
|
|
97
|
+
isPodman
|
|
98
|
+
};
|
|
99
|
+
const userInfo = os.userInfo();
|
|
100
|
+
opts.podmanPrefixUrl = isWin ? "" : `unix:/run/podman/podman.sock:`;
|
|
101
|
+
opts.podmanRootlessPrefixUrl = isWin
|
|
102
|
+
? ""
|
|
103
|
+
: `unix:/run/user/${userInfo.uid}/podman/podman.sock:`;
|
|
104
|
+
if (!process.env.DOCKER_HOST) {
|
|
105
|
+
if (isPodman) {
|
|
106
|
+
opts.prefixUrl = isPodmanRootless
|
|
107
|
+
? opts.podmanRootlessPrefixUrl
|
|
108
|
+
: opts.podmanPrefixUrl;
|
|
109
|
+
} else {
|
|
110
|
+
if (isWinLocalTLS) {
|
|
111
|
+
opts.prefixUrl = WIN_LOCAL_TLS;
|
|
112
|
+
} else {
|
|
113
|
+
// Named pipes syntax for Windows doesn't work with got
|
|
114
|
+
// See: https://github.com/sindresorhus/got/issues/2178
|
|
115
|
+
/*
|
|
116
|
+
opts.prefixUrl = isWin
|
|
117
|
+
? "npipe//./pipe/docker_engine:"
|
|
118
|
+
: "unix:/var/run/docker.sock:";
|
|
119
|
+
*/
|
|
120
|
+
opts.prefixUrl = isWin ? WIN_LOCAL_TLS : "unix:/var/run/docker.sock:";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
let hostStr = process.env.DOCKER_HOST;
|
|
125
|
+
if (hostStr.startsWith("unix:///")) {
|
|
126
|
+
hostStr = hostStr.replace("unix:///", "unix:/");
|
|
127
|
+
if (hostStr.includes("docker.sock")) {
|
|
128
|
+
hostStr = hostStr.replace("docker.sock", "docker.sock:");
|
|
129
|
+
isDockerRootless = true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
opts.prefixUrl = hostStr;
|
|
133
|
+
if (process.env.DOCKER_CERT_PATH) {
|
|
134
|
+
opts.https = {
|
|
135
|
+
certificate: fs.readFileSync(
|
|
136
|
+
path.join(process.env.DOCKER_CERT_PATH, "cert.pem"),
|
|
137
|
+
"utf8"
|
|
138
|
+
),
|
|
139
|
+
key: fs.readFileSync(
|
|
140
|
+
path.join(process.env.DOCKER_CERT_PATH, "key.pem"),
|
|
141
|
+
"utf8"
|
|
142
|
+
)
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return opts;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const getConnection = async (options) => {
|
|
151
|
+
if (!dockerConn) {
|
|
152
|
+
const opts = Object.assign({}, getDefaultOptions(), options);
|
|
153
|
+
try {
|
|
154
|
+
await got.get("_ping", opts);
|
|
155
|
+
dockerConn = got.extend(opts);
|
|
156
|
+
if (DEBUG_MODE) {
|
|
157
|
+
if (isDockerRootless) {
|
|
158
|
+
console.log("Docker service in rootless mode detected!");
|
|
159
|
+
} else {
|
|
160
|
+
console.log("Docker service in root mode detected!");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch (err) {
|
|
164
|
+
// console.log(err, opts);
|
|
165
|
+
try {
|
|
166
|
+
if (isWin) {
|
|
167
|
+
opts.prefixUrl = WIN_LOCAL_TLS;
|
|
168
|
+
await got.get("_ping", opts);
|
|
169
|
+
dockerConn = got.extend(opts);
|
|
170
|
+
isWinLocalTLS = true;
|
|
171
|
+
if (DEBUG_MODE) {
|
|
172
|
+
console.log("Docker desktop on Windows detected!");
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
opts.prefixUrl = opts.podmanRootlessPrefixUrl;
|
|
176
|
+
await got.get("libpod/_ping", opts);
|
|
177
|
+
isPodman = true;
|
|
178
|
+
isPodmanRootless = true;
|
|
179
|
+
dockerConn = got.extend(opts);
|
|
180
|
+
if (DEBUG_MODE) {
|
|
181
|
+
console.log("Podman in rootless mode detected!");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
// console.log(err);
|
|
186
|
+
try {
|
|
187
|
+
opts.prefixUrl = opts.podmanPrefixUrl;
|
|
188
|
+
await got.get("libpod/_ping", opts);
|
|
189
|
+
isPodman = true;
|
|
190
|
+
isPodmanRootless = false;
|
|
191
|
+
dockerConn = got.extend(opts);
|
|
192
|
+
console.log("Podman in root mode detected!");
|
|
193
|
+
} catch (err) {
|
|
194
|
+
if (os.platform() === "win32") {
|
|
195
|
+
console.warn(
|
|
196
|
+
"Ensure Docker for Desktop is running as an administrator with 'Exposing daemon on TCP without TLS' setting turned on.",
|
|
197
|
+
opts
|
|
198
|
+
);
|
|
199
|
+
} else {
|
|
200
|
+
console.warn(
|
|
201
|
+
"Ensure docker/podman service or Docker for Desktop is running.",
|
|
202
|
+
opts
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return dockerConn;
|
|
210
|
+
};
|
|
211
|
+
exports.getConnection = getConnection;
|
|
212
|
+
|
|
213
|
+
const makeRequest = async (path, method = "GET") => {
|
|
214
|
+
let client = await getConnection();
|
|
215
|
+
if (!client) {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
const extraOptions = {
|
|
219
|
+
responseType: method === "GET" ? "json" : "text",
|
|
220
|
+
resolveBodyOnly: true,
|
|
221
|
+
method
|
|
222
|
+
};
|
|
223
|
+
const opts = Object.assign({}, getDefaultOptions(), extraOptions);
|
|
224
|
+
return await client(path, opts);
|
|
225
|
+
};
|
|
226
|
+
exports.makeRequest = makeRequest;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Parse image name
|
|
230
|
+
*
|
|
231
|
+
* docker pull debian
|
|
232
|
+
* docker pull debian:jessie
|
|
233
|
+
* docker pull ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2
|
|
234
|
+
* docker pull myregistry.local:5000/testing/test-image
|
|
235
|
+
*/
|
|
236
|
+
const parseImageName = (fullImageName) => {
|
|
237
|
+
const nameObj = {
|
|
238
|
+
registry: "",
|
|
239
|
+
repo: "",
|
|
240
|
+
tag: "",
|
|
241
|
+
digest: "",
|
|
242
|
+
platform: ""
|
|
243
|
+
};
|
|
244
|
+
if (!fullImageName) {
|
|
245
|
+
return nameObj;
|
|
246
|
+
}
|
|
247
|
+
// Extract registry name
|
|
248
|
+
if (
|
|
249
|
+
fullImageName.includes("/") &&
|
|
250
|
+
(fullImageName.includes(".") || fullImageName.includes(":"))
|
|
251
|
+
) {
|
|
252
|
+
const urlObj = url.parse(fullImageName);
|
|
253
|
+
const tmpA = fullImageName.split("/");
|
|
254
|
+
if (
|
|
255
|
+
urlObj.path !== fullImageName ||
|
|
256
|
+
tmpA[0].includes(".") ||
|
|
257
|
+
tmpA[0].includes(":")
|
|
258
|
+
) {
|
|
259
|
+
nameObj.registry = tmpA[0];
|
|
260
|
+
fullImageName = fullImageName.replace(tmpA[0] + "/", "");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Extract digest name
|
|
264
|
+
if (fullImageName.includes("@sha256:")) {
|
|
265
|
+
const tmpA = fullImageName.split("@sha256:");
|
|
266
|
+
if (tmpA.length > 1) {
|
|
267
|
+
nameObj.digest = tmpA[tmpA.length - 1];
|
|
268
|
+
fullImageName = fullImageName.replace("@sha256:" + nameObj.digest, "");
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Extract tag name
|
|
272
|
+
if (fullImageName.includes(":")) {
|
|
273
|
+
const tmpA = fullImageName.split(":");
|
|
274
|
+
if (tmpA.length > 1) {
|
|
275
|
+
nameObj.tag = tmpA[tmpA.length - 1];
|
|
276
|
+
fullImageName = fullImageName.replace(":" + nameObj.tag, "");
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// The left over string is the repo name
|
|
280
|
+
nameObj.repo = fullImageName;
|
|
281
|
+
return nameObj;
|
|
282
|
+
};
|
|
283
|
+
exports.parseImageName = parseImageName;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Method to get image to the local registry by pulling from the remote if required
|
|
287
|
+
*/
|
|
288
|
+
const getImage = async (fullImageName) => {
|
|
289
|
+
let localData = undefined;
|
|
290
|
+
const { repo, tag, digest } = parseImageName(fullImageName);
|
|
291
|
+
// Fetch only the latest tag if none is specified
|
|
292
|
+
if (tag === "" && digest === "") {
|
|
293
|
+
fullImageName = fullImageName + ":latest";
|
|
294
|
+
}
|
|
295
|
+
if (isWin) {
|
|
296
|
+
let result = spawnSync("docker", ["pull", fullImageName], {
|
|
297
|
+
encoding: "utf-8"
|
|
298
|
+
});
|
|
299
|
+
if (result.status !== 0 || result.error) {
|
|
300
|
+
return localData;
|
|
301
|
+
} else {
|
|
302
|
+
result = spawnSync("docker", ["inspect", fullImageName], {
|
|
303
|
+
encoding: "utf-8"
|
|
304
|
+
});
|
|
305
|
+
if (result.status !== 0 || result.error) {
|
|
306
|
+
return localData;
|
|
307
|
+
} else {
|
|
308
|
+
try {
|
|
309
|
+
const stdout = result.stdout;
|
|
310
|
+
if (stdout) {
|
|
311
|
+
const inspectData = JSON.parse(Buffer.from(stdout).toString());
|
|
312
|
+
if (inspectData && Array.isArray(inspectData)) {
|
|
313
|
+
return inspectData[0];
|
|
314
|
+
} else {
|
|
315
|
+
return inspectData;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
} catch (err) {
|
|
319
|
+
// continue regardless of error
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
localData = await makeRequest(`images/${repo}/json`);
|
|
326
|
+
if (DEBUG_MODE) {
|
|
327
|
+
console.log(localData);
|
|
328
|
+
}
|
|
329
|
+
} catch (err) {
|
|
330
|
+
if (DEBUG_MODE) {
|
|
331
|
+
console.log(
|
|
332
|
+
`Trying to pull the image ${fullImageName} from registry. This might take a while ...`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
// If the data is not available locally
|
|
336
|
+
try {
|
|
337
|
+
const pullData = await makeRequest(
|
|
338
|
+
`images/create?fromImage=${fullImageName}`,
|
|
339
|
+
"POST"
|
|
340
|
+
);
|
|
341
|
+
if (
|
|
342
|
+
pullData &&
|
|
343
|
+
(pullData.includes("no match for platform in manifest") ||
|
|
344
|
+
pullData.includes("Error choosing an image from manifest list"))
|
|
345
|
+
) {
|
|
346
|
+
console.warn(
|
|
347
|
+
"You may have to enable experimental settings in docker to support this platform!"
|
|
348
|
+
);
|
|
349
|
+
console.warn(
|
|
350
|
+
"To scan windows images, run cdxgen on a windows server with hyper-v and docker installed. Switch to windows containers in your docker settings."
|
|
351
|
+
);
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
} catch (err) {
|
|
355
|
+
// continue regardless of error
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
if (DEBUG_MODE) {
|
|
359
|
+
console.log(`Trying with ${repo}`);
|
|
360
|
+
}
|
|
361
|
+
localData = await makeRequest(`images/${repo}/json`);
|
|
362
|
+
if (DEBUG_MODE) {
|
|
363
|
+
console.log(localData);
|
|
364
|
+
}
|
|
365
|
+
} catch (err) {
|
|
366
|
+
if (DEBUG_MODE) {
|
|
367
|
+
console.log(`Retrying with ${fullImageName} due to`, err);
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
localData = await makeRequest(`images/${fullImageName}/json`);
|
|
371
|
+
if (DEBUG_MODE) {
|
|
372
|
+
console.log(localData);
|
|
373
|
+
}
|
|
374
|
+
} catch (err) {
|
|
375
|
+
// continue regardless of error
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (!localData) {
|
|
380
|
+
console.log(
|
|
381
|
+
`Unable to pull ${fullImageName}. Check if the name is valid. Perform any authentication prior to invoking cdxgen.`
|
|
382
|
+
);
|
|
383
|
+
console.log(
|
|
384
|
+
`Trying manually pulling this image using docker pull ${fullImageName}`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
return localData;
|
|
388
|
+
};
|
|
389
|
+
exports.getImage = getImage;
|
|
390
|
+
|
|
391
|
+
const extractTar = async (fullImageName, dir) => {
|
|
392
|
+
try {
|
|
393
|
+
await pipeline(
|
|
394
|
+
fs.createReadStream(fullImageName),
|
|
395
|
+
tar.x({
|
|
396
|
+
sync: true,
|
|
397
|
+
preserveOwner: false,
|
|
398
|
+
noMtime: true,
|
|
399
|
+
noChmod: true,
|
|
400
|
+
strict: true,
|
|
401
|
+
C: dir,
|
|
402
|
+
portable: true,
|
|
403
|
+
onwarn: () => {}
|
|
404
|
+
})
|
|
405
|
+
);
|
|
406
|
+
return true;
|
|
407
|
+
} catch (err) {
|
|
408
|
+
if (DEBUG_MODE) {
|
|
409
|
+
console.log(err);
|
|
410
|
+
}
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
exports.extractTar = extractTar;
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Method to export a container image archive.
|
|
418
|
+
* Returns the location of the layers with additional packages related metadata
|
|
419
|
+
*/
|
|
420
|
+
const exportArchive = async (fullImageName) => {
|
|
421
|
+
if (!fs.existsSync(fullImageName)) {
|
|
422
|
+
console.log(`Unable to find container image archive ${fullImageName}`);
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
425
|
+
let manifest = {};
|
|
426
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "docker-images-"));
|
|
427
|
+
const allLayersExplodedDir = path.join(tempDir, "all-layers");
|
|
428
|
+
const blobsDir = path.join(tempDir, "blobs", "sha256");
|
|
429
|
+
fs.mkdirSync(allLayersExplodedDir);
|
|
430
|
+
const manifestFile = path.join(tempDir, "manifest.json");
|
|
431
|
+
try {
|
|
432
|
+
await extractTar(fullImageName, tempDir);
|
|
433
|
+
// podman use blobs dir
|
|
434
|
+
if (fs.existsSync(blobsDir)) {
|
|
435
|
+
if (DEBUG_MODE) {
|
|
436
|
+
console.log(
|
|
437
|
+
`Image archive ${fullImageName} successfully exported to directory ${tempDir}`
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
const allBlobs = getDirs(blobsDir, "*", false, true);
|
|
441
|
+
for (let ablob of allBlobs) {
|
|
442
|
+
if (DEBUG_MODE) {
|
|
443
|
+
console.log(`Extracting ${ablob} to ${allLayersExplodedDir}`);
|
|
444
|
+
}
|
|
445
|
+
await extractTar(ablob, allLayersExplodedDir);
|
|
446
|
+
}
|
|
447
|
+
let lastLayerConfig = {};
|
|
448
|
+
let lastWorkingDir = "";
|
|
449
|
+
const exportData = {
|
|
450
|
+
manifest,
|
|
451
|
+
allLayersDir: tempDir,
|
|
452
|
+
allLayersExplodedDir,
|
|
453
|
+
lastLayerConfig,
|
|
454
|
+
lastWorkingDir
|
|
455
|
+
};
|
|
456
|
+
exportData.pkgPathList = getPkgPathList(exportData, lastWorkingDir);
|
|
457
|
+
return exportData;
|
|
458
|
+
} else if (fs.existsSync(manifestFile)) {
|
|
459
|
+
// docker manifest file
|
|
460
|
+
return await extractFromManifest(
|
|
461
|
+
manifestFile,
|
|
462
|
+
{},
|
|
463
|
+
tempDir,
|
|
464
|
+
allLayersExplodedDir
|
|
465
|
+
);
|
|
466
|
+
} else {
|
|
467
|
+
console.log(`Unable to extract image archive to ${tempDir}`);
|
|
468
|
+
}
|
|
469
|
+
} catch (err) {
|
|
470
|
+
console.log(err);
|
|
471
|
+
}
|
|
472
|
+
return undefined;
|
|
473
|
+
};
|
|
474
|
+
exports.exportArchive = exportArchive;
|
|
475
|
+
|
|
476
|
+
const extractFromManifest = async (
|
|
477
|
+
manifestFile,
|
|
478
|
+
localData,
|
|
479
|
+
tempDir,
|
|
480
|
+
allLayersExplodedDir
|
|
481
|
+
) => {
|
|
482
|
+
// Example of manifests
|
|
483
|
+
// [{"Config":"blobs/sha256/dedc100afa8d6718f5ac537730dd4a5ceea3563e695c90f1a8ac6df32c4cb291","RepoTags":["shiftleft/core:latest"],"Layers":["blobs/sha256/eaead16dc43bb8811d4ff450935d607f9ba4baffda4fc110cc402fa43f601d83","blobs/sha256/2039af03c0e17a3025b989335e9414149577fa09e7d0dcbee80155333639d11f"]}]
|
|
484
|
+
// {"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.docker.distribution.manifest.list.v2+json","digest":"sha256:7706ac20c7587081dc7a00e0ec65a6633b0bb3788e0048a3e971d3eae492db63","size":318,"annotations":{"io.containerd.image.name":"docker.io/shiftleft/scan-slim:latest","org.opencontainers.image.ref.name":"latest"}}]}
|
|
485
|
+
let manifest = JSON.parse(
|
|
486
|
+
fs.readFileSync(manifestFile, {
|
|
487
|
+
encoding: "utf-8"
|
|
488
|
+
})
|
|
489
|
+
);
|
|
490
|
+
let lastLayerConfig = {};
|
|
491
|
+
let lastLayerConfigFile = "";
|
|
492
|
+
let lastWorkingDir = "";
|
|
493
|
+
// Extract the manifest for the new containerd syntax
|
|
494
|
+
if (Object.keys(manifest).length !== 0 && manifest.manifests) {
|
|
495
|
+
manifest = manifest.manifests;
|
|
496
|
+
}
|
|
497
|
+
if (Array.isArray(manifest)) {
|
|
498
|
+
if (manifest.length !== 1) {
|
|
499
|
+
if (DEBUG_MODE) {
|
|
500
|
+
console.log(
|
|
501
|
+
"Multiple image tags was downloaded. Only the last one would be used"
|
|
502
|
+
);
|
|
503
|
+
console.log(manifest[manifest.length - 1]);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
let layers = manifest[manifest.length - 1]["Layers"] || [];
|
|
507
|
+
if (!layers.length && fs.existsSync(tempDir)) {
|
|
508
|
+
const blobFiles = fs.readdirSync(path.join(tempDir, "blobs", "sha256"));
|
|
509
|
+
if (blobFiles && blobFiles.length) {
|
|
510
|
+
for (const blobf of blobFiles) {
|
|
511
|
+
layers.push(path.join("blobs", "sha256", blobf));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
const lastLayer = layers[layers.length - 1];
|
|
516
|
+
for (let layer of layers) {
|
|
517
|
+
if (DEBUG_MODE) {
|
|
518
|
+
console.log(`Extracting layer ${layer} to ${allLayersExplodedDir}`);
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
await extractTar(path.join(tempDir, layer), allLayersExplodedDir);
|
|
522
|
+
} catch (err) {
|
|
523
|
+
console.log(err);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (manifest.Config) {
|
|
527
|
+
lastLayerConfigFile = path.join(tempDir, manifest.Config);
|
|
528
|
+
}
|
|
529
|
+
if (lastLayer.includes("layer.tar")) {
|
|
530
|
+
lastLayerConfigFile = path.join(
|
|
531
|
+
tempDir,
|
|
532
|
+
lastLayer.replace("layer.tar", "json")
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
if (lastLayerConfigFile && fs.existsSync(lastLayerConfigFile)) {
|
|
536
|
+
try {
|
|
537
|
+
lastLayerConfig = JSON.parse(
|
|
538
|
+
fs.readFileSync(lastLayerConfigFile, {
|
|
539
|
+
encoding: "utf-8"
|
|
540
|
+
})
|
|
541
|
+
);
|
|
542
|
+
lastWorkingDir =
|
|
543
|
+
lastLayerConfig.config && lastLayerConfig.config.WorkingDir
|
|
544
|
+
? path.join(allLayersExplodedDir, lastLayerConfig.config.WorkingDir)
|
|
545
|
+
: "";
|
|
546
|
+
} catch (err) {
|
|
547
|
+
console.log(err);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const exportData = {
|
|
552
|
+
inspectData: localData,
|
|
553
|
+
manifest,
|
|
554
|
+
allLayersDir: tempDir,
|
|
555
|
+
allLayersExplodedDir,
|
|
556
|
+
lastLayerConfig,
|
|
557
|
+
lastWorkingDir
|
|
558
|
+
};
|
|
559
|
+
exportData.pkgPathList = getPkgPathList(exportData, lastWorkingDir);
|
|
560
|
+
return exportData;
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Method to export a container image by using the export feature in docker or podman service.
|
|
565
|
+
* Returns the location of the layers with additional packages related metadata
|
|
566
|
+
*/
|
|
567
|
+
const exportImage = async (fullImageName) => {
|
|
568
|
+
// Try to get the data locally first
|
|
569
|
+
const localData = await getImage(fullImageName);
|
|
570
|
+
if (!localData) {
|
|
571
|
+
return undefined;
|
|
572
|
+
}
|
|
573
|
+
const { tag, digest } = parseImageName(fullImageName);
|
|
574
|
+
// Fetch only the latest tag if none is specified
|
|
575
|
+
if (tag === "" && digest === "") {
|
|
576
|
+
fullImageName = fullImageName + ":latest";
|
|
577
|
+
}
|
|
578
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "docker-images-"));
|
|
579
|
+
const allLayersExplodedDir = path.join(tempDir, "all-layers");
|
|
580
|
+
let manifestFile = path.join(tempDir, "manifest.json");
|
|
581
|
+
// Windows containers use index.json
|
|
582
|
+
const manifestIndexFile = path.join(tempDir, "index.json");
|
|
583
|
+
// On Windows, fallback to invoking cli
|
|
584
|
+
if (isWin) {
|
|
585
|
+
const imageTarFile = path.join(tempDir, "image.tar");
|
|
586
|
+
console.log(
|
|
587
|
+
`About to export image ${fullImageName} to ${imageTarFile} using docker cli`
|
|
588
|
+
);
|
|
589
|
+
let result = spawnSync(
|
|
590
|
+
"docker",
|
|
591
|
+
["save", "-o", imageTarFile, fullImageName],
|
|
592
|
+
{
|
|
593
|
+
encoding: "utf-8"
|
|
594
|
+
}
|
|
595
|
+
);
|
|
596
|
+
if (result.status !== 0 || result.error) {
|
|
597
|
+
if (result.stdout || result.stderr) {
|
|
598
|
+
console.log(result.stdout, result.stderr);
|
|
599
|
+
}
|
|
600
|
+
return localData;
|
|
601
|
+
} else {
|
|
602
|
+
await extractTar(imageTarFile, tempDir);
|
|
603
|
+
if (DEBUG_MODE) {
|
|
604
|
+
console.log(`Cleaning up ${imageTarFile}`);
|
|
605
|
+
}
|
|
606
|
+
fs.rmSync(imageTarFile, { force: true });
|
|
607
|
+
}
|
|
608
|
+
} else {
|
|
609
|
+
let client = await getConnection();
|
|
610
|
+
try {
|
|
611
|
+
if (DEBUG_MODE) {
|
|
612
|
+
console.log(`About to export image ${fullImageName} to ${tempDir}`);
|
|
613
|
+
}
|
|
614
|
+
await pipeline(
|
|
615
|
+
client.stream(`images/${fullImageName}/get`),
|
|
616
|
+
tar.x({
|
|
617
|
+
sync: true,
|
|
618
|
+
preserveOwner: false,
|
|
619
|
+
noMtime: true,
|
|
620
|
+
noChmod: true,
|
|
621
|
+
strict: true,
|
|
622
|
+
C: tempDir,
|
|
623
|
+
portable: true,
|
|
624
|
+
onwarn: () => {}
|
|
625
|
+
})
|
|
626
|
+
);
|
|
627
|
+
} catch (err) {
|
|
628
|
+
console.error(err);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// Continue with extracting the layers
|
|
632
|
+
if (fs.existsSync(tempDir)) {
|
|
633
|
+
if (fs.existsSync(manifestFile)) {
|
|
634
|
+
// This is fine
|
|
635
|
+
} else if (fs.existsSync(manifestIndexFile)) {
|
|
636
|
+
manifestFile = manifestIndexFile;
|
|
637
|
+
} else {
|
|
638
|
+
console.log(
|
|
639
|
+
`Manifest file ${manifestFile} was not found after export at ${tempDir}`
|
|
640
|
+
);
|
|
641
|
+
return undefined;
|
|
642
|
+
}
|
|
643
|
+
if (DEBUG_MODE) {
|
|
644
|
+
console.log(
|
|
645
|
+
`Image ${fullImageName} successfully exported to directory ${tempDir}`
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
fs.mkdirSync(allLayersExplodedDir);
|
|
649
|
+
return await extractFromManifest(
|
|
650
|
+
manifestFile,
|
|
651
|
+
localData,
|
|
652
|
+
tempDir,
|
|
653
|
+
allLayersExplodedDir
|
|
654
|
+
);
|
|
655
|
+
} else {
|
|
656
|
+
console.log(`Unable to export image to ${tempDir}`);
|
|
657
|
+
}
|
|
658
|
+
return undefined;
|
|
659
|
+
};
|
|
660
|
+
exports.exportImage = exportImage;
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Method to retrieve path list for system-level packages
|
|
664
|
+
*/
|
|
665
|
+
const getPkgPathList = (exportData, lastWorkingDir) => {
|
|
666
|
+
const allLayersExplodedDir = exportData.allLayersExplodedDir;
|
|
667
|
+
const allLayersDir = exportData.allLayersDir;
|
|
668
|
+
let pathList = [];
|
|
669
|
+
let knownSysPaths = [];
|
|
670
|
+
if (allLayersExplodedDir && allLayersExplodedDir !== "") {
|
|
671
|
+
knownSysPaths = [
|
|
672
|
+
path.join(allLayersExplodedDir, "/usr/local/go"),
|
|
673
|
+
path.join(allLayersExplodedDir, "/usr/local/lib"),
|
|
674
|
+
path.join(allLayersExplodedDir, "/usr/local/lib64"),
|
|
675
|
+
path.join(allLayersExplodedDir, "/opt"),
|
|
676
|
+
path.join(allLayersExplodedDir, "/home"),
|
|
677
|
+
path.join(allLayersExplodedDir, "/usr/share"),
|
|
678
|
+
path.join(allLayersExplodedDir, "/usr/src"),
|
|
679
|
+
path.join(allLayersExplodedDir, "/var/www/html"),
|
|
680
|
+
path.join(allLayersExplodedDir, "/var/lib"),
|
|
681
|
+
path.join(allLayersExplodedDir, "/mnt")
|
|
682
|
+
];
|
|
683
|
+
} else if (allLayersExplodedDir === "") {
|
|
684
|
+
knownSysPaths = [
|
|
685
|
+
path.join(allLayersExplodedDir, "/usr/local/go"),
|
|
686
|
+
path.join(allLayersExplodedDir, "/usr/local/lib"),
|
|
687
|
+
path.join(allLayersExplodedDir, "/usr/local/lib64"),
|
|
688
|
+
path.join(allLayersExplodedDir, "/opt"),
|
|
689
|
+
path.join(allLayersExplodedDir, "/usr/share"),
|
|
690
|
+
path.join(allLayersExplodedDir, "/usr/src"),
|
|
691
|
+
path.join(allLayersExplodedDir, "/var/www/html"),
|
|
692
|
+
path.join(allLayersExplodedDir, "/var/lib")
|
|
693
|
+
];
|
|
694
|
+
}
|
|
695
|
+
if (fs.existsSync(path.join(allLayersDir, "Files"))) {
|
|
696
|
+
knownSysPaths.push(path.join(allLayersDir, "Files"));
|
|
697
|
+
}
|
|
698
|
+
/*
|
|
699
|
+
// Too slow
|
|
700
|
+
if (fs.existsSync(path.join(allLayersDir, "Users"))) {
|
|
701
|
+
knownSysPaths.push(path.join(allLayersDir, "Users"));
|
|
702
|
+
}
|
|
703
|
+
*/
|
|
704
|
+
if (fs.existsSync(path.join(allLayersDir, "ProgramData"))) {
|
|
705
|
+
knownSysPaths.push(path.join(allLayersDir, "ProgramData"));
|
|
706
|
+
}
|
|
707
|
+
const pyInstalls = getDirs(allLayersDir, "Python*/", false, false);
|
|
708
|
+
if (pyInstalls && pyInstalls.length) {
|
|
709
|
+
for (let pyiPath of pyInstalls) {
|
|
710
|
+
const pyDirs = getOnlyDirs(pyiPath, "site-packages");
|
|
711
|
+
if (pyDirs && pyDirs.length) {
|
|
712
|
+
pathList = pathList.concat(pyDirs);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (lastWorkingDir && lastWorkingDir !== "") {
|
|
717
|
+
knownSysPaths.push(lastWorkingDir);
|
|
718
|
+
// Some more common app dirs
|
|
719
|
+
if (!lastWorkingDir.startsWith("/app")) {
|
|
720
|
+
knownSysPaths.push(path.join(allLayersExplodedDir, "/app"));
|
|
721
|
+
}
|
|
722
|
+
if (!lastWorkingDir.startsWith("/data")) {
|
|
723
|
+
knownSysPaths.push(path.join(allLayersExplodedDir, "/data"));
|
|
724
|
+
}
|
|
725
|
+
if (!lastWorkingDir.startsWith("/srv")) {
|
|
726
|
+
knownSysPaths.push(path.join(allLayersExplodedDir, "/srv"));
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
// Known to cause EACCESS error
|
|
730
|
+
knownSysPaths.push(path.join(allLayersExplodedDir, "/usr/lib"));
|
|
731
|
+
knownSysPaths.push(path.join(allLayersExplodedDir, "/usr/lib64"));
|
|
732
|
+
// Build path list
|
|
733
|
+
for (let wpath of knownSysPaths) {
|
|
734
|
+
pathList = pathList.concat(wpath);
|
|
735
|
+
const pyDirs = getOnlyDirs(wpath, "site-packages");
|
|
736
|
+
if (pyDirs && pyDirs.length) {
|
|
737
|
+
pathList = pathList.concat(pyDirs);
|
|
738
|
+
}
|
|
739
|
+
const gemsDirs = getOnlyDirs(wpath, "gems");
|
|
740
|
+
if (gemsDirs && gemsDirs.length) {
|
|
741
|
+
pathList = pathList.concat(gemsDirs);
|
|
742
|
+
}
|
|
743
|
+
const cargoDirs = getOnlyDirs(wpath, ".cargo");
|
|
744
|
+
if (cargoDirs && cargoDirs.length) {
|
|
745
|
+
pathList = pathList.concat(cargoDirs);
|
|
746
|
+
}
|
|
747
|
+
const composerDirs = getOnlyDirs(wpath, ".composer");
|
|
748
|
+
if (composerDirs && composerDirs.length) {
|
|
749
|
+
pathList = pathList.concat(composerDirs);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (DEBUG_MODE) {
|
|
753
|
+
console.log("pathList", pathList);
|
|
754
|
+
}
|
|
755
|
+
return pathList;
|
|
756
|
+
};
|
|
757
|
+
exports.getPkgPathList = getPkgPathList;
|
|
758
|
+
|
|
759
|
+
const removeImage = async (fullImageName, force = false) => {
|
|
760
|
+
const removeData = await makeRequest(
|
|
761
|
+
`images/${fullImageName}?force=${force}`,
|
|
762
|
+
"DELETE"
|
|
763
|
+
);
|
|
764
|
+
if (DEBUG_MODE) {
|
|
765
|
+
console.log(removeData);
|
|
766
|
+
}
|
|
767
|
+
return removeData;
|
|
768
|
+
};
|
|
769
|
+
exports.removeImage = removeImage;
|