@cyclonedx/cdxgen 12.1.2 → 12.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +11 -9
  2. package/bin/cdxgen.js +1 -1
  3. package/lib/cli/index.js +9 -5
  4. package/lib/evinser/evinser.js +2 -8
  5. package/lib/helpers/display.js +1 -1
  6. package/lib/helpers/envcontext.js +10 -2
  7. package/lib/helpers/utils.js +462 -86
  8. package/lib/helpers/utils.poku.js +179 -2
  9. package/lib/helpers/validator.js +8 -5
  10. package/lib/managers/docker.getConnection.poku.js +61 -0
  11. package/lib/managers/docker.js +36 -23
  12. package/lib/parsers/iri.js +1 -2
  13. package/lib/server/server.js +164 -34
  14. package/lib/server/server.poku.js +232 -10
  15. package/lib/stages/postgen/annotator.js +281 -3
  16. package/lib/stages/postgen/postgen.js +4 -7
  17. package/lib/third-party/arborist/lib/diff.js +1 -1
  18. package/lib/third-party/arborist/lib/node.js +1 -1
  19. package/lib/third-party/arborist/lib/yarn-lock.js +1 -1
  20. package/package.json +22 -328
  21. package/types/bin/dependencies.d.ts.map +1 -1
  22. package/types/lib/cli/index.d.ts +39 -39
  23. package/types/lib/cli/index.d.ts.map +1 -1
  24. package/types/lib/evinser/evinser.d.ts +19 -19
  25. package/types/lib/evinser/evinser.d.ts.map +1 -1
  26. package/types/lib/evinser/swiftsem.d.ts +14 -14
  27. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  28. package/types/lib/helpers/cbomutils.d.ts +1 -1
  29. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  30. package/types/lib/helpers/db.d.ts +2 -2
  31. package/types/lib/helpers/db.d.ts.map +1 -1
  32. package/types/lib/helpers/display.d.ts +2 -2
  33. package/types/lib/helpers/display.d.ts.map +1 -1
  34. package/types/lib/helpers/envcontext.d.ts +14 -14
  35. package/types/lib/helpers/envcontext.d.ts.map +1 -1
  36. package/types/lib/helpers/logger.d.ts +1 -1
  37. package/types/lib/helpers/logger.d.ts.map +1 -1
  38. package/types/lib/helpers/protobom.d.ts +4 -2
  39. package/types/lib/helpers/protobom.d.ts.map +1 -1
  40. package/types/lib/helpers/utils.d.ts +103 -88
  41. package/types/lib/helpers/utils.d.ts.map +1 -1
  42. package/types/lib/helpers/validator.d.ts.map +1 -1
  43. package/types/lib/managers/binary.d.ts +2 -2
  44. package/types/lib/managers/binary.d.ts.map +1 -1
  45. package/types/lib/managers/docker.d.ts +2 -2
  46. package/types/lib/managers/docker.d.ts.map +1 -1
  47. package/types/lib/managers/oci.d.ts +1 -1
  48. package/types/lib/managers/oci.d.ts.map +1 -1
  49. package/types/lib/managers/piptree.d.ts +1 -1
  50. package/types/lib/managers/piptree.d.ts.map +1 -1
  51. package/types/lib/parsers/iri.d.ts +6 -6
  52. package/types/lib/parsers/iri.d.ts.map +1 -1
  53. package/types/lib/server/server.d.ts +14 -0
  54. package/types/lib/server/server.d.ts.map +1 -1
  55. package/types/lib/stages/postgen/annotator.d.ts +3 -3
  56. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  57. package/types/lib/stages/postgen/postgen.d.ts +5 -5
  58. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  59. package/types/lib/stages/pregen/pregen.d.ts +6 -6
  60. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  61. package/types/lib/third-party/arborist/lib/arborist/index.d.ts +4 -3
  62. package/types/lib/third-party/arborist/lib/arborist/index.d.ts.map +1 -1
  63. package/types/lib/third-party/arborist/lib/can-place-dep.d.ts +5 -5
  64. package/types/lib/third-party/arborist/lib/can-place-dep.d.ts.map +1 -1
  65. package/types/lib/third-party/arborist/lib/case-insensitive-map.d.ts +4 -4
  66. package/types/lib/third-party/arborist/lib/case-insensitive-map.d.ts.map +1 -1
  67. package/types/lib/third-party/arborist/lib/diff.d.ts +3 -3
  68. package/types/lib/third-party/arborist/lib/diff.d.ts.map +1 -1
  69. package/types/lib/third-party/arborist/lib/edge.d.ts +2 -2
  70. package/types/lib/third-party/arborist/lib/edge.d.ts.map +1 -1
  71. package/types/lib/third-party/arborist/lib/gather-dep-set.d.ts +1 -1
  72. package/types/lib/third-party/arborist/lib/gather-dep-set.d.ts.map +1 -1
  73. package/types/lib/third-party/arborist/lib/inventory.d.ts +3 -2
  74. package/types/lib/third-party/arborist/lib/inventory.d.ts.map +1 -1
  75. package/types/lib/third-party/arborist/lib/link.d.ts +10 -7
  76. package/types/lib/third-party/arborist/lib/link.d.ts.map +1 -1
  77. package/types/lib/third-party/arborist/lib/node.d.ts +8 -8
  78. package/types/lib/third-party/arborist/lib/node.d.ts.map +1 -1
  79. package/types/lib/third-party/arborist/lib/optional-set.d.ts +1 -1
  80. package/types/lib/third-party/arborist/lib/optional-set.d.ts.map +1 -1
  81. package/types/lib/third-party/arborist/lib/override-set.d.ts +3 -3
  82. package/types/lib/third-party/arborist/lib/override-set.d.ts.map +1 -1
  83. package/types/lib/third-party/arborist/lib/peer-entry-sets.d.ts +1 -1
  84. package/types/lib/third-party/arborist/lib/peer-entry-sets.d.ts.map +1 -1
  85. package/types/lib/third-party/arborist/lib/place-dep.d.ts +3 -3
  86. package/types/lib/third-party/arborist/lib/place-dep.d.ts.map +1 -1
  87. package/types/lib/third-party/arborist/lib/shrinkwrap.d.ts +7 -7
  88. package/types/lib/third-party/arborist/lib/shrinkwrap.d.ts.map +1 -1
  89. package/types/lib/third-party/arborist/lib/version-from-tgz.d.ts +1 -1
  90. package/types/lib/third-party/arborist/lib/version-from-tgz.d.ts.map +1 -1
  91. package/types/lib/third-party/arborist/lib/yarn-lock.d.ts +4 -3
  92. package/types/lib/third-party/arborist/lib/yarn-lock.d.ts.map +1 -1
  93. package/bin/dependencies.js +0 -131
  94. package/bin/licenses.js +0 -78
  95. package/lib/helpers/dependencies.poku.js +0 -11
  96. package/lib/helpers/licenses.poku.js +0 -11
  97. package/types/lib/third-party/arborist/lib/arborist/load-actual.d.ts +0 -34
  98. package/types/lib/third-party/arborist/lib/arborist/load-actual.d.ts.map +0 -1
  99. package/types/lib/third-party/arborist/lib/arborist/load-virtual.d.ts +0 -24
  100. package/types/lib/third-party/arborist/lib/arborist/load-virtual.d.ts.map +0 -1
  101. package/types/lib/third-party/arborist/lib/tracker.d.ts +0 -13
  102. package/types/lib/third-party/arborist/lib/tracker.d.ts.map +0 -1
