@cyclonedx/cdxgen 9.6.0 → 9.6.1
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 +1 -1
- package/bin/evinse.js +0 -0
- package/bin/verify.js +0 -0
- package/binary.js +128 -23
- package/index.js +58 -6
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -376,7 +376,7 @@ cdxgen -t os
|
|
|
376
376
|
|
|
377
377
|
This feature is powered by osquery which is [installed](https://github.com/cyclonedx/cdxgen-plugins-bin/blob/main/build.sh#L8) along with the binary plugins. cdxgen would opportunistically try to detect as many components, apps and extensions as possible using the [default queries](queries.json). The process would take several minutes and result in an SBoM file with thousands of components.
|
|
378
378
|
|
|
379
|
-
## Generating component
|
|
379
|
+
## Generating SaaSBoM and component evidences
|
|
380
380
|
|
|
381
381
|
See [evinse mode](./ADVANCED.md) in the advanced documentation.
|
|
382
382
|
|
package/bin/evinse.js
CHANGED
|
File without changes
|
package/bin/verify.js
CHANGED
|
File without changes
|
package/binary.js
CHANGED
|
@@ -6,13 +6,12 @@ import { PackageURL } from "packageurl-js";
|
|
|
6
6
|
import { DEBUG_MODE } from "./utils.js";
|
|
7
7
|
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import path from "node:path";
|
|
10
9
|
|
|
11
10
|
let url = import.meta.url;
|
|
12
11
|
if (!url.startsWith("file://")) {
|
|
13
12
|
url = new URL(`file://${import.meta.url}`).toString();
|
|
14
13
|
}
|
|
15
|
-
const dirName = import.meta ?
|
|
14
|
+
const dirName = import.meta ? dirname(fileURLToPath(url)) : __dirname;
|
|
16
15
|
|
|
17
16
|
const isWin = _platform() === "win32";
|
|
18
17
|
|
|
@@ -181,6 +180,7 @@ const OS_DISTRO_ALIAS = {
|
|
|
181
180
|
"ubuntu-19.10": "eoan",
|
|
182
181
|
"ubuntu-20.04": "focal",
|
|
183
182
|
"ubuntu-20.10": "groovy",
|
|
183
|
+
"ubuntu-22.04": "jammy",
|
|
184
184
|
"ubuntu-23.04": "lunar",
|
|
185
185
|
"debian-14": "forky",
|
|
186
186
|
"debian-14.5": "forky",
|
|
@@ -266,6 +266,7 @@ export const getCargoAuditableInfo = (src) => {
|
|
|
266
266
|
|
|
267
267
|
export const getOSPackages = (src) => {
|
|
268
268
|
const pkgList = [];
|
|
269
|
+
const dependenciesList = [];
|
|
269
270
|
const allTypes = new Set();
|
|
270
271
|
if (TRIVY_BIN) {
|
|
271
272
|
let imageType = "image";
|
|
@@ -321,6 +322,54 @@ export const getOSPackages = (src) => {
|
|
|
321
322
|
rmSync(tempDir, { recursive: true, force: true });
|
|
322
323
|
}
|
|
323
324
|
}
|
|
325
|
+
const osReleaseData = {};
|
|
326
|
+
// Let's try to read the os-release file
|
|
327
|
+
if (existsSync(join(src, "usr", "lib", "os-release"))) {
|
|
328
|
+
const osReleaseInfo = readFileSync(
|
|
329
|
+
join(src, "usr", "lib", "os-release"),
|
|
330
|
+
"utf-8"
|
|
331
|
+
);
|
|
332
|
+
if (osReleaseInfo) {
|
|
333
|
+
osReleaseInfo.split("\n").forEach((l) => {
|
|
334
|
+
if (l.includes("=")) {
|
|
335
|
+
const tmpA = l.split("=");
|
|
336
|
+
osReleaseData[tmpA[0]] = tmpA[1].replace(/"/g, "");
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (DEBUG_MODE) {
|
|
342
|
+
console.log(osReleaseData);
|
|
343
|
+
}
|
|
344
|
+
let distro_codename = osReleaseData["VERSION_CODENAME"] || "";
|
|
345
|
+
let distro_id = osReleaseData["ID"] || "";
|
|
346
|
+
let distro_id_like = osReleaseData["ID_LIKE"] || "";
|
|
347
|
+
let purl_type = "rpm";
|
|
348
|
+
switch (distro_id) {
|
|
349
|
+
case "debian":
|
|
350
|
+
case "ubuntu":
|
|
351
|
+
case "pop":
|
|
352
|
+
purl_type = "deb";
|
|
353
|
+
break;
|
|
354
|
+
default:
|
|
355
|
+
if (distro_id_like.includes("debian")) {
|
|
356
|
+
purl_type = "deb";
|
|
357
|
+
} else if (
|
|
358
|
+
distro_id_like.includes("rhel") ||
|
|
359
|
+
distro_id_like.includes("centos") ||
|
|
360
|
+
distro_id_like.includes("fedora")
|
|
361
|
+
) {
|
|
362
|
+
purl_type = "rpm";
|
|
363
|
+
}
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
if (osReleaseData["VERSION_ID"]) {
|
|
367
|
+
distro_id = distro_id + "-" + osReleaseData["VERSION_ID"];
|
|
368
|
+
}
|
|
369
|
+
const tmpDependencies = {};
|
|
370
|
+
(tmpBom.dependencies || []).forEach((d) => {
|
|
371
|
+
tmpDependencies[d.ref] = d.dependsOn;
|
|
372
|
+
});
|
|
324
373
|
if (tmpBom && tmpBom.components) {
|
|
325
374
|
for (const comp of tmpBom.components) {
|
|
326
375
|
if (comp.purl) {
|
|
@@ -342,11 +391,11 @@ export const getOSPackages = (src) => {
|
|
|
342
391
|
) {
|
|
343
392
|
continue;
|
|
344
393
|
}
|
|
394
|
+
const origBomRef = comp["bom-ref"];
|
|
345
395
|
// Fix the group
|
|
346
396
|
let group = dirname(comp.name);
|
|
347
397
|
const name = basename(comp.name);
|
|
348
398
|
let purlObj = undefined;
|
|
349
|
-
let distro_codename = "";
|
|
350
399
|
if (group === ".") {
|
|
351
400
|
group = "";
|
|
352
401
|
}
|
|
@@ -360,19 +409,23 @@ export const getOSPackages = (src) => {
|
|
|
360
409
|
comp.group = group;
|
|
361
410
|
purlObj.namespace = group;
|
|
362
411
|
}
|
|
412
|
+
if (distro_id && distro_id.length) {
|
|
413
|
+
purlObj.qualifiers["distro"] = distro_id;
|
|
414
|
+
}
|
|
415
|
+
if (distro_codename && distro_codename.length) {
|
|
416
|
+
purlObj.qualifiers["distro_name"] = distro_codename;
|
|
417
|
+
}
|
|
363
418
|
// Bug fix for mageia and oracle linux
|
|
419
|
+
// Type is being returned as none for ubuntu as well!
|
|
364
420
|
if (purlObj.type === "none") {
|
|
365
|
-
purlObj["type"] =
|
|
421
|
+
purlObj["type"] = purl_type;
|
|
366
422
|
purlObj["namespace"] = "";
|
|
367
423
|
comp.group = "";
|
|
368
|
-
distro_codename = undefined;
|
|
369
424
|
if (comp.purl && comp.purl.includes(".mga")) {
|
|
370
425
|
purlObj["namespace"] = "mageia";
|
|
371
426
|
comp.group = "mageia";
|
|
372
427
|
purlObj.qualifiers["distro"] = "mageia";
|
|
373
428
|
distro_codename = "mga";
|
|
374
|
-
} else if (comp.purl && comp.purl.includes(".el8")) {
|
|
375
|
-
purlObj.qualifiers["distro"] = "el8";
|
|
376
429
|
}
|
|
377
430
|
comp.purl = new PackageURL(
|
|
378
431
|
purlObj.type,
|
|
@@ -412,25 +465,25 @@ export const getOSPackages = (src) => {
|
|
|
412
465
|
);
|
|
413
466
|
}
|
|
414
467
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
468
|
+
}
|
|
469
|
+
if (distro_codename !== "") {
|
|
470
|
+
allTypes.add(distro_codename);
|
|
471
|
+
allTypes.add(purlObj.namespace);
|
|
472
|
+
comp.purl = new PackageURL(
|
|
473
|
+
purlObj.type,
|
|
474
|
+
purlObj.namespace,
|
|
475
|
+
name,
|
|
476
|
+
purlObj.version,
|
|
477
|
+
purlObj.qualifiers,
|
|
478
|
+
purlObj.subpath
|
|
479
|
+
).toString();
|
|
480
|
+
comp["bom-ref"] = decodeURIComponent(comp.purl);
|
|
429
481
|
}
|
|
430
482
|
} catch (err) {
|
|
431
483
|
// continue regardless of error
|
|
432
484
|
}
|
|
433
485
|
}
|
|
486
|
+
// Fix licenses
|
|
434
487
|
if (
|
|
435
488
|
comp.licenses &&
|
|
436
489
|
Array.isArray(comp.licenses) &&
|
|
@@ -438,6 +491,17 @@ export const getOSPackages = (src) => {
|
|
|
438
491
|
) {
|
|
439
492
|
comp.licenses = [comp.licenses[0]];
|
|
440
493
|
}
|
|
494
|
+
// Fix hashes
|
|
495
|
+
if (
|
|
496
|
+
comp.hashes &&
|
|
497
|
+
Array.isArray(comp.hashes) &&
|
|
498
|
+
comp.hashes.length
|
|
499
|
+
) {
|
|
500
|
+
const hashContent = comp.hashes[0].content;
|
|
501
|
+
if (!hashContent || hashContent.length < 32) {
|
|
502
|
+
delete comp.hashes;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
441
505
|
const compProperties = comp.properties;
|
|
442
506
|
let srcName = undefined;
|
|
443
507
|
let srcVersion = undefined;
|
|
@@ -453,6 +517,14 @@ export const getOSPackages = (src) => {
|
|
|
453
517
|
}
|
|
454
518
|
delete comp.properties;
|
|
455
519
|
pkgList.push(comp);
|
|
520
|
+
const compDeps = retrieveDependencies(
|
|
521
|
+
tmpDependencies,
|
|
522
|
+
origBomRef,
|
|
523
|
+
comp
|
|
524
|
+
);
|
|
525
|
+
if (compDeps) {
|
|
526
|
+
dependenciesList.push(compDeps);
|
|
527
|
+
}
|
|
456
528
|
// If there is a source package defined include it as well
|
|
457
529
|
if (srcName && srcVersion && srcName !== comp.name) {
|
|
458
530
|
const newComp = Object.assign({}, comp);
|
|
@@ -474,10 +546,43 @@ export const getOSPackages = (src) => {
|
|
|
474
546
|
}
|
|
475
547
|
}
|
|
476
548
|
}
|
|
477
|
-
return { osPackages: pkgList, allTypes: Array.from(allTypes) };
|
|
478
549
|
}
|
|
479
550
|
}
|
|
480
|
-
return {
|
|
551
|
+
return {
|
|
552
|
+
osPackages: pkgList,
|
|
553
|
+
dependenciesList,
|
|
554
|
+
allTypes: Array.from(allTypes)
|
|
555
|
+
};
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const retrieveDependencies = (tmpDependencies, origBomRef, comp) => {
|
|
559
|
+
try {
|
|
560
|
+
const tmpDependsOn = tmpDependencies[origBomRef] || [];
|
|
561
|
+
const dependsOn = new Set();
|
|
562
|
+
tmpDependsOn.forEach((d) => {
|
|
563
|
+
try {
|
|
564
|
+
const compPurl = PackageURL.fromString(comp.purl);
|
|
565
|
+
const tmpPurl = PackageURL.fromString(d.replace("none", compPurl.type));
|
|
566
|
+
tmpPurl.type = compPurl.type;
|
|
567
|
+
tmpPurl.namespace = compPurl.namespace;
|
|
568
|
+
if (compPurl.qualifiers) {
|
|
569
|
+
if (compPurl.qualifiers.distro_name) {
|
|
570
|
+
tmpPurl.qualifiers.distro_name = compPurl.qualifiers.distro_name;
|
|
571
|
+
}
|
|
572
|
+
if (compPurl.qualifiers.distro) {
|
|
573
|
+
tmpPurl.qualifiers.distro = compPurl.qualifiers.distro;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
dependsOn.add(decodeURIComponent(tmpPurl.toString()));
|
|
577
|
+
} catch (e) {
|
|
578
|
+
// ignore
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
return { ref: comp["bom-ref"], dependsOn: Array.from(dependsOn).sort() };
|
|
582
|
+
} catch (e) {
|
|
583
|
+
// ignore
|
|
584
|
+
}
|
|
585
|
+
return undefined;
|
|
481
586
|
};
|
|
482
587
|
|
|
483
588
|
export const executeOsQuery = (query) => {
|
package/index.js
CHANGED
|
@@ -372,7 +372,16 @@ function addMetadata(parentComponent = {}, format = "xml", options = {}) {
|
|
|
372
372
|
// We cannot use purl or bom-ref here since they would not match
|
|
373
373
|
// purl - could have application on one side and a different type
|
|
374
374
|
// bom-ref could have qualifiers on one side
|
|
375
|
-
|
|
375
|
+
// Ignore components that have the same name as the parent component but with latest as the version.
|
|
376
|
+
// These are default components created based on directory names
|
|
377
|
+
if (
|
|
378
|
+
fullName !== parentFullName &&
|
|
379
|
+
!(
|
|
380
|
+
(comp.name === parentComponent.name ||
|
|
381
|
+
comp.name === parentComponent.name + ":latest") &&
|
|
382
|
+
comp.version === "latest"
|
|
383
|
+
)
|
|
384
|
+
) {
|
|
376
385
|
if (!comp["bom-ref"]) {
|
|
377
386
|
comp["bom-ref"] = `pkg:${comp.type}/${fullName}`;
|
|
378
387
|
}
|
|
@@ -380,6 +389,7 @@ function addMetadata(parentComponent = {}, format = "xml", options = {}) {
|
|
|
380
389
|
}
|
|
381
390
|
}
|
|
382
391
|
} // for
|
|
392
|
+
parentComponent.components = subComponents;
|
|
383
393
|
}
|
|
384
394
|
if (format === "json") {
|
|
385
395
|
metadata.component = parentComponent;
|
|
@@ -2199,10 +2209,13 @@ export const createPythonBom = async (path, options) => {
|
|
|
2199
2209
|
if (pdmLockFiles && pdmLockFiles.length) {
|
|
2200
2210
|
poetryFiles = poetryFiles.concat(pdmLockFiles);
|
|
2201
2211
|
}
|
|
2202
|
-
|
|
2212
|
+
let reqFiles = getAllFiles(
|
|
2203
2213
|
path,
|
|
2204
2214
|
(options.multiProject ? "**/" : "") + "*requirements*.txt"
|
|
2205
2215
|
);
|
|
2216
|
+
reqFiles = reqFiles.filter(
|
|
2217
|
+
(f) => !f.includes(join("mercurial", "helptext", "internals"))
|
|
2218
|
+
);
|
|
2206
2219
|
const reqDirFiles = getAllFiles(
|
|
2207
2220
|
path,
|
|
2208
2221
|
(options.multiProject ? "**/" : "") + "requirements/*.txt"
|
|
@@ -3910,7 +3923,7 @@ export const mergeDependencies = (
|
|
|
3910
3923
|
for (const akey of Object.keys(deps_map)) {
|
|
3911
3924
|
retlist.push({
|
|
3912
3925
|
ref: akey,
|
|
3913
|
-
dependsOn: Array.from(deps_map[akey])
|
|
3926
|
+
dependsOn: Array.from(deps_map[akey]).sort()
|
|
3914
3927
|
});
|
|
3915
3928
|
}
|
|
3916
3929
|
return retlist;
|
|
@@ -4003,7 +4016,7 @@ export const createMultiXBom = async (pathList, options) => {
|
|
|
4003
4016
|
["docker", "oci", "container"].includes(options.projectType) &&
|
|
4004
4017
|
options.allLayersExplodedDir
|
|
4005
4018
|
) {
|
|
4006
|
-
const { osPackages, allTypes } = getOSPackages(
|
|
4019
|
+
const { osPackages, dependenciesList, allTypes } = getOSPackages(
|
|
4007
4020
|
options.allLayersExplodedDir
|
|
4008
4021
|
);
|
|
4009
4022
|
if (DEBUG_MODE) {
|
|
@@ -4018,6 +4031,17 @@ export const createMultiXBom = async (pathList, options) => {
|
|
|
4018
4031
|
componentsXmls = componentsXmls.concat(
|
|
4019
4032
|
listComponents(options, {}, osPackages, "", "xml")
|
|
4020
4033
|
);
|
|
4034
|
+
if (dependenciesList && dependenciesList.length) {
|
|
4035
|
+
dependencies = dependencies.concat(dependenciesList);
|
|
4036
|
+
}
|
|
4037
|
+
if (parentComponent && Object.keys(parentComponent).length) {
|
|
4038
|
+
// Make the parent oci image depend on all os components
|
|
4039
|
+
const parentDependsOn = new Set(osPackages.map((p) => p["bom-ref"]));
|
|
4040
|
+
dependencies.splice(0, 0, {
|
|
4041
|
+
ref: parentComponent["bom-ref"],
|
|
4042
|
+
dependsOn: Array.from(parentDependsOn).sort()
|
|
4043
|
+
});
|
|
4044
|
+
}
|
|
4021
4045
|
}
|
|
4022
4046
|
if (options.projectType === "os" && options.bomData) {
|
|
4023
4047
|
bomData = options.bomData;
|
|
@@ -4409,7 +4433,12 @@ export const createMultiXBom = async (pathList, options) => {
|
|
|
4409
4433
|
// Jar scanning is enabled by default
|
|
4410
4434
|
// See #330
|
|
4411
4435
|
bomData = createJarBom(path, options);
|
|
4412
|
-
if (
|
|
4436
|
+
if (
|
|
4437
|
+
bomData &&
|
|
4438
|
+
bomData.bomJson &&
|
|
4439
|
+
bomData.bomJson.components &&
|
|
4440
|
+
bomData.bomJson.components.length
|
|
4441
|
+
) {
|
|
4413
4442
|
if (DEBUG_MODE) {
|
|
4414
4443
|
console.log(
|
|
4415
4444
|
`Found ${bomData.bomJson.components.length} jar packages at ${path}`
|
|
@@ -4430,7 +4459,12 @@ export const createMultiXBom = async (pathList, options) => {
|
|
|
4430
4459
|
} // for
|
|
4431
4460
|
if (options.lastWorkingDir && options.lastWorkingDir !== "") {
|
|
4432
4461
|
bomData = createJarBom(options.lastWorkingDir, options);
|
|
4433
|
-
if (
|
|
4462
|
+
if (
|
|
4463
|
+
bomData &&
|
|
4464
|
+
bomData.bomJson &&
|
|
4465
|
+
bomData.bomJson.components &&
|
|
4466
|
+
bomData.bomJson.components.length
|
|
4467
|
+
) {
|
|
4434
4468
|
if (DEBUG_MODE) {
|
|
4435
4469
|
console.log(
|
|
4436
4470
|
`Found ${bomData.bomJson.components.length} jar packages at ${options.lastWorkingDir}`
|
|
@@ -4818,8 +4852,26 @@ export const createBom = async (path, options) => {
|
|
|
4818
4852
|
purl: "pkg:oci/" + inspectData.RepoDigests[0],
|
|
4819
4853
|
_integrity: inspectData.RepoDigests[0].replace("sha256:", "sha256-")
|
|
4820
4854
|
};
|
|
4855
|
+
options.parentComponent["bom-ref"] = options.parentComponent.purl;
|
|
4821
4856
|
}
|
|
4857
|
+
} else if (inspectData.Id) {
|
|
4858
|
+
options.parentComponent = {
|
|
4859
|
+
name: inspectData.RepoDigests[0].split("@")[0],
|
|
4860
|
+
version: inspectData.RepoDigests[0]
|
|
4861
|
+
.split("@")[1]
|
|
4862
|
+
.replace("sha256:", ""),
|
|
4863
|
+
type: "container",
|
|
4864
|
+
purl: "pkg:oci/" + inspectData.RepoDigests[0],
|
|
4865
|
+
_integrity: inspectData.RepoDigests[0].replace("sha256:", "sha256-")
|
|
4866
|
+
};
|
|
4867
|
+
options.parentComponent["bom-ref"] = options.parentComponent.purl;
|
|
4822
4868
|
}
|
|
4869
|
+
} else {
|
|
4870
|
+
options.parentComponent = createDefaultParentComponent(
|
|
4871
|
+
path,
|
|
4872
|
+
"container",
|
|
4873
|
+
options
|
|
4874
|
+
);
|
|
4823
4875
|
}
|
|
4824
4876
|
// Pass the entire export data about the image layers
|
|
4825
4877
|
options.exportData = exportData;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyclonedx/cdxgen",
|
|
3
|
-
"version": "9.6.
|
|
3
|
+
"version": "9.6.1",
|
|
4
4
|
"description": "Creates CycloneDX Software Bill-of-Materials (SBOM) from source or container image",
|
|
5
5
|
"homepage": "http://github.com/cyclonedx/cdxgen",
|
|
6
6
|
"author": "Prabhu Subramanian <prabhu@appthreat.com>",
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"url": "https://github.com/cyclonedx/cdxgen/issues"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@babel/parser": "^7.22.
|
|
57
|
-
"@babel/traverse": "^7.22.
|
|
56
|
+
"@babel/parser": "^7.22.11",
|
|
57
|
+
"@babel/traverse": "^7.22.11",
|
|
58
58
|
"ajv": "^8.12.0",
|
|
59
59
|
"ajv-formats": "^2.1.1",
|
|
60
60
|
"cheerio": "^1.0.0-rc.12",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"node-stream-zip": "^1.15.0",
|
|
69
69
|
"packageurl-js": "^1.0.2",
|
|
70
70
|
"prettify-xml": "^1.2.0",
|
|
71
|
-
"properties-reader": "^2.
|
|
71
|
+
"properties-reader": "^2.3.0",
|
|
72
72
|
"semver": "^7.5.3",
|
|
73
73
|
"ssri": "^10.0.4",
|
|
74
74
|
"table": "^6.8.1",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
},
|
|
81
81
|
"optionalDependencies": {
|
|
82
82
|
"@appthreat/atom": "^1.1.4",
|
|
83
|
-
"@cyclonedx/cdxgen-plugins-bin": "^1.
|
|
83
|
+
"@cyclonedx/cdxgen-plugins-bin": "^1.3.0",
|
|
84
84
|
"body-parser": "^1.20.2",
|
|
85
85
|
"compression": "^1.7.4",
|
|
86
86
|
"connect": "^3.7.0",
|
|
@@ -95,8 +95,8 @@
|
|
|
95
95
|
],
|
|
96
96
|
"devDependencies": {
|
|
97
97
|
"caxa": "^3.0.1",
|
|
98
|
-
"eslint": "^8.
|
|
99
|
-
"jest": "^29.
|
|
100
|
-
"prettier": "3.0.
|
|
98
|
+
"eslint": "^8.48.0",
|
|
99
|
+
"jest": "^29.6.4",
|
|
100
|
+
"prettier": "3.0.2"
|
|
101
101
|
}
|
|
102
102
|
}
|