@cyclonedx/cdxgen 9.3.2 → 9.5.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/utils.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { globSync } from "glob";
2
- import { tmpdir, platform, freemem } from "node:os";
2
+ import { homedir, tmpdir, platform, freemem } from "node:os";
3
3
  import {
4
4
  dirname,
5
5
  sep as _sep,
@@ -11,6 +11,7 @@ import {
11
11
  import {
12
12
  existsSync,
13
13
  readFileSync,
14
+ lstatSync,
14
15
  mkdtempSync,
15
16
  rmSync,
16
17
  copyFileSync,
@@ -26,19 +27,19 @@ let url = import.meta.url;
26
27
  if (!url.startsWith("file://")) {
27
28
  url = new URL(`file://${import.meta.url}`).toString();
28
29
  }
29
- const dirName = import.meta ? dirname(fileURLToPath(url)) : __dirname;
30
+ const dirNameStr = import.meta ? dirname(fileURLToPath(url)) : __dirname;
30
31
 
31
32
  const licenseMapping = JSON.parse(
32
- readFileSync(join(dirName, "data", "lic-mapping.json"))
33
+ readFileSync(join(dirNameStr, "data", "lic-mapping.json"))
33
34
  );
34
35
  const vendorAliases = JSON.parse(
35
- readFileSync(join(dirName, "data", "vendor-alias.json"))
36
+ readFileSync(join(dirNameStr, "data", "vendor-alias.json"))
36
37
  );
37
38
  const spdxLicenses = JSON.parse(
38
- readFileSync(join(dirName, "data", "spdx-licenses.json"))
39
+ readFileSync(join(dirNameStr, "data", "spdx-licenses.json"))
39
40
  );
40
41
  const knownLicenses = JSON.parse(
41
- readFileSync(join(dirName, "data", "known-licenses.json"))
42
+ readFileSync(join(dirNameStr, "data", "known-licenses.json"))
42
43
  );
43
44
  import { load } from "cheerio";
44
45
  import { load as _load } from "js-yaml";
@@ -49,18 +50,19 @@ import StreamZip from "node-stream-zip";
49
50
  import { parseEDNString } from "edn-data";
50
51
  import { PackageURL } from "packageurl-js";
51
52
  import { getTreeWithPlugin } from "./piptree.js";
53
+ import iconv from "iconv-lite";
52
54
 
53
- const selfPJson = JSON.parse(readFileSync(join(dirName, "package.json")));
55
+ const selfPJson = JSON.parse(readFileSync(join(dirNameStr, "package.json")));
54
56
  const _version = selfPJson.version;
55
57
 
56
58
  // Refer to contrib/py-modules.py for a script to generate this list
57
59
  // The script needs to be used once every few months to update this list
58
60
  const PYTHON_STD_MODULES = JSON.parse(
59
- readFileSync(join(dirName, "data", "python-stdlib.json"))
61
+ readFileSync(join(dirNameStr, "data", "python-stdlib.json"))
60
62
  );
61
63
  // Mapping between modules and package names
62
64
  const PYPI_MODULE_PACKAGE_MAPPING = JSON.parse(
63
- readFileSync(join(dirName, "data", "pypi-pkg-aliases.json"))
65
+ readFileSync(join(dirNameStr, "data", "pypi-pkg-aliases.json"))
64
66
  );
65
67
 
66
68
  // Debug mode flag
@@ -168,10 +170,24 @@ export function getLicenses(pkg, format = "xml") {
168
170
  } else if (l.startsWith("http")) {
169
171
  if (!l.includes("opensource.org")) {
170
172
  licenseContent.name = "CUSTOM";
173
+ } else {
174
+ const possibleId = l
175
+ .replace("http://www.opensource.org/licenses/", "")
176
+ .toUpperCase();
177
+ spdxLicenses.forEach((v) => {
178
+ if (v.toUpperCase() === possibleId) {
179
+ licenseContent.id = v;
180
+ }
181
+ });
171
182
  }
172
183
  if (l.includes("mit-license")) {
173
184
  licenseContent.id = "MIT";
174
185
  }
186
+ // We always need a name to avoid validation errors
187
+ // Issue: #469
188
+ if (!licenseContent.name && !licenseContent.id) {
189
+ licenseContent.name = "CUSTOM";
190
+ }
175
191
  licenseContent.url = l;
176
192
  } else {
177
193
  licenseContent.name = l;
@@ -497,12 +513,16 @@ export const parsePkgJson = async (pkgJsonFile) => {
497
513
  * Parse nodejs package lock file
498
514
  *
499
515
  * @param {string} pkgLockFile package-lock.json file
516
+ * @param {object} options Command line options
500
517
  */
501
- export const parsePkgLock = async (pkgLockFile) => {
518
+ export const parsePkgLock = async (pkgLockFile, options = {}) => {
502
519
  let pkgList = [];
503
520
  const dependenciesList = [];
504
521
  const depKeys = {};
505
522
  let rootPkg = {};
523
+ if (!options) {
524
+ options = {};
525
+ }
506
526
  if (existsSync(pkgLockFile)) {
507
527
  const lockData = JSON.parse(readFileSync(pkgLockFile, "utf8"));
508
528
  rootPkg.name = lockData.name || "";
@@ -510,16 +530,16 @@ export const parsePkgLock = async (pkgLockFile) => {
510
530
  if (lockData.name && lockData.packages && lockData.packages[""]) {
511
531
  // Build the initial dependency tree for the root package
512
532
  rootPkg = {
513
- group: "",
514
- name: lockData.name,
515
- version: lockData.version,
533
+ group: options.projectGroup || "",
534
+ name: options.projectName || lockData.name,
535
+ version: options.projectVersion || lockData.version,
516
536
  type: "application",
517
537
  "bom-ref": decodeURIComponent(
518
538
  new PackageURL(
519
539
  "npm",
520
- "",
521
- lockData.name,
522
- lockData.version,
540
+ options.projectGroup || "",
541
+ options.projectName || lockData.name,
542
+ options.projectVersion || lockData.version,
523
543
  null,
524
544
  null
525
545
  ).toString()
@@ -531,10 +551,10 @@ export const parsePkgLock = async (pkgLockFile) => {
531
551
  dirName = tmpA[tmpA.length - 1];
532
552
  // v1 lock file
533
553
  rootPkg = {
534
- group: "",
535
- name: lockData.name || dirName,
536
- version: lockData.version || "",
537
- type: "application"
554
+ group: options.projectGroup || "",
555
+ name: options.projectName || lockData.name || dirName,
556
+ version: options.projectVersion || lockData.version || "",
557
+ type: "npm"
538
558
  };
539
559
  }
540
560
  if (rootPkg && rootPkg.name) {
@@ -625,6 +645,7 @@ export const yarnLockToIdentMap = function (lockData) {
625
645
  const identMap = {};
626
646
  let currentIdents = [];
627
647
  lockData.split("\n").forEach((l) => {
648
+ l = l.replace("\r", "");
628
649
  if (l === "\n" || l.startsWith("#")) {
629
650
  return;
630
651
  }
@@ -687,6 +708,7 @@ export const parseYarnLock = async function (yarnLockFile) {
687
708
  const identMap = yarnLockToIdentMap(lockData);
688
709
  let prefixAtSymbol = false;
689
710
  lockData.split("\n").forEach((l) => {
711
+ l = l.replace("\r", "");
690
712
  if (l === "\n" || l.startsWith("#")) {
691
713
  return;
692
714
  }
@@ -1443,7 +1465,7 @@ export const parseGradleDep = function (
1443
1465
  }
1444
1466
  let stack = [last_purl];
1445
1467
  const depRegex =
1446
- /^.*?--- +(?<group>[^\s:]+):(?<name>[^\s:]+)(?::(?:{strictly [[]?)?(?<versionspecified>[^,\s:}]+))?(?:})?(?:[^->]* +-> +(?<versionoverride>[^\s:]+))?/gm;
1468
+ /^.*?--- +(?<groupspecified>[^\s:]+) ?:(?<namespecified>[^\s:]+)(?::(?:{strictly [[]?)?(?<versionspecified>[^,\s:}]+))?(?:})?(?:[^->]* +-> +(?:(?<groupoverride>[^\s:]+):(?<nameoverride>[^\s:]+):)?(?<versionoverride>[^\s:]+))?/gm;
1447
1469
  for (const rline of rawOutput.split("\n")) {
1448
1470
  if (!rline) {
1449
1471
  continue;
@@ -1461,16 +1483,9 @@ export const parseGradleDep = function (
1461
1483
  rline.startsWith("\\--- ")
1462
1484
  ) {
1463
1485
  last_level = 1;
1464
- if (rline.startsWith("+--- project :")) {
1465
- const tmpProj = rline.split("+--- project :");
1466
- last_project_purl = `pkg:maven/${tmpProj[1].trim()}@${rootProjectVersion}?type=jar`;
1467
- stack = [last_project_purl];
1468
- last_purl = last_project_purl;
1469
- } else {
1470
- last_project_purl = first_purl;
1471
- last_purl = last_project_purl;
1472
- stack = [first_purl];
1473
- }
1486
+ last_project_purl = first_purl;
1487
+ last_purl = last_project_purl;
1488
+ stack = [first_purl];
1474
1489
  }
1475
1490
  if (rline.includes(" - ")) {
1476
1491
  profileName = rline.split(" - ")[0];
@@ -1483,76 +1498,86 @@ export const parseGradleDep = function (
1483
1498
  }
1484
1499
  }
1485
1500
  while ((match = depRegex.exec(rline))) {
1486
- const [line, group, name, versionspecified, versionoverride] = match;
1501
+ const [
1502
+ line,
1503
+ groupspecified,
1504
+ namespecified,
1505
+ versionspecified,
1506
+ groupoverride,
1507
+ nameoverride,
1508
+ versionoverride
1509
+ ] = match;
1510
+ const group = groupoverride || groupspecified;
1511
+ const name = nameoverride || namespecified;
1487
1512
  const version = versionoverride || versionspecified;
1488
- const level = line.split(group)[0].length / 5;
1489
- if (version !== undefined) {
1513
+ const level = line.split(groupspecified)[0].length / 5;
1514
+ if (version !== undefined || group === "project") {
1490
1515
  let purlString = new PackageURL(
1491
1516
  "maven",
1492
- group,
1517
+ group !== "project" ? group : rootProjectGroup,
1493
1518
  name,
1494
- version,
1519
+ version !== undefined ? version : rootProjectVersion,
1495
1520
  { type: "jar" },
1496
1521
  null
1497
1522
  ).toString();
1498
1523
  purlString = decodeURIComponent(purlString);
1499
1524
  keys_cache[purlString + "_" + last_purl] = true;
1500
- if (group !== "project") {
1501
- // Filter duplicates
1502
- if (!deps_keys_cache[purlString]) {
1503
- deps_keys_cache[purlString] = true;
1504
- const adep = {
1505
- group,
1506
- name: name,
1507
- version: version,
1508
- qualifiers: { type: "jar" }
1509
- };
1510
- if (scope) {
1511
- adep["scope"] = scope;
1512
- }
1513
- if (profileName) {
1514
- adep.properties = [
1515
- {
1516
- name: "GradleProfileName",
1517
- value: profileName
1518
- }
1519
- ];
1520
- }
1521
- deps.push(adep);
1525
+ // Filter duplicates
1526
+ if (!deps_keys_cache[purlString]) {
1527
+ deps_keys_cache[purlString] = true;
1528
+ const adep = {
1529
+ group: group !== "project" ? group : rootProjectGroup,
1530
+ name: name,
1531
+ version: version !== undefined ? version : rootProjectVersion,
1532
+ qualifiers: { type: "jar" }
1533
+ };
1534
+ adep["purl"] = purlString;
1535
+ adep["bom-ref"] = purlString;
1536
+ if (scope) {
1537
+ adep["scope"] = scope;
1522
1538
  }
1523
- if (!level_trees[purlString]) {
1524
- level_trees[purlString] = [];
1539
+ if (profileName) {
1540
+ adep.properties = [
1541
+ {
1542
+ name: "GradleProfileName",
1543
+ value: profileName
1544
+ }
1545
+ ];
1525
1546
  }
1526
- if (level == 0) {
1527
- stack = [first_purl];
1528
- stack.push(purlString);
1529
- } else if (last_purl === "") {
1530
- stack.push(purlString);
1531
- } else if (level > last_level) {
1532
- const cnodes = level_trees[last_purl] || [];
1533
- if (!cnodes.includes(purlString)) {
1534
- cnodes.push(purlString);
1535
- }
1536
- level_trees[last_purl] = cnodes;
1537
- if (stack[stack.length - 1] !== purlString) {
1538
- stack.push(purlString);
1539
- }
1540
- } else {
1541
- for (let i = level; i <= last_level; i++) {
1542
- stack.pop();
1543
- }
1544
- const last_stack =
1545
- stack.length > 0 ? stack[stack.length - 1] : last_project_purl;
1546
- const cnodes = level_trees[last_stack] || [];
1547
- if (!cnodes.includes(purlString)) {
1548
- cnodes.push(purlString);
1549
- }
1550
- level_trees[last_stack] = cnodes;
1547
+ deps.push(adep);
1548
+ }
1549
+ if (!level_trees[purlString]) {
1550
+ level_trees[purlString] = [];
1551
+ }
1552
+ if (level == 0) {
1553
+ stack = [first_purl];
1554
+ stack.push(purlString);
1555
+ } else if (last_purl === "") {
1556
+ stack.push(purlString);
1557
+ } else if (level > last_level) {
1558
+ const cnodes = level_trees[last_purl] || [];
1559
+ if (!cnodes.includes(purlString)) {
1560
+ cnodes.push(purlString);
1561
+ }
1562
+ level_trees[last_purl] = cnodes;
1563
+ if (stack[stack.length - 1] !== purlString) {
1551
1564
  stack.push(purlString);
1552
1565
  }
1553
- last_level = level;
1554
- last_purl = purlString;
1566
+ } else {
1567
+ for (let i = level; i <= last_level; i++) {
1568
+ stack.pop();
1569
+ }
1570
+ const last_stack =
1571
+ stack.length > 0 ? stack[stack.length - 1] : last_project_purl;
1572
+ const cnodes = level_trees[last_stack] || [];
1573
+ if (!cnodes.includes(purlString)) {
1574
+ cnodes.push(purlString);
1575
+ }
1576
+ level_trees[last_stack] = cnodes;
1577
+ stack.push(purlString);
1555
1578
  }
1579
+ last_level = level;
1580
+ last_purl = purlString;
1556
1581
  }
1557
1582
  }
1558
1583
  }
@@ -1664,7 +1689,7 @@ export const parseGradleProjects = function (rawOutput) {
1664
1689
  if (typeof rawOutput === "string") {
1665
1690
  const tmpA = rawOutput.split("\n");
1666
1691
  tmpA.forEach((l) => {
1667
- l = l.replace("\r", "")
1692
+ l = l.replace("\r", "");
1668
1693
  if (l.startsWith("Root project ")) {
1669
1694
  rootProject = l
1670
1695
  .split("Root project ")[1]
@@ -1676,7 +1701,8 @@ export const parseGradleProjects = function (rawOutput) {
1676
1701
  const projName = tmpB[1].split(" ")[0].replace(/'/g, "");
1677
1702
  // Include all projects including test projects
1678
1703
  if (projName.startsWith(":")) {
1679
- projects.add(projName);
1704
+ // Handle the case where the project name could have a space. Eg: +--- project :app (*)
1705
+ projects.add(projName.split(" ")[0]);
1680
1706
  }
1681
1707
  }
1682
1708
  } else if (l.includes("--- project ")) {
@@ -1684,7 +1710,7 @@ export const parseGradleProjects = function (rawOutput) {
1684
1710
  if (tmpB && tmpB.length > 1) {
1685
1711
  const projName = tmpB[1];
1686
1712
  if (projName.startsWith(":")) {
1687
- projects.add(projName);
1713
+ projects.add(projName.split(" ")[0]);
1688
1714
  }
1689
1715
  }
1690
1716
  }
@@ -1726,7 +1752,7 @@ export const parseGradleProperties = function (rawOutput) {
1726
1752
  const spStrs = tmpB[1].replace(/[[\]']/g, "").split(", ");
1727
1753
  const tmpprojects = spStrs
1728
1754
  .flatMap((s) => s.replace("project ", ""))
1729
- .filter((s) => ![":app", ""].includes(s.trim()));
1755
+ .filter((s) => ![""].includes(s.trim()));
1730
1756
  tmpprojects.forEach(projects.add, projects);
1731
1757
  }
1732
1758
  }
@@ -1893,6 +1919,7 @@ export const parseKVDep = function (rawOutput) {
1893
1919
  if (typeof rawOutput === "string") {
1894
1920
  const deps = [];
1895
1921
  rawOutput.split("\n").forEach((l) => {
1922
+ l = l.replace("\r", "");
1896
1923
  const tmpA = l.split(":");
1897
1924
  if (tmpA.length === 3) {
1898
1925
  deps.push({
@@ -2331,6 +2358,9 @@ export const parsePyProjectToml = (tomlFile) => {
2331
2358
  pkg.name = value;
2332
2359
  break;
2333
2360
  case "version":
2361
+ if (value.includes("{")) {
2362
+ value = "latest";
2363
+ }
2334
2364
  pkg.version = value;
2335
2365
  break;
2336
2366
  case "authors":
@@ -2365,6 +2395,7 @@ export const parsePoetrylockData = async function (lockData, lockFile) {
2365
2395
  return pkgList;
2366
2396
  }
2367
2397
  lockData.split("\n").forEach((l) => {
2398
+ l = l.replace("\r", "");
2368
2399
  let key = null;
2369
2400
  let value = null;
2370
2401
  // Package section starts with this marker
@@ -2420,6 +2451,7 @@ export async function parseReqFile(reqData, fetchDepsInfo) {
2420
2451
  const pkgList = [];
2421
2452
  let compScope = undefined;
2422
2453
  reqData.split("\n").forEach((l) => {
2454
+ l = l.replace("\r", "");
2423
2455
  l = l.trim();
2424
2456
  if (l.startsWith("Skipping line") || l.startsWith("(add")) {
2425
2457
  return;
@@ -3146,6 +3178,7 @@ export const parseGemfileLockData = async function (gemLockData) {
3146
3178
  let specsFound = false;
3147
3179
  gemLockData.split("\n").forEach((l) => {
3148
3180
  l = l.trim();
3181
+ l = l.replace("\r", "");
3149
3182
  if (specsFound) {
3150
3183
  const tmpA = l.split(" ");
3151
3184
  if (tmpA && tmpA.length == 2) {
@@ -3358,6 +3391,7 @@ export const parseCargoData = async function (cargoData) {
3358
3391
  cargoData.split("\n").forEach((l) => {
3359
3392
  let key = null;
3360
3393
  let value = null;
3394
+ l = l.replace("\r", "");
3361
3395
  // Ignore version = 3 found at the top of newer lock files
3362
3396
  if (!pkg && l.startsWith("version =")) {
3363
3397
  return;
@@ -3402,6 +3436,7 @@ export const parseCargoAuditableData = async function (cargoData) {
3402
3436
  return pkgList;
3403
3437
  }
3404
3438
  cargoData.split("\n").forEach((l) => {
3439
+ l = l.replace("\r", "");
3405
3440
  const tmpA = l.split("\t");
3406
3441
  if (tmpA && tmpA.length > 2) {
3407
3442
  let group = dirname(tmpA[0].trim());
@@ -3433,6 +3468,7 @@ export const parsePubLockData = async function (pubLockData) {
3433
3468
  pubLockData.split("\n").forEach((l) => {
3434
3469
  let key = null;
3435
3470
  let value = null;
3471
+ l = l.replace("\r", "");
3436
3472
  if (!pkg && (l.startsWith("sdks:") || !l.startsWith(" "))) {
3437
3473
  return;
3438
3474
  }
@@ -3848,6 +3884,7 @@ export const parseCabalData = function (cabalData) {
3848
3884
  if (!l.includes(" ==")) {
3849
3885
  return;
3850
3886
  }
3887
+ l = l.replace("\r", "");
3851
3888
  if (l.includes(" ==")) {
3852
3889
  const tmpA = l.split(" ==");
3853
3890
  const name = tmpA[0]
@@ -3875,6 +3912,7 @@ export const parseMixLockData = function (mixData) {
3875
3912
  if (!l.includes(":hex")) {
3876
3913
  return;
3877
3914
  }
3915
+ l = l.replace("\r", "");
3878
3916
  if (l.includes(":hex")) {
3879
3917
  const tmpA = l.split(",");
3880
3918
  if (tmpA.length > 3) {
@@ -4099,13 +4137,19 @@ export const parseEdnData = function (rawEdnData) {
4099
4137
  };
4100
4138
 
4101
4139
  export const parseNupkg = async function (nupkgFile) {
4102
- const pkgList = [];
4103
- const pkg = { group: "" };
4104
4140
  let nuspecData = await readZipEntry(nupkgFile, ".nuspec");
4105
- // Remove byte order mark
4106
- if (nuspecData.charCodeAt(0) === 0xfeff) {
4107
- nuspecData = nuspecData.slice(1);
4141
+ if (!nuspecData) {
4142
+ return [];
4108
4143
  }
4144
+ if (nuspecData.charCodeAt(0) === 65533) {
4145
+ nuspecData = await readZipEntry(nupkgFile, ".nuspec", "ucs2");
4146
+ }
4147
+ return await parseNuspecData(nupkgFile, nuspecData);
4148
+ };
4149
+
4150
+ export const parseNuspecData = async function (nupkgFile, nuspecData) {
4151
+ const pkgList = [];
4152
+ const pkg = { group: "" };
4109
4153
  let npkg = undefined;
4110
4154
  try {
4111
4155
  npkg = xml2js(nuspecData, {
@@ -4117,9 +4161,13 @@ export const parseNupkg = async function (nupkgFile) {
4117
4161
  commentKey: "value"
4118
4162
  }).package;
4119
4163
  } catch (e) {
4120
- // If we are parsing with invalid encoding unicode replacement character is used
4164
+ // If we are parsing with invalid encoding, unicode replacement character is used
4121
4165
  if (nuspecData.charCodeAt(0) === 65533) {
4122
4166
  console.log(`Unable to parse ${nupkgFile} in utf-8 mode`);
4167
+ } else {
4168
+ console.log(
4169
+ "Unable to parse this package. Tried utf-8 and ucs2 encoding."
4170
+ );
4123
4171
  }
4124
4172
  }
4125
4173
  if (!npkg) {
@@ -4832,48 +4880,100 @@ export const parseSwiftResolved = (resolvedFile) => {
4832
4880
  *
4833
4881
  * @param {string} mavenCmd Maven command to use
4834
4882
  * @param {string} basePath Path to the maven project
4883
+ * @param {boolean} cleanup Remove temporary directories
4884
+ * @param {boolean} includeCacheDir Include maven and gradle cache directories
4835
4885
  */
4836
- export const collectMvnDependencies = function (mavenCmd, basePath) {
4837
- const tempDir = mkdtempSync(join(tmpdir(), "mvn-deps-"));
4838
- console.log(
4839
- `Executing 'mvn dependency:copy-dependencies -DoutputDirectory=${tempDir} -DexcludeTransitive=true -DincludeScope=runtime' in ${basePath}`
4840
- );
4841
- const result = spawnSync(
4842
- mavenCmd,
4843
- [
4844
- "dependency:copy-dependencies",
4845
- `-DoutputDirectory=${tempDir}`,
4846
- "-DexcludeTransitive=true",
4847
- "-DincludeScope=runtime",
4848
- "-U",
4849
- "-Dmdep.prependGroupId=" + (process.env.MAVEN_PREPEND_GROUP || "false"),
4850
- "-Dmdep.stripVersion=" + (process.env.MAVEN_STRIP_VERSION || "false")
4851
- ],
4852
- { cwd: basePath, encoding: "utf-8" }
4853
- );
4886
+ export const collectMvnDependencies = function (
4887
+ mavenCmd,
4888
+ basePath,
4889
+ cleanup = true,
4890
+ includeCacheDir = false
4891
+ ) {
4854
4892
  let jarNSMapping = {};
4855
- if (result.status !== 0 || result.error) {
4856
- console.error(result.stdout, result.stderr);
4857
- console.log(
4858
- "Resolve the above maven error. You can try the following remediation tips:\n"
4859
- );
4893
+ const MAVEN_CACHE_DIR =
4894
+ process.env.MAVEN_CACHE_DIR || join(homedir(), ".m2", "repository");
4895
+ const tempDir = mkdtempSync(join(tmpdir(), "mvn-deps-"));
4896
+ const copyArgs = [
4897
+ "dependency:copy-dependencies",
4898
+ `-DoutputDirectory=${tempDir}`,
4899
+ "-U",
4900
+ "-Dmdep.copyPom=true",
4901
+ "-Dmdep.useRepositoryLayout=true",
4902
+ "-Dmdep.includeScope=compile",
4903
+ "-Dmdep.prependGroupId=" + (process.env.MAVEN_PREPEND_GROUP || "false"),
4904
+ "-Dmdep.stripVersion=" + (process.env.MAVEN_STRIP_VERSION || "false")
4905
+ ];
4906
+ if (basePath && basePath !== MAVEN_CACHE_DIR) {
4860
4907
  console.log(
4861
- "1. Check if the correct version of maven is installed and available in the PATH."
4908
+ `Executing '${mavenCmd} dependency:copy-dependencies ${copyArgs.join(
4909
+ " "
4910
+ )}' in ${basePath}`
4862
4911
  );
4863
- console.log(
4864
- "2. Perform 'mvn compile package' before invoking this command. Fix any errors found during this invocation."
4912
+ const result = spawnSync(mavenCmd, copyArgs, {
4913
+ cwd: basePath,
4914
+ encoding: "utf-8"
4915
+ });
4916
+ if (result.status !== 0 || result.error) {
4917
+ console.error(result.stdout, result.stderr);
4918
+ console.log(
4919
+ "Resolve the above maven error. You can try the following remediation tips:\n"
4920
+ );
4921
+ console.log(
4922
+ "1. Check if the correct version of maven is installed and available in the PATH."
4923
+ );
4924
+ console.log(
4925
+ "2. Perform 'mvn compile package' before invoking this command. Fix any errors found during this invocation."
4926
+ );
4927
+ console.log(
4928
+ "3. Ensure the temporary directory is available and has sufficient disk space to copy all the artifacts."
4929
+ );
4930
+ } else {
4931
+ jarNSMapping = collectJarNS(tempDir);
4932
+ }
4933
+ }
4934
+ if (includeCacheDir || basePath === MAVEN_CACHE_DIR) {
4935
+ // slow operation
4936
+ jarNSMapping = collectJarNS(MAVEN_CACHE_DIR);
4937
+ }
4938
+
4939
+ // Clean up
4940
+ if (cleanup && tempDir && tempDir.startsWith(tmpdir()) && rmSync) {
4941
+ rmSync(tempDir, { recursive: true, force: true });
4942
+ }
4943
+ return jarNSMapping;
4944
+ };
4945
+
4946
+ export const collectGradleDependencies = (
4947
+ gradleCmd,
4948
+ basePath,
4949
+ cleanup = true, // eslint-disable-line no-unused-vars
4950
+ includeCacheDir = false // eslint-disable-line no-unused-vars
4951
+ ) => {
4952
+ // HELP WANTED: We need an init script that mimics maven copy-dependencies that only collects the project specific jars and poms
4953
+ // Construct gradle cache directory
4954
+ let GRADLE_CACHE_DIR =
4955
+ process.env.GRADLE_CACHE_DIR ||
4956
+ join(homedir(), ".gradle", "caches", "modules-2", "files-2.1");
4957
+ if (process.env.GRADLE_USER_HOME) {
4958
+ GRADLE_CACHE_DIR = join(
4959
+ process.env.GRADLE_USER_HOME,
4960
+ "caches",
4961
+ "modules-2",
4962
+ "files-2.1"
4865
4963
  );
4964
+ }
4965
+ if (DEBUG_MODE) {
4966
+ console.log("Collecting jars from", GRADLE_CACHE_DIR);
4866
4967
  console.log(
4867
- "3. Ensure the temporary directory is available and has sufficient disk space to copy all the artifacts."
4968
+ "To improve performance, ensure only the project dependencies are present in this cache location."
4868
4969
  );
4869
- } else {
4870
- jarNSMapping = collectJarNS(tempDir);
4871
4970
  }
4872
- // Clean up
4873
- if (tempDir && tempDir.startsWith(tmpdir()) && rmSync) {
4874
- console.log(`Cleaning up ${tempDir}`);
4875
- rmSync(tempDir, { recursive: true, force: true });
4971
+ const pomPathMap = {};
4972
+ const pomFiles = getAllFiles(GRADLE_CACHE_DIR, "**/*.pom");
4973
+ for (const apom of pomFiles) {
4974
+ pomPathMap[basename(apom)] = apom;
4876
4975
  }
4976
+ const jarNSMapping = collectJarNS(GRADLE_CACHE_DIR, pomPathMap);
4877
4977
  return jarNSMapping;
4878
4978
  };
4879
4979
 
@@ -4881,10 +4981,11 @@ export const collectMvnDependencies = function (mavenCmd, basePath) {
4881
4981
  * Method to collect class names from all jars in a directory
4882
4982
  *
4883
4983
  * @param {string} jarPath Path containing jars
4984
+ * @param {object} pomPathMap Map containing jar to pom names. Required to successful parse gradle cache.
4884
4985
  *
4885
4986
  * @return object containing jar name and class list
4886
4987
  */
4887
- export const collectJarNS = function (jarPath) {
4988
+ export const collectJarNS = function (jarPath, pomPathMap = {}) {
4888
4989
  const jarNSMapping = {};
4889
4990
  console.log(
4890
4991
  `About to identify class names for all jars in the path ${jarPath}`
@@ -4893,31 +4994,72 @@ export const collectJarNS = function (jarPath) {
4893
4994
  const jarFiles = getAllFiles(jarPath, "**/*.jar");
4894
4995
  if (jarFiles && jarFiles.length) {
4895
4996
  for (const jf of jarFiles) {
4896
- const jarname = basename(jf);
4997
+ const jarname = jf;
4998
+ const pomname =
4999
+ pomPathMap[basename(jf).replace(".jar", ".pom")] ||
5000
+ jarname.replace(".jar", ".pom");
5001
+ let pomData = undefined;
5002
+ let purl = undefined;
5003
+ if (existsSync(pomname)) {
5004
+ pomData = parsePomXml(readFileSync(pomname, "utf-8"));
5005
+ if (pomData) {
5006
+ const purlObj = new PackageURL(
5007
+ "maven",
5008
+ pomData.groupId || "",
5009
+ pomData.artifactId,
5010
+ pomData.version,
5011
+ { type: "jar" },
5012
+ null
5013
+ );
5014
+ purl = purlObj.toString();
5015
+ }
5016
+ } else if (jf.includes(join(".gradle", "caches"))) {
5017
+ // Let's try our best to construct a purl for gradle cache entries of the form
5018
+ // .gradle/caches/modules-2/files-2.1/org.xmlresolver/xmlresolver/4.2.0/f4dbdaa83d636dcac91c9003ffa7fb173173fe8d/xmlresolver-4.2.0-data.jar
5019
+ const tmpA = jf.split(join("files-2.1", ""));
5020
+ if (tmpA && tmpA.length) {
5021
+ let tmpJarPath = tmpA[tmpA.length - 1];
5022
+ // This would yield xmlresolver-4.2.0-data.jar
5023
+ const jarFileName = basename(tmpJarPath);
5024
+ let tmpDirParts = dirname(tmpJarPath).split(_sep);
5025
+ // This would remove the hash from the end of the directory name
5026
+ tmpDirParts.pop();
5027
+ // Retrieve the version
5028
+ const jarVersion = tmpDirParts.pop();
5029
+ // The result would form the group name
5030
+ const jarGroupName = tmpDirParts.join(".").replace(/^\./, "");
5031
+ const purlObj = new PackageURL(
5032
+ "maven",
5033
+ jarGroupName,
5034
+ jarFileName.replace(`-${jarVersion}`, ""),
5035
+ jarVersion,
5036
+ { type: "jar" },
5037
+ null
5038
+ );
5039
+ purl = purlObj.toString();
5040
+ }
5041
+ }
4897
5042
  if (DEBUG_MODE) {
4898
5043
  console.log(`Executing 'jar tf ${jf}'`);
4899
5044
  }
4900
5045
  const jarResult = spawnSync("jar", ["-tf", jf], { encoding: "utf-8" });
4901
- if (jarResult.status !== 0) {
4902
- console.error(jarResult.stdout, jarResult.stderr);
4903
- console.log(
4904
- "Check if JRE is installed and the jar command is available in the PATH."
4905
- );
4906
- break;
4907
- } else {
4908
- const consolelines = (jarResult.stdout || "").split("\n");
4909
- const nsList = consolelines
4910
- .filter((l) => {
4911
- return l.includes(".class") && !l.includes("-INF");
4912
- })
4913
- .map((e) => {
4914
- return e
4915
- .replace(/\/$/, "")
4916
- .replace(/\//g, ".")
4917
- .replace(".class", "");
4918
- });
4919
- jarNSMapping[jarname] = nsList;
4920
- }
5046
+ const consolelines = (jarResult.stdout || "").split("\n");
5047
+ const nsList = consolelines
5048
+ .filter((l) => {
5049
+ return (
5050
+ l.includes(".class") &&
5051
+ !l.includes("-INF") &&
5052
+ !l.includes("module-info")
5053
+ );
5054
+ })
5055
+ .map((e) => {
5056
+ return e.replace(".class", "").replace(/\/$/, "").replace(/\//g, ".");
5057
+ });
5058
+ jarNSMapping[purl || jf] = {
5059
+ jarFile: jf,
5060
+ pom: pomData,
5061
+ namespaces: nsList
5062
+ };
4921
5063
  }
4922
5064
  if (!jarNSMapping) {
4923
5065
  console.log(`Unable to determine class names for the jars in ${jarPath}`);
@@ -4925,12 +5067,69 @@ export const collectJarNS = function (jarPath) {
4925
5067
  } else {
4926
5068
  console.log(`${jarPath} did not contain any jars.`);
4927
5069
  }
4928
- if (DEBUG_MODE) {
4929
- console.log("JAR Namespace mapping", jarNSMapping);
4930
- }
4931
5070
  return jarNSMapping;
4932
5071
  };
4933
5072
 
5073
+ export const convertJarNSToPackages = (jarNSMapping) => {
5074
+ let pkgList = [];
5075
+ for (const purl of Object.keys(jarNSMapping)) {
5076
+ let { jarFile, pom, namespaces } = jarNSMapping[purl];
5077
+ if (!pom) {
5078
+ pom = {};
5079
+ }
5080
+ let purlObj = undefined;
5081
+ try {
5082
+ purlObj = PackageURL.fromString(purl);
5083
+ } catch (e) {
5084
+ // ignore
5085
+ purlObj = {};
5086
+ }
5087
+ const name = pom.artifactId || purlObj.name;
5088
+ if (!name) {
5089
+ continue;
5090
+ }
5091
+ const apackage = {
5092
+ name,
5093
+ group: pom.groupId || purlObj.namespace || "",
5094
+ version: pom.version || purlObj.version,
5095
+ description: (pom.description || "").trim(),
5096
+ purl,
5097
+ "bom-ref": purl,
5098
+ evidence: {
5099
+ identity: {
5100
+ field: "purl",
5101
+ confidence: 0.5,
5102
+ methods: [
5103
+ {
5104
+ technique: "filename",
5105
+ confidence: 1,
5106
+ value: jarFile
5107
+ }
5108
+ ]
5109
+ }
5110
+ },
5111
+ properties: [
5112
+ {
5113
+ name: "SrcFile",
5114
+ value: jarFile
5115
+ },
5116
+ {
5117
+ name: "Namespaces",
5118
+ value: namespaces.join("\n")
5119
+ }
5120
+ ]
5121
+ };
5122
+ if (pom.url) {
5123
+ apackage["homepage"] = { url: pom.url };
5124
+ }
5125
+ if (pom.scm) {
5126
+ apackage["repository"] = { url: pom.scm };
5127
+ }
5128
+ pkgList.push(apackage);
5129
+ }
5130
+ return pkgList;
5131
+ };
5132
+
4934
5133
  export const parsePomXml = function (pomXmlData) {
4935
5134
  if (!pomXmlData) {
4936
5135
  return undefined;
@@ -4969,6 +5168,7 @@ export const parseJarManifest = function (jarMetadata) {
4969
5168
  return metadata;
4970
5169
  }
4971
5170
  jarMetadata.split("\n").forEach((l) => {
5171
+ l = l.replace("\r", "");
4972
5172
  if (l.includes(": ")) {
4973
5173
  const tmpA = l.split(": ");
4974
5174
  if (tmpA && tmpA.length === 2) {
@@ -4999,10 +5199,19 @@ export const extractJarArchive = function (jarFile, tempDir) {
4999
5199
  const fname = basename(jarFile);
5000
5200
  let pomname = undefined;
5001
5201
  // If there is a pom file in the same directory, try to use it
5202
+ let manifestname = join(dirname(jarFile), "META-INF", "MANIFEST.MF");
5203
+ // Issue 439: Current implementation checks for existance of a .pom file, but .pom file is not used.
5204
+ // Instead code expects to find META-INF/MANIFEST.MF in the same folder as a .jar file.
5205
+ // For now check for presence of both .pom and MANIFEST.MF files.
5002
5206
  if (jarFile.endsWith(".jar")) {
5003
5207
  pomname = jarFile.replace(".jar", ".pom");
5004
5208
  }
5005
- if (pomname && existsSync(pomname)) {
5209
+ if (
5210
+ pomname &&
5211
+ existsSync(pomname) &&
5212
+ manifestname &&
5213
+ existsSync(manifestname)
5214
+ ) {
5006
5215
  tempDir = dirname(jarFile);
5007
5216
  } else if (!existsSync(join(tempDir, fname))) {
5008
5217
  // Only copy if the file doesn't exist
@@ -5264,10 +5473,15 @@ export const sbtPluginsPath = function (projectPath) {
5264
5473
  *
5265
5474
  * @param {string} zipFile Zip file to read
5266
5475
  * @param {string} filePattern File pattern
5476
+ * @param {string} contentEncoding Encoding. Defaults to utf-8
5267
5477
  *
5268
5478
  * @returns File contents
5269
5479
  */
5270
- export const readZipEntry = async function (zipFile, filePattern) {
5480
+ export const readZipEntry = async function (
5481
+ zipFile,
5482
+ filePattern,
5483
+ contentEncoding = "utf-8"
5484
+ ) {
5271
5485
  let retData = undefined;
5272
5486
  try {
5273
5487
  const zip = new StreamZip.async({ file: zipFile });
@@ -5282,7 +5496,7 @@ export const readZipEntry = async function (zipFile, filePattern) {
5282
5496
  }
5283
5497
  if (entry.name.endsWith(filePattern)) {
5284
5498
  const fileData = await zip.entryData(entry.name);
5285
- retData = Buffer.from(fileData).toString();
5499
+ retData = iconv.decode(Buffer.from(fileData), contentEncoding);
5286
5500
  break;
5287
5501
  }
5288
5502
  }
@@ -5389,7 +5603,7 @@ export const getAtomCommand = () => {
5389
5603
  }
5390
5604
  const NODE_CMD = process.env.NODE_CMD || "node";
5391
5605
  const localAtom = join(
5392
- dirName,
5606
+ dirNameStr,
5393
5607
  "node_modules",
5394
5608
  "@appthreat",
5395
5609
  "atom",
@@ -5402,6 +5616,8 @@ export const getAtomCommand = () => {
5402
5616
  };
5403
5617
 
5404
5618
  export const executeAtom = (src, args) => {
5619
+ let cwd =
5620
+ existsSync(src) && lstatSync(src).isDirectory() ? src : dirname(src);
5405
5621
  let ATOM_BIN = getAtomCommand();
5406
5622
  if (ATOM_BIN.includes(" ")) {
5407
5623
  const tmpA = ATOM_BIN.split(" ");
@@ -5419,7 +5635,7 @@ export const executeAtom = (src, args) => {
5419
5635
  JAVA_OPTS: `-Xms${freeMemoryGB}G -Xmx${freeMemoryGB}G`
5420
5636
  };
5421
5637
  const result = spawnSync(ATOM_BIN, args, {
5422
- cwd: src,
5638
+ cwd,
5423
5639
  encoding: "utf-8",
5424
5640
  timeout: TIMEOUT_MS,
5425
5641
  env
@@ -5548,7 +5764,11 @@ export const getPipFrozenTree = (basePath, reqOrSetupFile, tempVenvDir) => {
5548
5764
  *
5549
5765
  * By checking the environment variable "VIRTUAL_ENV" we decide whether to create an env or not
5550
5766
  */
5551
- if (!process.env.VIRTUAL_ENV) {
5767
+ if (
5768
+ !process.env.VIRTUAL_ENV &&
5769
+ reqOrSetupFile &&
5770
+ !reqOrSetupFile.endsWith("poetry.lock")
5771
+ ) {
5552
5772
  result = spawnSync(PYTHON_CMD, ["-m", "venv", tempVenvDir], {
5553
5773
  encoding: "utf-8"
5554
5774
  });
@@ -5562,29 +5782,43 @@ export const getPipFrozenTree = (basePath, reqOrSetupFile, tempVenvDir) => {
5562
5782
  }
5563
5783
  }
5564
5784
  } else {
5785
+ if (DEBUG_MODE) {
5786
+ console.log(join("Using virtual environment in ", tempVenvDir));
5787
+ }
5565
5788
  env.VIRTUAL_ENV = tempVenvDir;
5566
5789
  env.PATH = `${join(
5567
5790
  tempVenvDir,
5568
- platform() == "win32" ? "Scripts" : "bin"
5791
+ platform() === "win32" ? "Scripts" : "bin"
5569
5792
  )}${_delimiter}${process.env.PATH || ""}`;
5570
5793
  }
5571
5794
  }
5572
5795
  /**
5573
5796
  * We now have a virtual environment so we can attempt to install the project and perform
5574
5797
  * pip freeze to collect the packages that got installed.
5798
+ * Note that we did not create a virtual environment for poetry because poetry will do this when we run the install.
5575
5799
  * This step is accurate but not reproducible since the resulting list could differ based on various factors
5576
5800
  * such as the version of python, pip, os, pypi.org availability (and weather?)
5577
5801
  */
5578
5802
  // Bug #388. Perform pip install in all virtualenv to make the experience consistent
5579
5803
  if (reqOrSetupFile) {
5580
5804
  if (reqOrSetupFile.endsWith("poetry.lock")) {
5581
- let poetryInstallArgs = ["-m", "poetry", "install", "-n", "--no-root"];
5805
+ let poetryConfigArgs = [
5806
+ "config",
5807
+ "virtualenvs.options.no-setuptools",
5808
+ "true",
5809
+ "--local"
5810
+ ];
5811
+ result = spawnSync("poetry", poetryConfigArgs, {
5812
+ cwd: basePath,
5813
+ encoding: "utf-8",
5814
+ timeout: TIMEOUT_MS
5815
+ });
5816
+ let poetryInstallArgs = ["install", "-n", "--no-root"];
5582
5817
  // Attempt to perform poetry install
5583
- result = spawnSync(PYTHON_CMD, poetryInstallArgs, {
5818
+ result = spawnSync("poetry", poetryInstallArgs, {
5584
5819
  cwd: basePath,
5585
5820
  encoding: "utf-8",
5586
- timeout: TIMEOUT_MS,
5587
- env
5821
+ timeout: TIMEOUT_MS
5588
5822
  });
5589
5823
  if (result.status !== 0 || result.error) {
5590
5824
  if (result.stderr && result.stderr.includes("No module named poetry")) {
@@ -5597,6 +5831,9 @@ export const getPipFrozenTree = (basePath, reqOrSetupFile, tempVenvDir) => {
5597
5831
  env
5598
5832
  });
5599
5833
  if (result.status !== 0 || result.error) {
5834
+ if (DEBUG_MODE && result.stderr) {
5835
+ console.log(result.stderr);
5836
+ }
5600
5837
  console.log("poetry install has failed.");
5601
5838
  console.log(
5602
5839
  "1. Install the poetry command using python -m pip install poetry."
@@ -5610,11 +5847,27 @@ export const getPipFrozenTree = (basePath, reqOrSetupFile, tempVenvDir) => {
5610
5847
  }
5611
5848
  } else {
5612
5849
  console.log(
5613
- "poetry install has failed. Setup and activate the poetry virtual environment and re-run cdxgen."
5850
+ "Poetry install has failed. Setup and activate the poetry virtual environment and re-run cdxgen."
5614
5851
  );
5615
5852
  }
5853
+ } else {
5854
+ let poetryEnvArgs = ["env info", "--path"];
5855
+ result = spawnSync("poetry", poetryEnvArgs, {
5856
+ cwd: basePath,
5857
+ encoding: "utf-8",
5858
+ timeout: TIMEOUT_MS,
5859
+ env
5860
+ });
5861
+ tempVenvDir = result.stdout.replaceAll(/[\r\n]+/g, "");
5862
+ if (tempVenvDir && tempVenvDir.length) {
5863
+ env.VIRTUAL_ENV = tempVenvDir;
5864
+ env.PATH = `${join(
5865
+ tempVenvDir,
5866
+ platform() === "win32" ? "Scripts" : "bin"
5867
+ )}${_delimiter}${process.env.PATH || ""}`;
5868
+ }
5616
5869
  }
5617
- } else if (!reqOrSetupFile.endsWith(".lock")) {
5870
+ } else {
5618
5871
  const pipInstallArgs = [
5619
5872
  "-m",
5620
5873
  "pip",
@@ -5659,7 +5912,7 @@ export const getPipFrozenTree = (basePath, reqOrSetupFile, tempVenvDir) => {
5659
5912
  console.log(
5660
5913
  "Possible build errors detected. The resulting list in the SBoM would therefore be incomplete.\nTry installing any missing build tools or development libraries to improve the accuracy."
5661
5914
  );
5662
- if (platform() == "win32") {
5915
+ if (platform() === "win32") {
5663
5916
  console.log(
5664
5917
  "- Install the appropriate compilers and build tools on Windows by following this documentation - https://wiki.python.org/moin/WindowsCompilers"
5665
5918
  );
@@ -5703,28 +5956,32 @@ export const getPipFrozenTree = (basePath, reqOrSetupFile, tempVenvDir) => {
5703
5956
  continue;
5704
5957
  }
5705
5958
  const name = t.name.replace(/_/g, "-").toLowerCase();
5706
- pkgList.push({
5707
- name,
5708
- version: t.version,
5709
- evidence: {
5710
- identity: {
5711
- field: "purl",
5712
- confidence: 1,
5713
- methods: [
5714
- {
5715
- technique: "instrumentation",
5716
- confidence: 1,
5717
- value: env.VIRTUAL_ENV
5718
- }
5719
- ]
5959
+ const version = t.version;
5960
+ let exclude = ["pip", "setuptools", "wheel"];
5961
+ if (!exclude.includes(name)) {
5962
+ pkgList.push({
5963
+ name,
5964
+ version,
5965
+ evidence: {
5966
+ identity: {
5967
+ field: "purl",
5968
+ confidence: 1,
5969
+ methods: [
5970
+ {
5971
+ technique: "instrumentation",
5972
+ confidence: 1,
5973
+ value: env.VIRTUAL_ENV
5974
+ }
5975
+ ]
5976
+ }
5720
5977
  }
5721
- }
5722
- });
5723
- rootList.push({
5724
- name,
5725
- version: t.version
5726
- });
5727
- flattenDeps(dependenciesMap, pkgList, reqOrSetupFile, t);
5978
+ });
5979
+ rootList.push({
5980
+ name,
5981
+ version
5982
+ });
5983
+ flattenDeps(dependenciesMap, pkgList, reqOrSetupFile, t);
5984
+ }
5728
5985
  } // end for
5729
5986
  for (const k of Object.keys(dependenciesMap)) {
5730
5987
  dependenciesList.push({ ref: k, dependsOn: dependenciesMap[k] });