@@ -48,6 +48,7 @@ import {
48
48
  satisfies,
49
49
  valid,
50
50
  } from "semver";
51
+ import { v4 as uuidv4 } from "uuid";
51
52
  import { xml2js } from "xml-js";
52
53
  import { parse as _load } from "yaml";
53
54
 
@@ -160,6 +161,20 @@ export function safeSpawnSync(command, args, options) {
160
161
  if (!options.timeout) {
161
162
  options.timeout = TIMEOUT_MS;
162
163
  }
164
+ // Check for -S for python invocations in secure mode
165
+ if (command.includes("python") && (!args?.length || args[0] !== "-S")) {
166
+ if (isSecureMode) {
167
+ console.warn(
168
+ "\x1b[1;35mNotice: Running python command without '-S' argument. This is a bug in cdxgen. Please report with an example repo here https://github.com/cdxgen/cdxgen/issues.\x1b[0m",
169
+ );
170
+ } else if (process.env?.CDXGEN_IN_CONTAINER === "true") {
171
+ console.log("Running python command without '-S' argument.");
172
+ } else {
173
+ console.warn(
174
+ "\x1b[1;35mNotice: Running python command without '-S' argument. Only run cdxgen in trusted directories to prevent auto-executing local scripts.\x1b[0m",
175
+ );
176
+ }
177
+ }
163
178
  traceLog("spawn", { command, args, ...options });
164
179
  commandsExecuted.add(command);
165
180
  // Fix for DEP0190 warning
@@ -1033,18 +1048,10 @@ export function getKnownLicense(licenseUrl, pkg) {
1033
1048
  return { id: akLic.license, name: akLic.licenseName };
1034
1049
  }
1035
1050
  }
1036
- if (
1037
- akLic.urlIncludes &&
1038
- licenseUrl &&
1039
- licenseUrl.includes(akLic.urlIncludes)
1040
- ) {
1051
+ if (akLic.urlIncludes && licenseUrl?.includes(akLic.urlIncludes)) {
1041
1052
  return { id: akLic.license, name: akLic.licenseName };
1042
1053
  }
1043
- if (
1044
- akLic.urlEndswith &&
1045
- licenseUrl &&
1046
- licenseUrl.endsWith(akLic.urlEndswith)
1047
- ) {
1054
+ if (akLic.urlEndswith && licenseUrl?.endsWith(akLic.urlEndswith)) {
1048
1055
  return { id: akLic.license, name: akLic.licenseName };
1049
1056
  }
1050
1057
  }
@@ -1265,7 +1272,7 @@ export async function parsePkgJson(pkgJsonFile, simple = false) {
1265
1272
  // continue regardless of error
1266
1273
  }
1267
1274
  }
1268
- if (!simple && shouldFetchLicense() && pkgList && pkgList.length) {
1275
+ if (!simple && shouldFetchLicense() && pkgList?.length) {
1269
1276
  if (DEBUG_MODE) {
1270
1277
  console.log(
1271
1278
  `About to fetch license information for ${pkgList.length} packages in parsePkgJson`,
@@ -1455,6 +1462,40 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
1455
1462
  value: "true",
1456
1463
  });
1457
1464
  }
1465
+ // Detect version spoofing by comparing the version in the lockfile with the version in package.json
1466
+ if (node.path && safeExistsSync(join(node.path, "package.json"))) {
1467
+ try {
1468
+ const diskPkgStr = readFileSync(
1469
+ join(node.path, "package.json"),
1470
+ "utf8",
1471
+ );
1472
+ const diskPkg = JSON.parse(diskPkgStr);
1473
+ if (!diskPkg.name || diskPkg.name !== node.packageName) {
1474
+ console.warn(
1475
+ `\x1b[1;35mWARNING: Package name spoofing detected for ${node.packageName}! Lockfile says ${node.packageName}, but disk says ${diskPkg.name}.\x1b[0m`,
1476
+ );
1477
+ if (diskPkg.name) {
1478
+ pkg.properties.push({
1479
+ name: "cdx:npm:nameMismatchError",
1480
+ value: `${diskPkg.name} used instead of ${node.packageName}`,
1481
+ });
1482
+ }
1483
+ }
1484
+ if (!diskPkg.version || diskPkg.version !== node.version) {
1485
+ console.warn(
1486
+ `\x1b[1;35mWARNING: Package version spoofing detected for ${node.packageName}! Lockfile says ${node.version}, but disk says ${diskPkg.version}.\x1b[0m`,
1487
+ );
1488
+ if (diskPkg.version) {
1489
+ pkg.properties.push({
1490
+ name: "cdx:npm:versionMismatchError",
1491
+ value: `${diskPkg.version} used instead of ${node.version}`,
1492
+ });
1493
+ }
1494
+ }
1495
+ } catch (_err) {
1496
+ // ignore
1497
+ }
1498
+ }
1458
1499
  if (node?.inBundle) {
1459
1500
  pkg.properties.push({
1460
1501
  name: "cdx:npm:inBundle",
@@ -1786,7 +1827,7 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
1786
1827
  options,
1787
1828
  ));
1788
1829
 
1789
- if (shouldFetchLicense() && pkgList && pkgList.length) {
1830
+ if (shouldFetchLicense() && pkgList?.length) {
1790
1831
  if (DEBUG_MODE) {
1791
1832
  console.log(
1792
1833
  `About to fetch license information for ${pkgList.length} packages in parsePkgLock`,
@@ -2419,7 +2460,7 @@ export async function parseYarnLock(
2419
2460
  }
2420
2461
  }
2421
2462
 
2422
- if (shouldFetchLicense() && pkgList && pkgList.length) {
2463
+ if (shouldFetchLicense() && pkgList?.length) {
2423
2464
  if (DEBUG_MODE) {
2424
2465
  console.log(
2425
2466
  `About to fetch license information for ${pkgList.length} packages in parseYarnLock`,
@@ -2496,7 +2537,7 @@ export async function parseNodeShrinkwrap(swFile) {
2496
2537
  }
2497
2538
  }
2498
2539
  }
2499
- if (shouldFetchLicense() && pkgList && pkgList.length) {
2540
+ if (shouldFetchLicense() && pkgList?.length) {
2500
2541
  if (DEBUG_MODE) {
2501
2542
  console.log(
2502
2543
  `About to fetch license information for ${pkgList.length} packages in parseNodeShrinkwrap`,
@@ -3698,7 +3739,7 @@ export async function parsePnpmLock(
3698
3739
  pkgList = await pnpmMetadata(pkgList, pnpmLock);
3699
3740
  }
3700
3741
 
3701
- if (shouldFetchLicense() && pkgList && pkgList.length) {
3742
+ if (shouldFetchLicense() && pkgList?.length) {
3702
3743
  if (DEBUG_MODE) {
3703
3744
  console.log(
3704
3745
  `About to fetch license information for ${pkgList.length} packages in parsePnpmLock`,
@@ -3759,7 +3800,7 @@ export async function parseBowerJson(bowerJsonFile) {
3759
3800
  // continue regardless of error
3760
3801
  }
3761
3802
  }
3762
- if (shouldFetchLicense() && pkgList && pkgList.length) {
3803
+ if (shouldFetchLicense() && pkgList?.length) {
3763
3804
  if (DEBUG_MODE) {
3764
3805
  console.log(
3765
3806
  `About to fetch license information for ${pkgList.length} packages in parseBowerJson`,
@@ -3857,7 +3898,7 @@ export async function parseMinJs(minJsFile) {
3857
3898
  // continue regardless of error
3858
3899
  }
3859
3900
  }
3860
- if (shouldFetchLicense() && pkgList && pkgList.length) {
3901
+ if (shouldFetchLicense() && pkgList?.length) {
3861
3902
  if (DEBUG_MODE) {
3862
3903
  console.log(
3863
3904
  `About to fetch license information for ${pkgList.length} packages in parseMinJs`,
@@ -6132,6 +6173,9 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
6132
6173
  ).toString();
6133
6174
  pkg.purl = purlString;
6134
6175
  pkg["bom-ref"] = decodeURIComponent(purlString);
6176
+ if (parentComponent && pkg["bom-ref"] === parentComponent["bom-ref"]) {
6177
+ continue;
6178
+ }
6135
6179
  pkg.evidence = {
6136
6180
  identity: {
6137
6181
  field: "purl",
@@ -6387,6 +6431,9 @@ export async function parseReqFile(reqFile, fetchDepsInfo = false) {
6387
6431
  return await parseReqData(reqFile, null, fetchDepsInfo);
6388
6432
  }
6389
6433
 
6434
+ const LICENSE_ID_COMMENTS_PATTERN =
6435
+ /^(Apache-2\.0|MIT|ISC|GPL-|LGPL-|BSD-[23]-Clause)/i;
6436
+
6390
6437
  /**
6391
6438
  * Method to parse requirements.txt file. Must only be used internally.
6392
6439
  *
@@ -6434,6 +6481,12 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
6434
6481
  if (!l || l.startsWith("#") || l.startsWith("-")) {
6435
6482
  continue;
6436
6483
  }
6484
+ let comment = null;
6485
+ const commentMatch = l.match(/\s+#(.*)$/);
6486
+ if (commentMatch) {
6487
+ comment = commentMatch[1].trim();
6488
+ l = l.substring(0, commentMatch.index).trim();
6489
+ }
6437
6490
  const properties = reqFile
6438
6491
  ? [
6439
6492
  {
@@ -6482,6 +6535,21 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
6482
6535
  scope: compScope,
6483
6536
  evidence,
6484
6537
  };
6538
+ if (comment) {
6539
+ apkg.licenses = comment
6540
+ .split("/")
6541
+ .map((c) => {
6542
+ const licenseObj = {};
6543
+ const cId = c.trim();
6544
+ if (cId.match(LICENSE_ID_COMMENTS_PATTERN)) {
6545
+ licenseObj.id = cId;
6546
+ } else {
6547
+ return undefined;
6548
+ }
6549
+ return { license: licenseObj };
6550
+ })
6551
+ .filter((l) => l !== undefined);
6552
+ }
6485
6553
  if (extras && extras.length > 0) {
6486
6554
  properties.push({
6487
6555
  name: "cdx:pypi:extras",
@@ -6533,6 +6601,21 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
6533
6601
  scope: compScope,
6534
6602
  evidence,
6535
6603
  };
6604
+ if (comment) {
6605
+ apkg.licenses = comment
6606
+ .split("/")
6607
+ .map((c) => {
6608
+ const licenseObj = {};
6609
+ const cId = c.trim();
6610
+ if (cId.match(LICENSE_ID_COMMENTS_PATTERN)) {
6611
+ licenseObj.id = cId;
6612
+ } else {
6613
+ return undefined;
6614
+ }
6615
+ return { license: licenseObj };
6616
+ })
6617
+ .filter((l) => l !== undefined);
6618
+ }
6536
6619
  if (versionSpecifiers && !versionSpecifiers.startsWith("==")) {
6537
6620
  properties.push({
6538
6621
  name: "cdx:pypi:versionSpecifiers",
@@ -9877,82 +9960,250 @@ export function parseGitHubWorkflowData(f) {
9877
9960
  return pkgList;
9878
9961
  }
9879
9962
  const lines = ghwData.split("\n");
9963
+ // workflow-related values
9964
+ const workflowName = yamlObj.name || path.basename(f, path.extname(f));
9965
+ const workflowTriggers = yamlObj.on || yamlObj.true;
9966
+ const workflowPermissions = yamlObj.permissions || {};
9967
+ let hasWritePermissions = analyzePermissions(workflowPermissions);
9968
+ // GitHub of course supports strings such as "write-all" to make it easy to create supply-chain attacks.
9969
+ if (
9970
+ (typeof workflowPermissions === "string" ||
9971
+ workflowPermissions instanceof String) &&
9972
+ workflowPermissions.includes("write")
9973
+ ) {
9974
+ hasWritePermissions = true;
9975
+ }
9976
+ const workflowConcurrency = yamlObj.concurrency || {};
9977
+ const _workflowEnv = yamlObj.env || {};
9978
+ const hasIdTokenWrite = workflowPermissions?.["id-token"] === "write";
9880
9979
  for (const jobName of Object.keys(yamlObj.jobs)) {
9881
- if (yamlObj.jobs[jobName].steps) {
9882
- for (const step of yamlObj.jobs[jobName].steps) {
9883
- if (step.uses) {
9884
- const tmpA = step.uses.split("@");
9885
- if (tmpA.length !== 2) {
9886
- continue;
9980
+ const job = yamlObj.jobs[jobName];
9981
+ if (!job.steps) {
9982
+ continue;
9983
+ }
9984
+ // job-related values
9985
+ const jobRunner = job["runs-on"] || "unknown";
9986
+ const jobEnvironment = job.environment?.name || job.environment || "";
9987
+ const jobTimeout = job["timeout-minutes"] || null;
9988
+ const jobPermissions = job.permissions || {};
9989
+ const jobServices = job.services ? Object.keys(job.services) : [];
9990
+ let jobNeeds = job.needs || [];
9991
+ if (!Array.isArray(jobNeeds)) {
9992
+ jobNeeds = [jobNeeds];
9993
+ }
9994
+ const _jobIf = job.if || "";
9995
+ const _jobStrategy = job.strategy ? JSON.stringify(job.strategy) : "";
9996
+ const jobHasWritePermissions = analyzePermissions(jobPermissions);
9997
+ for (const step of job.steps) {
9998
+ if (step.uses) {
9999
+ const tmpA = step.uses.split("@");
10000
+ if (tmpA.length !== 2) {
10001
+ continue;
10002
+ }
10003
+ const groupName = tmpA[0];
10004
+ let name = groupName;
10005
+ let group = "";
10006
+ const tagOrCommit = tmpA[1];
10007
+ let version = tagOrCommit;
10008
+ const tmpB = groupName.split("/");
10009
+ if (tmpB.length >= 2) {
10010
+ name = tmpB.pop();
10011
+ group = tmpB.join("/");
10012
+ } else if (tmpB.length === 1) {
10013
+ name = tmpB[0];
10014
+ group = "";
10015
+ }
10016
+ const versionPinningType = getVersionPinningType(tagOrCommit);
10017
+ const isShaPinned = versionPinningType === "sha";
10018
+ const _isTagPinned = versionPinningType === "tag";
10019
+ const _isBranchRef = versionPinningType === "branch";
10020
+ let lineNum = -1;
10021
+ const stepLineMatch = ghwData.indexOf(step.uses);
10022
+ if (stepLineMatch >= 0) {
10023
+ lineNum = ghwData.substring(0, stepLineMatch).split("\n").length - 1;
10024
+ }
10025
+ if (lineNum >= 0 && lines[lineNum]) {
10026
+ const line = lines[lineNum];
10027
+ const commentMatch = line.match(/#\s*v?([0-9]+(?:\.[0-9]+)*)/);
10028
+ if (commentMatch?.[1]) {
10029
+ version = commentMatch[1];
9887
10030
  }
9888
- const groupName = tmpA[0];
9889
- let name = groupName;
9890
- let group = "";
9891
- const tagOrCommit = tmpA[1];
9892
- let version = tagOrCommit;
9893
- const tmpB = groupName.split("/");
9894
- if (tmpB.length >= 2) {
9895
- name = tmpB.pop();
9896
- group = tmpB.join("/");
9897
- } else if (tmpB.length === 1) {
9898
- name = tmpB[0];
9899
- group = "";
10031
+ }
10032
+ const key = `${group}-${name}-${version}`;
10033
+ let confidence = 0.6;
10034
+ if (!keys_cache[key] && name && version) {
10035
+ keys_cache[key] = key;
10036
+ let fullName = name;
10037
+ if (group.length) {
10038
+ fullName = `${group}/${name}`;
9900
10039
  }
9901
- let lineNum = -1;
9902
- const stepLineMatch = ghwData.indexOf(step.uses);
9903
- if (stepLineMatch >= 0) {
9904
- lineNum =
9905
- ghwData.substring(0, stepLineMatch).split("\n").length - 1;
10040
+ let purl = `pkg:github/${fullName}@${version}`;
10041
+ if (tagOrCommit && version !== tagOrCommit) {
10042
+ const qualifierDesc = tagOrCommit.startsWith("v")
10043
+ ? "tag"
10044
+ : "commit";
10045
+ purl = `${purl}?${qualifierDesc}=${tagOrCommit}`;
10046
+ confidence = 0.7;
9906
10047
  }
9907
- if (lineNum >= 0 && lines[lineNum]) {
9908
- const line = lines[lineNum];
9909
- const commentMatch = line.match(/#\s*v?([0-9]+(?:\.[0-9]+)*)/);
9910
- if (commentMatch?.[1]) {
9911
- version = commentMatch[1];
9912
- }
10048
+ const properties = [
10049
+ { name: "SrcFile", value: f },
10050
+ { name: "cdx:github:workflow:name", value: workflowName },
10051
+ { name: "cdx:github:job:name", value: jobName },
10052
+ {
10053
+ name: "cdx:github:job:runner",
10054
+ value: Array.isArray(jobRunner) ? jobRunner.join(",") : jobRunner,
10055
+ },
10056
+ { name: "cdx:github:action:uses", value: step.uses },
10057
+ {
10058
+ name: "cdx:github:action:versionPinningType",
10059
+ value: versionPinningType,
10060
+ },
10061
+ {
10062
+ name: "cdx:github:action:isShaPinned",
10063
+ value: isShaPinned.toString(),
10064
+ },
10065
+ ];
10066
+ if (step.name) {
10067
+ properties.push({ name: "cdx:github:step:name", value: step.name });
9913
10068
  }
9914
- const key = `${group}-${name}-${version}`;
9915
- let confidence = 0.6;
9916
- if (!keys_cache[key] && name && version) {
9917
- keys_cache[key] = key;
9918
- let fullName = name;
9919
- if (group.length) {
9920
- fullName = `${group}/${name}`;
9921
- }
9922
- let purl = `pkg:github/${fullName}@${version}`;
9923
- if (tagOrCommit && version !== tagOrCommit) {
9924
- const qualifierDesc = tagOrCommit.startsWith("v")
9925
- ? "tag"
9926
- : "commit";
9927
- purl = `${purl}?${qualifierDesc}=${tagOrCommit}`;
9928
- confidence = 0.7;
9929
- }
9930
- const properties = [
9931
- {
9932
- name: "SrcFile",
9933
- value: f,
10069
+ if (step.if) {
10070
+ properties.push({
10071
+ name: "cdx:github:step:condition",
10072
+ value: step.if,
10073
+ });
10074
+ }
10075
+ if (step["continue-on-error"]) {
10076
+ properties.push({
10077
+ name: "cdx:github:step:continueOnError",
10078
+ value: "true",
10079
+ });
10080
+ }
10081
+ if (step.timeout) {
10082
+ properties.push({
10083
+ name: "cdx:github:step:timeout",
10084
+ value: step.timeout.toString(),
10085
+ });
10086
+ }
10087
+ if (jobEnvironment) {
10088
+ properties.push({
10089
+ name: "cdx:github:job:environment",
10090
+ value: jobEnvironment,
10091
+ });
10092
+ }
10093
+ if (jobTimeout) {
10094
+ properties.push({
10095
+ name: "cdx:github:job:timeoutMinutes",
10096
+ value: jobTimeout.toString(),
10097
+ });
10098
+ }
10099
+ if (jobHasWritePermissions) {
10100
+ properties.push({
10101
+ name: "cdx:github:job:hasWritePermissions",
10102
+ value: "true",
10103
+ });
10104
+ }
10105
+ if (jobServices.length > 0) {
10106
+ properties.push({
10107
+ name: "cdx:github:job:services",
10108
+ value: jobServices.join(","),
10109
+ });
10110
+ }
10111
+ if (jobNeeds.length > 0) {
10112
+ properties.push({
10113
+ name: "cdx:github:job:needs",
10114
+ value: jobNeeds.join(","),
10115
+ });
10116
+ }
10117
+ if (hasWritePermissions) {
10118
+ properties.push({
10119
+ name: "cdx:github:workflow:hasWritePermissions",
10120
+ value: "true",
10121
+ });
10122
+ }
10123
+ if (hasIdTokenWrite) {
10124
+ properties.push({
10125
+ name: "cdx:github:workflow:hasIdTokenWrite",
10126
+ value: "true",
10127
+ });
10128
+ }
10129
+ if (workflowConcurrency?.group) {
10130
+ properties.push({
10131
+ name: "cdx:github:workflow:concurrencyGroup",
10132
+ value: workflowConcurrency.group,
10133
+ });
10134
+ }
10135
+ if (group?.startsWith("github/") || group === "actions") {
10136
+ properties.push({ name: "cdx:actions:isOfficial", value: "true" });
10137
+ }
10138
+ if (group?.startsWith("github/")) {
10139
+ properties.push({ name: "cdx:actions:isVerified", value: "true" });
10140
+ }
10141
+ if (workflowTriggers) {
10142
+ const triggers =
10143
+ typeof workflowTriggers === "string"
10144
+ ? workflowTriggers
10145
+ : Object.keys(workflowTriggers).join(",");
10146
+ properties.push({
10147
+ name: "cdx:github:workflow:triggers",
10148
+ value: triggers,
10149
+ });
10150
+ }
10151
+ pkgList.push({
10152
+ group,
10153
+ name,
10154
+ version,
10155
+ purl,
10156
+ properties,
10157
+ evidence: {
10158
+ identity: {
10159
+ field: "purl",
10160
+ confidence,
10161
+ methods: [
10162
+ {
10163
+ technique: "source-code-analysis",
10164
+ confidence,
10165
+ value: f,
10166
+ },
10167
+ ],
9934
10168
  },
9935
- ];
9936
- if (group?.startsWith("github/") || group === "actions") {
9937
- properties.push({
9938
- name: "cdx:actions:isOfficial",
9939
- value: "true",
9940
- });
9941
- }
10169
+ },
10170
+ });
10171
+ }
10172
+ }
10173
+ if (step.run) {
10174
+ const runLineNum = ghwData.indexOf(step.run);
10175
+ const runLine =
10176
+ runLineNum >= 0
10177
+ ? ghwData.substring(0, runLineNum).split("\n").length
10178
+ : -1;
10179
+ const pkgCommands = extractPackageManagerCommands(step.run);
10180
+ for (const pkgCmd of pkgCommands) {
10181
+ const key = `run-${pkgCmd.name}-${pkgCmd.version || "latest"}`;
10182
+ if (!keys_cache[key]) {
10183
+ keys_cache[key] = key;
9942
10184
  pkgList.push({
9943
- group,
9944
- name,
9945
- version,
9946
- purl,
9947
- properties,
10185
+ group: "",
10186
+ name: pkgCmd.name,
10187
+ version: pkgCmd.version || undefined,
10188
+ scope: "excluded",
10189
+ "bom-ref": uuidv4(),
10190
+ description: key,
10191
+ properties: [
10192
+ { name: "SrcFile", value: f },
10193
+ { name: "cdx:github:workflow:name", value: workflowName },
10194
+ { name: "cdx:github:job:name", value: jobName },
10195
+ { name: "cdx:github:step:type", value: "run" },
10196
+ { name: "cdx:github:step:command", value: pkgCmd.command },
10197
+ { name: "cdx:github:run:line", value: runLine.toString() },
10198
+ ],
9948
10199
  evidence: {
9949
10200
  identity: {
9950
10201
  field: "purl",
9951
- confidence,
10202
+ confidence: 0.5,
9952
10203
  methods: [
9953
10204
  {
9954
10205
  technique: "source-code-analysis",
9955
- confidence,
10206
+ confidence: 0.5,
9956
10207
  value: f,
9957
10208
  },
9958
10209
  ],
@@ -9967,6 +10218,103 @@ export function parseGitHubWorkflowData(f) {
9967
10218
  return pkgList;
9968
10219
  }
9969
10220
 
10221
+ /**
10222
+ * Analyze permissions for write access.
10223
+ *
10224
+ * Refer to https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#permissions
10225
+ */
10226
+ function analyzePermissions(permissions) {
10227
+ if (!permissions || typeof permissions !== "object") {
10228
+ return false;
10229
+ }
10230
+ const writePermissions = [
10231
+ "actions",
10232
+ "artifact-metadata",
10233
+ "attestations",
10234
+ "checks",
10235
+ "contents",
10236
+ "deployments",
10237
+ "id-token",
10238
+ "models",
10239
+ "discussions",
10240
+ "packages",
10241
+ "pages",
10242
+ "actions",
10243
+ "deployments",
10244
+ "issues",
10245
+ "pull-requests",
10246
+ "security-events",
10247
+ "statuses",
10248
+ ];
10249
+ for (const perm of writePermissions) {
10250
+ if (permissions[perm] === "write") {
10251
+ return true;
10252
+ }
10253
+ }
10254
+ return false;
10255
+ }
10256
+
10257
+ /**
10258
+ * Determine version pinning type for security assessment
10259
+ */
10260
+ function getVersionPinningType(versionRef) {
10261
+ if (!versionRef) {
10262
+ return "unknown";
10263
+ }
10264
+ if (/^[a-f0-9]{40}$/.test(versionRef)) {
10265
+ return "sha";
10266
+ }
10267
+ if (/^[a-f0-9]{7,}$/.test(versionRef)) {
10268
+ return "sha";
10269
+ }
10270
+ if (
10271
+ versionRef === "main" ||
10272
+ versionRef === "master" ||
10273
+ versionRef.includes("/")
10274
+ ) {
10275
+ return "branch";
10276
+ }
10277
+ return "tag";
10278
+ }
10279
+
10280
+ /**
10281
+ * Extract package manager commands from run steps
10282
+ */
10283
+ function extractPackageManagerCommands(runScript) {
10284
+ const commands = [];
10285
+ if (!runScript) {
10286
+ return commands;
10287
+ }
10288
+ const patterns = [
10289
+ { regex: /npm\s+(install|i|ci)\s+([^&\n|;]+)/g, name: "npm", type: "npm" },
10290
+ {
10291
+ regex: /yarn\s+(add|install)\s+([^&\n|;]+)/g,
10292
+ name: "yarn",
10293
+ type: "yarn",
10294
+ },
10295
+ { regex: /pip\s+(install)\s+([^&\n|;]+)/g, name: "pip", type: "pip" },
10296
+ { regex: /pip3\s+(install)\s+([^&\n|;]+)/g, name: "pip3", type: "pip" },
10297
+ { regex: /gem\s+(install)\s+([^&\n|;]+)/g, name: "gem", type: "gem" },
10298
+ { regex: /go\s+(get|install)\s+([^&\n|;]+)/g, name: "go", type: "go" },
10299
+ {
10300
+ regex: /cargo\s+(install|add)\s+([^&\n|;]+)/g,
10301
+ name: "cargo",
10302
+ type: "cargo",
10303
+ },
10304
+ ];
10305
+ for (const pattern of patterns) {
10306
+ let match;
10307
+ while ((match = pattern.regex.exec(runScript)) !== null) {
10308
+ commands.push({
10309
+ name: pattern.name,
10310
+ command: match[0],
10311
+ version: null,
10312
+ });
10313
+ }
10314
+ }
10315
+ return commands;
10316
+ }
10317
+
9970
10318
  export function parseCloudBuildData(cbwData) {
9971
10319
  const pkgList = [];
9972
10320
  const keys_cache = {};
@@ -10833,6 +11181,9 @@ export function parseCsPkgData(pkgData, pkgFile) {
10833
11181
  export function getPropertyGroupTextNodes(propsFiles) {
10834
11182
  const matches = {};
10835
11183
  for (const f of propsFiles) {
11184
+ if (!f) {
11185
+ continue;
11186
+ }
10836
11187
  let projects;
10837
11188
  try {
10838
11189
  const data = readFileSync(f, { encoding: "utf-8" });
@@ -11623,9 +11974,7 @@ export function parseCsPkgLockData(csLockData, pkgLockFile) {
11623
11974
  assetData.dependencies[aversion][adep].resolved;
11624
11975
  } else if (
11625
11976
  aversion.includes("/") &&
11626
- assetData.dependencies[aversionNoRuntime] &&
11627
- assetData.dependencies[aversionNoRuntime][adep] &&
11628
- assetData.dependencies[aversionNoRuntime][adep].resolved
11977
+ assetData.dependencies[aversionNoRuntime]?.[adep]?.resolved
11629
11978
  ) {
11630
11979
  adepResolvedVersion =
11631
11980
  assetData.dependencies[aversionNoRuntime][adep].resolved;
@@ -12695,7 +13044,7 @@ export async function collectMvnDependencies(
12695
13044
  }
12696
13045
 
12697
13046
  // Clean up
12698
- if (cleanup && tempDir && tempDir.startsWith(tmpdir()) && rmSync) {
13047
+ if (cleanup && tempDir?.startsWith(tmpdir()) && rmSync) {
12699
13048
  rmSync(tempDir, { recursive: true, force: true });
12700
13049
  }
12701
13050
  return jarNSMapping;
@@ -15056,7 +15405,11 @@ export async function getPipFrozenTree(
15056
15405
  thoughtLog(
15057
15406
  "Let me create a new virtual environment for installing the packages with pip.",
15058
15407
  );
15059
- result = safeSpawnSync(PYTHON_CMD, ["-m", "venv", tempVenvDir], {
15408
+ const venvCreationArgs = ["-m", "venv", tempVenvDir];
15409
+ if (isSecureMode) {
15410
+ venvCreationArgs.unshift("-S");
15411
+ }
15412
+ result = safeSpawnSync(PYTHON_CMD, venvCreationArgs, {
15060
15413
  shell: isWin,
15061
15414
  });
15062
15415
  if (result.status !== 0 || result.error) {
@@ -15107,12 +15460,18 @@ export async function getPipFrozenTree(
15107
15460
  "true",
15108
15461
  "--local",
15109
15462
  ];
15463
+ if (isSecureMode) {
15464
+ poetryConfigArgs.unshift("-S");
15465
+ }
15110
15466
  result = safeSpawnSync(PYTHON_CMD, poetryConfigArgs, {
15111
15467
  cwd: basePath,
15112
15468
  shell: isWin,
15113
15469
  });
15114
15470
  thoughtLog("Performing poetry install");
15115
15471
  let poetryInstallArgs = ["-m", "poetry", "install", "-n", "--no-root"];
15472
+ if (isSecureMode) {
15473
+ poetryInstallArgs.unshift("-S");
15474
+ }
15116
15475
  // Attempt to perform poetry install
15117
15476
  result = safeSpawnSync(PYTHON_CMD, poetryInstallArgs, {
15118
15477
  cwd: basePath,
@@ -15189,6 +15548,9 @@ export async function getPipFrozenTree(
15189
15548
  "install",
15190
15549
  "--disable-pip-version-check",
15191
15550
  ];
15551
+ if (isSecureMode) {
15552
+ pipInstallArgs.unshift("-S");
15553
+ }
15192
15554
  // Requirements.txt could be called with any name so best to check for not setup.py and not pyproject.toml
15193
15555
  if (
15194
15556
  !reqOrSetupFile.endsWith("setup.py") &&
@@ -15509,7 +15871,11 @@ export function getPipTreeForPackages(
15509
15871
  };
15510
15872
  if (!process.env.VIRTUAL_ENV && !process.env.CONDA_PREFIX) {
15511
15873
  // Create a virtual environment
15512
- result = safeSpawnSync(PYTHON_CMD, ["-m", "venv", tempVenvDir], {
15874
+ const venvCreationArgs = ["-m", "venv", tempVenvDir];
15875
+ if (isSecureMode) {
15876
+ venvCreationArgs.unshift("-S");
15877
+ }
15878
+ result = safeSpawnSync(PYTHON_CMD, venvCreationArgs, {
15513
15879
  shell: isWin,
15514
15880
  });
15515
15881
  if (result.status !== 0 || result.error) {
@@ -15532,6 +15898,9 @@ export function getPipTreeForPackages(
15532
15898
  }
15533
15899
  const python_cmd_for_tree = get_python_command_from_env(env);
15534
15900
  let pipInstallArgs = ["-m", "pip", "install", "--disable-pip-version-check"];
15901
+ if (isSecureMode) {
15902
+ pipInstallArgs.unshift("-S");
15903
+ }
15535
15904
  // Support for passing additional arguments to pip
15536
15905
  // Eg: --python-version 3.10 --ignore-requires-python --no-warn-conflicts
15537
15906
  if (process?.env?.PIP_INSTALL_ARGS) {
@@ -15713,6 +16082,7 @@ export async function addEvidenceForImports(
15713
16082
  const aliases = group?.length
15714
16083
  ? [name, `${group}/${name}`, `@${group}/${name}`]
15715
16084
  : [name];
16085
+ let isImported = false;
15716
16086
  for (const alias of aliases) {
15717
16087
  const all_includes = impPkgs.filter(
15718
16088
  (find_pkg) =>
@@ -15761,6 +16131,7 @@ export async function addEvidenceForImports(
15761
16131
  }
15762
16132
  // Identify all the imported modules of a component
15763
16133
  if (impPkgs.includes(alias) || all_includes.length) {
16134
+ isImported = true;
15764
16135
  let importedModules = new Set();
15765
16136
  pkg.scope = "required";
15766
16137
  for (const subevidence of all_includes) {
@@ -15798,6 +16169,11 @@ export async function addEvidenceForImports(
15798
16169
  }
15799
16170
  break;
15800
16171
  }
16172
+ if (impPkgs?.length > 0 && !isImported && DEBUG_MODE) {
16173
+ console.debug(
16174
+ `\x1b[1;35mNotice: Package ${pkg.name} has no usage in code. Check if it is needed.\x1b[0m`,
16175
+ );
16176
+ }
15801
16177
  // Capture metadata such as description from local node_modules in deep mode
15802
16178
  if (deep && !pkg.description && pkg.properties) {
15803
16179
  let localNodeModulesPath;