@cyclonedx/cdxgen 12.1.1 → 12.1.3

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 (45) hide show
  1. package/README.md +27 -9
  2. package/bin/cdxgen.js +1 -1
  3. package/data/spdx.schema.json +35 -2
  4. package/data/templates/asvs-5.0.cdx.json +1727 -3471
  5. package/lib/cli/index.js +32 -4
  6. package/lib/evinser/evinser.js +2 -8
  7. package/lib/helpers/display.js +1 -1
  8. package/lib/helpers/envcontext.js +10 -2
  9. package/lib/helpers/utils.js +487 -115
  10. package/lib/helpers/utils.poku.js +200 -3
  11. package/lib/helpers/validator.js +37 -3
  12. package/lib/managers/binary.js +34 -12
  13. package/lib/managers/containerutils.js +68 -0
  14. package/lib/managers/docker.getConnection.poku.js +61 -0
  15. package/lib/managers/docker.js +72 -119
  16. package/lib/parsers/iri.js +1 -2
  17. package/lib/server/server.js +164 -34
  18. package/lib/server/server.poku.js +232 -10
  19. package/lib/stages/postgen/annotator.js +281 -3
  20. package/lib/stages/postgen/postgen.js +4 -7
  21. package/lib/third-party/arborist/lib/diff.js +1 -1
  22. package/lib/third-party/arborist/lib/node.js +1 -1
  23. package/lib/third-party/arborist/lib/yarn-lock.js +1 -1
  24. package/package.json +22 -326
  25. package/types/bin/dependencies.d.ts.map +1 -1
  26. package/types/bin/licenses.d.ts +3 -0
  27. package/types/bin/licenses.d.ts.map +1 -0
  28. package/types/lib/cli/index.d.ts.map +1 -1
  29. package/types/lib/evinser/evinser.d.ts.map +1 -1
  30. package/types/lib/helpers/envcontext.d.ts.map +1 -1
  31. package/types/lib/helpers/utils.d.ts +1 -1
  32. package/types/lib/helpers/utils.d.ts.map +1 -1
  33. package/types/lib/helpers/validator.d.ts.map +1 -1
  34. package/types/lib/managers/binary.d.ts.map +1 -1
  35. package/types/lib/managers/containerutils.d.ts +3 -0
  36. package/types/lib/managers/containerutils.d.ts.map +1 -0
  37. package/types/lib/managers/docker.d.ts +0 -2
  38. package/types/lib/managers/docker.d.ts.map +1 -1
  39. package/types/lib/parsers/iri.d.ts.map +1 -1
  40. package/types/lib/server/server.d.ts +14 -0
  41. package/types/lib/server/server.d.ts.map +1 -1
  42. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  43. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  44. package/bin/dependencies.js +0 -131
  45. package/lib/helpers/dependencies.poku.js +0 -11
@@ -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`,
@@ -1786,7 +1793,7 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
1786
1793
  options,
1787
1794
  ));
1788
1795
 
1789
- if (shouldFetchLicense() && pkgList && pkgList.length) {
1796
+ if (shouldFetchLicense() && pkgList?.length) {
1790
1797
  if (DEBUG_MODE) {
1791
1798
  console.log(
1792
1799
  `About to fetch license information for ${pkgList.length} packages in parsePkgLock`,
@@ -2419,7 +2426,7 @@ export async function parseYarnLock(
2419
2426
  }
2420
2427
  }
2421
2428
 
2422
- if (shouldFetchLicense() && pkgList && pkgList.length) {
2429
+ if (shouldFetchLicense() && pkgList?.length) {
2423
2430
  if (DEBUG_MODE) {
2424
2431
  console.log(
2425
2432
  `About to fetch license information for ${pkgList.length} packages in parseYarnLock`,
@@ -2496,7 +2503,7 @@ export async function parseNodeShrinkwrap(swFile) {
2496
2503
  }
2497
2504
  }
2498
2505
  }
2499
- if (shouldFetchLicense() && pkgList && pkgList.length) {
2506
+ if (shouldFetchLicense() && pkgList?.length) {
2500
2507
  if (DEBUG_MODE) {
2501
2508
  console.log(
2502
2509
  `About to fetch license information for ${pkgList.length} packages in parseNodeShrinkwrap`,
@@ -3698,7 +3705,7 @@ export async function parsePnpmLock(
3698
3705
  pkgList = await pnpmMetadata(pkgList, pnpmLock);
3699
3706
  }
3700
3707
 
3701
- if (shouldFetchLicense() && pkgList && pkgList.length) {
3708
+ if (shouldFetchLicense() && pkgList?.length) {
3702
3709
  if (DEBUG_MODE) {
3703
3710
  console.log(
3704
3711
  `About to fetch license information for ${pkgList.length} packages in parsePnpmLock`,
@@ -3759,7 +3766,7 @@ export async function parseBowerJson(bowerJsonFile) {
3759
3766
  // continue regardless of error
3760
3767
  }
3761
3768
  }
3762
- if (shouldFetchLicense() && pkgList && pkgList.length) {
3769
+ if (shouldFetchLicense() && pkgList?.length) {
3763
3770
  if (DEBUG_MODE) {
3764
3771
  console.log(
3765
3772
  `About to fetch license information for ${pkgList.length} packages in parseBowerJson`,
@@ -3857,7 +3864,7 @@ export async function parseMinJs(minJsFile) {
3857
3864
  // continue regardless of error
3858
3865
  }
3859
3866
  }
3860
- if (shouldFetchLicense() && pkgList && pkgList.length) {
3867
+ if (shouldFetchLicense() && pkgList?.length) {
3861
3868
  if (DEBUG_MODE) {
3862
3869
  console.log(
3863
3870
  `About to fetch license information for ${pkgList.length} packages in parseMinJs`,
@@ -6132,6 +6139,9 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
6132
6139
  ).toString();
6133
6140
  pkg.purl = purlString;
6134
6141
  pkg["bom-ref"] = decodeURIComponent(purlString);
6142
+ if (parentComponent && pkg["bom-ref"] === parentComponent["bom-ref"]) {
6143
+ continue;
6144
+ }
6135
6145
  pkg.evidence = {
6136
6146
  identity: {
6137
6147
  field: "purl",
@@ -6387,6 +6397,9 @@ export async function parseReqFile(reqFile, fetchDepsInfo = false) {
6387
6397
  return await parseReqData(reqFile, null, fetchDepsInfo);
6388
6398
  }
6389
6399
 
6400
+ const LICENSE_ID_COMMENTS_PATTERN =
6401
+ /^(Apache-2\.0|MIT|ISC|GPL-|LGPL-|BSD-[23]-Clause)/i;
6402
+
6390
6403
  /**
6391
6404
  * Method to parse requirements.txt file. Must only be used internally.
6392
6405
  *
@@ -6434,6 +6447,12 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
6434
6447
  if (!l || l.startsWith("#") || l.startsWith("-")) {
6435
6448
  continue;
6436
6449
  }
6450
+ let comment = null;
6451
+ const commentMatch = l.match(/\s+#(.*)$/);
6452
+ if (commentMatch) {
6453
+ comment = commentMatch[1].trim();
6454
+ l = l.substring(0, commentMatch.index).trim();
6455
+ }
6437
6456
  const properties = reqFile
6438
6457
  ? [
6439
6458
  {
@@ -6482,6 +6501,21 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
6482
6501
  scope: compScope,
6483
6502
  evidence,
6484
6503
  };
6504
+ if (comment) {
6505
+ apkg.licenses = comment
6506
+ .split("/")
6507
+ .map((c) => {
6508
+ const licenseObj = {};
6509
+ const cId = c.trim();
6510
+ if (cId.match(LICENSE_ID_COMMENTS_PATTERN)) {
6511
+ licenseObj.id = cId;
6512
+ } else {
6513
+ return undefined;
6514
+ }
6515
+ return { license: licenseObj };
6516
+ })
6517
+ .filter((l) => l !== undefined);
6518
+ }
6485
6519
  if (extras && extras.length > 0) {
6486
6520
  properties.push({
6487
6521
  name: "cdx:pypi:extras",
@@ -6533,6 +6567,21 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
6533
6567
  scope: compScope,
6534
6568
  evidence,
6535
6569
  };
6570
+ if (comment) {
6571
+ apkg.licenses = comment
6572
+ .split("/")
6573
+ .map((c) => {
6574
+ const licenseObj = {};
6575
+ const cId = c.trim();
6576
+ if (cId.match(LICENSE_ID_COMMENTS_PATTERN)) {
6577
+ licenseObj.id = cId;
6578
+ } else {
6579
+ return undefined;
6580
+ }
6581
+ return { license: licenseObj };
6582
+ })
6583
+ .filter((l) => l !== undefined);
6584
+ }
6536
6585
  if (versionSpecifiers && !versionSpecifiers.startsWith("==")) {
6537
6586
  properties.push({
6538
6587
  name: "cdx:pypi:versionSpecifiers",
@@ -7257,18 +7306,20 @@ export async function parseGoModData(goModData, gosumMap) {
7257
7306
  isTool = false;
7258
7307
  continue;
7259
7308
  }
7260
- if (l.includes("tool ")) {
7309
+ if (l.includes("tool ") || isTool) {
7261
7310
  continue;
7262
7311
  }
7263
- if (isTool) {
7312
+ if (l.startsWith("toolchain ")) {
7313
+ const toolchainVer = l.split(" ").pop().trim();
7314
+ parentComponent.properties = [
7315
+ { name: "cdx:go:toolchain", value: toolchainVer },
7316
+ ];
7264
7317
  continue;
7265
7318
  }
7266
-
7267
7319
  // Skip go.mod file headers, whitespace, and/or comments
7268
7320
  if (
7269
7321
  l.startsWith("go ") ||
7270
7322
  //TODO: should toolchain be considered as a dependency
7271
- l.startsWith("toolchain ") ||
7272
7323
  l.includes(")") ||
7273
7324
  l.trim() === "" ||
7274
7325
  l.trim().startsWith("//")
@@ -7378,14 +7429,17 @@ export async function parseGoListDep(rawOutput, gosumMap) {
7378
7429
  .split("\n")
7379
7430
  .filter((p) => p.trim().replace(/["']/g, "").length);
7380
7431
  for (const l of pkgs) {
7381
- const verArr = l.trim().replace(/["']/g, "").split(" ");
7432
+ const verArr = l.trim().replace(/["']/g, "").split("|");
7382
7433
  if (verArr && verArr.length >= 5) {
7383
7434
  const key = `${verArr[0]}-${verArr[1]}`;
7384
7435
  // Filter duplicates
7385
7436
  if (!keys_cache[key]) {
7386
7437
  keys_cache[key] = key;
7387
7438
  const version = verArr[1];
7388
- const gosumHash = gosumMap[`${verArr[0]}@${version}`];
7439
+ let gosumHash = gosumMap[`${verArr[0]}@${version}`];
7440
+ if (!gosumHash && verArr.length >= 8 && verArr[8]?.length) {
7441
+ gosumHash = `sha256-${verArr[8].replace("h1:", "")}`;
7442
+ }
7389
7443
  const component = await getGoPkgComponent(
7390
7444
  "",
7391
7445
  verArr[0],
@@ -7412,6 +7466,43 @@ export async function parseGoListDep(rawOutput, gosumMap) {
7412
7466
  value: verArr[2],
7413
7467
  },
7414
7468
  ];
7469
+ if (
7470
+ verArr.length >= 6 &&
7471
+ verArr[6]?.length &&
7472
+ verArr[6] !== "<nil>"
7473
+ ) {
7474
+ component.properties.push({
7475
+ name: "cdx:go:creation_time",
7476
+ value: verArr[6],
7477
+ });
7478
+ }
7479
+ if (verArr.length >= 7 && verArr[7]?.length) {
7480
+ component.properties.push({
7481
+ name: "cdx:go:deprecated",
7482
+ value: verArr[7],
7483
+ });
7484
+ }
7485
+ if (verArr.length >= 9 && verArr[9]?.length) {
7486
+ component.properties.push({
7487
+ name: "cdx:go:local_dir",
7488
+ value: verArr[9],
7489
+ });
7490
+ if (safeExistsSync(join(verArr[9], "LICENSE"))) {
7491
+ const licenseText = readFileSync(join(verArr[9], "LICENSE"), {
7492
+ encoding: "utf-8",
7493
+ });
7494
+ if (licenseText?.length) {
7495
+ component.licenses = [
7496
+ {
7497
+ license: {
7498
+ name: "CUSTOM",
7499
+ text: { contentType: "text/plain", content: licenseText },
7500
+ },
7501
+ },
7502
+ ];
7503
+ }
7504
+ }
7505
+ }
7415
7506
  if (verArr.length > 5 && verArr[5] === "true") {
7416
7507
  parentComponent = component;
7417
7508
  } else {
@@ -9835,82 +9926,250 @@ export function parseGitHubWorkflowData(f) {
9835
9926
  return pkgList;
9836
9927
  }
9837
9928
  const lines = ghwData.split("\n");
9929
+ // workflow-related values
9930
+ const workflowName = yamlObj.name || path.basename(f, path.extname(f));
9931
+ const workflowTriggers = yamlObj.on || yamlObj.true;
9932
+ const workflowPermissions = yamlObj.permissions || {};
9933
+ let hasWritePermissions = analyzePermissions(workflowPermissions);
9934
+ // GitHub of course supports strings such as "write-all" to make it easy to create supply-chain attacks.
9935
+ if (
9936
+ (typeof workflowPermissions === "string" ||
9937
+ workflowPermissions instanceof String) &&
9938
+ workflowPermissions.includes("write")
9939
+ ) {
9940
+ hasWritePermissions = true;
9941
+ }
9942
+ const workflowConcurrency = yamlObj.concurrency || {};
9943
+ const _workflowEnv = yamlObj.env || {};
9944
+ const hasIdTokenWrite = workflowPermissions?.["id-token"] === "write";
9838
9945
  for (const jobName of Object.keys(yamlObj.jobs)) {
9839
- if (yamlObj.jobs[jobName].steps) {
9840
- for (const step of yamlObj.jobs[jobName].steps) {
9841
- if (step.uses) {
9842
- const tmpA = step.uses.split("@");
9843
- if (tmpA.length !== 2) {
9844
- continue;
9946
+ const job = yamlObj.jobs[jobName];
9947
+ if (!job.steps) {
9948
+ continue;
9949
+ }
9950
+ // job-related values
9951
+ const jobRunner = job["runs-on"] || "unknown";
9952
+ const jobEnvironment = job.environment?.name || job.environment || "";
9953
+ const jobTimeout = job["timeout-minutes"] || null;
9954
+ const jobPermissions = job.permissions || {};
9955
+ const jobServices = job.services ? Object.keys(job.services) : [];
9956
+ let jobNeeds = job.needs || [];
9957
+ if (!Array.isArray(jobNeeds)) {
9958
+ jobNeeds = [jobNeeds];
9959
+ }
9960
+ const _jobIf = job.if || "";
9961
+ const _jobStrategy = job.strategy ? JSON.stringify(job.strategy) : "";
9962
+ const jobHasWritePermissions = analyzePermissions(jobPermissions);
9963
+ for (const step of job.steps) {
9964
+ if (step.uses) {
9965
+ const tmpA = step.uses.split("@");
9966
+ if (tmpA.length !== 2) {
9967
+ continue;
9968
+ }
9969
+ const groupName = tmpA[0];
9970
+ let name = groupName;
9971
+ let group = "";
9972
+ const tagOrCommit = tmpA[1];
9973
+ let version = tagOrCommit;
9974
+ const tmpB = groupName.split("/");
9975
+ if (tmpB.length >= 2) {
9976
+ name = tmpB.pop();
9977
+ group = tmpB.join("/");
9978
+ } else if (tmpB.length === 1) {
9979
+ name = tmpB[0];
9980
+ group = "";
9981
+ }
9982
+ const versionPinningType = getVersionPinningType(tagOrCommit);
9983
+ const isShaPinned = versionPinningType === "sha";
9984
+ const _isTagPinned = versionPinningType === "tag";
9985
+ const _isBranchRef = versionPinningType === "branch";
9986
+ let lineNum = -1;
9987
+ const stepLineMatch = ghwData.indexOf(step.uses);
9988
+ if (stepLineMatch >= 0) {
9989
+ lineNum = ghwData.substring(0, stepLineMatch).split("\n").length - 1;
9990
+ }
9991
+ if (lineNum >= 0 && lines[lineNum]) {
9992
+ const line = lines[lineNum];
9993
+ const commentMatch = line.match(/#\s*v?([0-9]+(?:\.[0-9]+)*)/);
9994
+ if (commentMatch?.[1]) {
9995
+ version = commentMatch[1];
9845
9996
  }
9846
- const groupName = tmpA[0];
9847
- let name = groupName;
9848
- let group = "";
9849
- const tagOrCommit = tmpA[1];
9850
- let version = tagOrCommit;
9851
- const tmpB = groupName.split("/");
9852
- if (tmpB.length >= 2) {
9853
- name = tmpB.pop();
9854
- group = tmpB.join("/");
9855
- } else if (tmpB.length === 1) {
9856
- name = tmpB[0];
9857
- group = "";
9997
+ }
9998
+ const key = `${group}-${name}-${version}`;
9999
+ let confidence = 0.6;
10000
+ if (!keys_cache[key] && name && version) {
10001
+ keys_cache[key] = key;
10002
+ let fullName = name;
10003
+ if (group.length) {
10004
+ fullName = `${group}/${name}`;
9858
10005
  }
9859
- let lineNum = -1;
9860
- const stepLineMatch = ghwData.indexOf(step.uses);
9861
- if (stepLineMatch >= 0) {
9862
- lineNum =
9863
- ghwData.substring(0, stepLineMatch).split("\n").length - 1;
10006
+ let purl = `pkg:github/${fullName}@${version}`;
10007
+ if (tagOrCommit && version !== tagOrCommit) {
10008
+ const qualifierDesc = tagOrCommit.startsWith("v")
10009
+ ? "tag"
10010
+ : "commit";
10011
+ purl = `${purl}?${qualifierDesc}=${tagOrCommit}`;
10012
+ confidence = 0.7;
9864
10013
  }
9865
- if (lineNum >= 0 && lines[lineNum]) {
9866
- const line = lines[lineNum];
9867
- const commentMatch = line.match(/#\s*v?([0-9]+(?:\.[0-9]+)*)/);
9868
- if (commentMatch?.[1]) {
9869
- version = commentMatch[1];
9870
- }
10014
+ const properties = [
10015
+ { name: "SrcFile", value: f },
10016
+ { name: "cdx:github:workflow:name", value: workflowName },
10017
+ { name: "cdx:github:job:name", value: jobName },
10018
+ {
10019
+ name: "cdx:github:job:runner",
10020
+ value: Array.isArray(jobRunner) ? jobRunner.join(",") : jobRunner,
10021
+ },
10022
+ { name: "cdx:github:action:uses", value: step.uses },
10023
+ {
10024
+ name: "cdx:github:action:versionPinningType",
10025
+ value: versionPinningType,
10026
+ },
10027
+ {
10028
+ name: "cdx:github:action:isShaPinned",
10029
+ value: isShaPinned.toString(),
10030
+ },
10031
+ ];
10032
+ if (step.name) {
10033
+ properties.push({ name: "cdx:github:step:name", value: step.name });
9871
10034
  }
9872
- const key = `${group}-${name}-${version}`;
9873
- let confidence = 0.6;
9874
- if (!keys_cache[key] && name && version) {
9875
- keys_cache[key] = key;
9876
- let fullName = name;
9877
- if (group.length) {
9878
- fullName = `${group}/${name}`;
9879
- }
9880
- let purl = `pkg:github/${fullName}@${version}`;
9881
- if (tagOrCommit && version !== tagOrCommit) {
9882
- const qualifierDesc = tagOrCommit.startsWith("v")
9883
- ? "tag"
9884
- : "commit";
9885
- purl = `${purl}?${qualifierDesc}=${tagOrCommit}`;
9886
- confidence = 0.7;
9887
- }
9888
- const properties = [
9889
- {
9890
- name: "SrcFile",
9891
- value: f,
10035
+ if (step.if) {
10036
+ properties.push({
10037
+ name: "cdx:github:step:condition",
10038
+ value: step.if,
10039
+ });
10040
+ }
10041
+ if (step["continue-on-error"]) {
10042
+ properties.push({
10043
+ name: "cdx:github:step:continueOnError",
10044
+ value: "true",
10045
+ });
10046
+ }
10047
+ if (step.timeout) {
10048
+ properties.push({
10049
+ name: "cdx:github:step:timeout",
10050
+ value: step.timeout.toString(),
10051
+ });
10052
+ }
10053
+ if (jobEnvironment) {
10054
+ properties.push({
10055
+ name: "cdx:github:job:environment",
10056
+ value: jobEnvironment,
10057
+ });
10058
+ }
10059
+ if (jobTimeout) {
10060
+ properties.push({
10061
+ name: "cdx:github:job:timeoutMinutes",
10062
+ value: jobTimeout.toString(),
10063
+ });
10064
+ }
10065
+ if (jobHasWritePermissions) {
10066
+ properties.push({
10067
+ name: "cdx:github:job:hasWritePermissions",
10068
+ value: "true",
10069
+ });
10070
+ }
10071
+ if (jobServices.length > 0) {
10072
+ properties.push({
10073
+ name: "cdx:github:job:services",
10074
+ value: jobServices.join(","),
10075
+ });
10076
+ }
10077
+ if (jobNeeds.length > 0) {
10078
+ properties.push({
10079
+ name: "cdx:github:job:needs",
10080
+ value: jobNeeds.join(","),
10081
+ });
10082
+ }
10083
+ if (hasWritePermissions) {
10084
+ properties.push({
10085
+ name: "cdx:github:workflow:hasWritePermissions",
10086
+ value: "true",
10087
+ });
10088
+ }
10089
+ if (hasIdTokenWrite) {
10090
+ properties.push({
10091
+ name: "cdx:github:workflow:hasIdTokenWrite",
10092
+ value: "true",
10093
+ });
10094
+ }
10095
+ if (workflowConcurrency?.group) {
10096
+ properties.push({
10097
+ name: "cdx:github:workflow:concurrencyGroup",
10098
+ value: workflowConcurrency.group,
10099
+ });
10100
+ }
10101
+ if (group?.startsWith("github/") || group === "actions") {
10102
+ properties.push({ name: "cdx:actions:isOfficial", value: "true" });
10103
+ }
10104
+ if (group?.startsWith("github/")) {
10105
+ properties.push({ name: "cdx:actions:isVerified", value: "true" });
10106
+ }
10107
+ if (workflowTriggers) {
10108
+ const triggers =
10109
+ typeof workflowTriggers === "string"
10110
+ ? workflowTriggers
10111
+ : Object.keys(workflowTriggers).join(",");
10112
+ properties.push({
10113
+ name: "cdx:github:workflow:triggers",
10114
+ value: triggers,
10115
+ });
10116
+ }
10117
+ pkgList.push({
10118
+ group,
10119
+ name,
10120
+ version,
10121
+ purl,
10122
+ properties,
10123
+ evidence: {
10124
+ identity: {
10125
+ field: "purl",
10126
+ confidence,
10127
+ methods: [
10128
+ {
10129
+ technique: "source-code-analysis",
10130
+ confidence,
10131
+ value: f,
10132
+ },
10133
+ ],
9892
10134
  },
9893
- ];
9894
- if (group?.startsWith("github/") || group === "actions") {
9895
- properties.push({
9896
- name: "cdx:actions:isOfficial",
9897
- value: "true",
9898
- });
9899
- }
10135
+ },
10136
+ });
10137
+ }
10138
+ }
10139
+ if (step.run) {
10140
+ const runLineNum = ghwData.indexOf(step.run);
10141
+ const runLine =
10142
+ runLineNum >= 0
10143
+ ? ghwData.substring(0, runLineNum).split("\n").length
10144
+ : -1;
10145
+ const pkgCommands = extractPackageManagerCommands(step.run);
10146
+ for (const pkgCmd of pkgCommands) {
10147
+ const key = `run-${pkgCmd.name}-${pkgCmd.version || "latest"}`;
10148
+ if (!keys_cache[key]) {
10149
+ keys_cache[key] = key;
9900
10150
  pkgList.push({
9901
- group,
9902
- name,
9903
- version,
9904
- purl,
9905
- properties,
10151
+ group: "",
10152
+ name: pkgCmd.name,
10153
+ version: pkgCmd.version || undefined,
10154
+ scope: "excluded",
10155
+ "bom-ref": uuidv4(),
10156
+ description: key,
10157
+ properties: [
10158
+ { name: "SrcFile", value: f },
10159
+ { name: "cdx:github:workflow:name", value: workflowName },
10160
+ { name: "cdx:github:job:name", value: jobName },
10161
+ { name: "cdx:github:step:type", value: "run" },
10162
+ { name: "cdx:github:step:command", value: pkgCmd.command },
10163
+ { name: "cdx:github:run:line", value: runLine.toString() },
10164
+ ],
9906
10165
  evidence: {
9907
10166
  identity: {
9908
10167
  field: "purl",
9909
- confidence,
10168
+ confidence: 0.5,
9910
10169
  methods: [
9911
10170
  {
9912
10171
  technique: "source-code-analysis",
9913
- confidence,
10172
+ confidence: 0.5,
9914
10173
  value: f,
9915
10174
  },
9916
10175
  ],
@@ -9925,6 +10184,103 @@ export function parseGitHubWorkflowData(f) {
9925
10184
  return pkgList;
9926
10185
  }
9927
10186
 
10187
+ /**
10188
+ * Analyze permissions for write access.
10189
+ *
10190
+ * Refer to https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#permissions
10191
+ */
10192
+ function analyzePermissions(permissions) {
10193
+ if (!permissions || typeof permissions !== "object") {
10194
+ return false;
10195
+ }
10196
+ const writePermissions = [
10197
+ "actions",
10198
+ "artifact-metadata",
10199
+ "attestations",
10200
+ "checks",
10201
+ "contents",
10202
+ "deployments",
10203
+ "id-token",
10204
+ "models",
10205
+ "discussions",
10206
+ "packages",
10207
+ "pages",
10208
+ "actions",
10209
+ "deployments",
10210
+ "issues",
10211
+ "pull-requests",
10212
+ "security-events",
10213
+ "statuses",
10214
+ ];
10215
+ for (const perm of writePermissions) {
10216
+ if (permissions[perm] === "write") {
10217
+ return true;
10218
+ }
10219
+ }
10220
+ return false;
10221
+ }
10222
+
10223
+ /**
10224
+ * Determine version pinning type for security assessment
10225
+ */
10226
+ function getVersionPinningType(versionRef) {
10227
+ if (!versionRef) {
10228
+ return "unknown";
10229
+ }
10230
+ if (/^[a-f0-9]{40}$/.test(versionRef)) {
10231
+ return "sha";
10232
+ }
10233
+ if (/^[a-f0-9]{7,}$/.test(versionRef)) {
10234
+ return "sha";
10235
+ }
10236
+ if (
10237
+ versionRef === "main" ||
10238
+ versionRef === "master" ||
10239
+ versionRef.includes("/")
10240
+ ) {
10241
+ return "branch";
10242
+ }
10243
+ return "tag";
10244
+ }
10245
+
10246
+ /**
10247
+ * Extract package manager commands from run steps
10248
+ */
10249
+ function extractPackageManagerCommands(runScript) {
10250
+ const commands = [];
10251
+ if (!runScript) {
10252
+ return commands;
10253
+ }
10254
+ const patterns = [
10255
+ { regex: /npm\s+(install|i|ci)\s+([^&\n|;]+)/g, name: "npm", type: "npm" },
10256
+ {
10257
+ regex: /yarn\s+(add|install)\s+([^&\n|;]+)/g,
10258
+ name: "yarn",
10259
+ type: "yarn",
10260
+ },
10261
+ { regex: /pip\s+(install)\s+([^&\n|;]+)/g, name: "pip", type: "pip" },
10262
+ { regex: /pip3\s+(install)\s+([^&\n|;]+)/g, name: "pip3", type: "pip" },
10263
+ { regex: /gem\s+(install)\s+([^&\n|;]+)/g, name: "gem", type: "gem" },
10264
+ { regex: /go\s+(get|install)\s+([^&\n|;]+)/g, name: "go", type: "go" },
10265
+ {
10266
+ regex: /cargo\s+(install|add)\s+([^&\n|;]+)/g,
10267
+ name: "cargo",
10268
+ type: "cargo",
10269
+ },
10270
+ ];
10271
+ for (const pattern of patterns) {
10272
+ let match;
10273
+ while ((match = pattern.regex.exec(runScript)) !== null) {
10274
+ commands.push({
10275
+ name: pattern.name,
10276
+ command: match[0],
10277
+ version: null,
10278
+ });
10279
+ }
10280
+ }
10281
+ return commands;
10282
+ }
10283
+
9928
10284
  export function parseCloudBuildData(cbwData) {
9929
10285
  const pkgList = [];
9930
10286
  const keys_cache = {};
@@ -10791,6 +11147,9 @@ export function parseCsPkgData(pkgData, pkgFile) {
10791
11147
  export function getPropertyGroupTextNodes(propsFiles) {
10792
11148
  const matches = {};
10793
11149
  for (const f of propsFiles) {
11150
+ if (!f) {
11151
+ continue;
11152
+ }
10794
11153
  let projects;
10795
11154
  try {
10796
11155
  const data = readFileSync(f, { encoding: "utf-8" });
@@ -11581,9 +11940,7 @@ export function parseCsPkgLockData(csLockData, pkgLockFile) {
11581
11940
  assetData.dependencies[aversion][adep].resolved;
11582
11941
  } else if (
11583
11942
  aversion.includes("/") &&
11584
- assetData.dependencies[aversionNoRuntime] &&
11585
- assetData.dependencies[aversionNoRuntime][adep] &&
11586
- assetData.dependencies[aversionNoRuntime][adep].resolved
11943
+ assetData.dependencies[aversionNoRuntime]?.[adep]?.resolved
11587
11944
  ) {
11588
11945
  adepResolvedVersion =
11589
11946
  assetData.dependencies[aversionNoRuntime][adep].resolved;
@@ -12653,7 +13010,7 @@ export async function collectMvnDependencies(
12653
13010
  }
12654
13011
 
12655
13012
  // Clean up
12656
- if (cleanup && tempDir && tempDir.startsWith(tmpdir()) && rmSync) {
13013
+ if (cleanup && tempDir?.startsWith(tmpdir()) && rmSync) {
12657
13014
  rmSync(tempDir, { recursive: true, force: true });
12658
13015
  }
12659
13016
  return jarNSMapping;
@@ -15014,7 +15371,11 @@ export async function getPipFrozenTree(
15014
15371
  thoughtLog(
15015
15372
  "Let me create a new virtual environment for installing the packages with pip.",
15016
15373
  );
15017
- result = safeSpawnSync(PYTHON_CMD, ["-m", "venv", tempVenvDir], {
15374
+ const venvCreationArgs = ["-m", "venv", tempVenvDir];
15375
+ if (isSecureMode) {
15376
+ venvCreationArgs.unshift("-S");
15377
+ }
15378
+ result = safeSpawnSync(PYTHON_CMD, venvCreationArgs, {
15018
15379
  shell: isWin,
15019
15380
  });
15020
15381
  if (result.status !== 0 || result.error) {
@@ -15065,12 +15426,18 @@ export async function getPipFrozenTree(
15065
15426
  "true",
15066
15427
  "--local",
15067
15428
  ];
15429
+ if (isSecureMode) {
15430
+ poetryConfigArgs.unshift("-S");
15431
+ }
15068
15432
  result = safeSpawnSync(PYTHON_CMD, poetryConfigArgs, {
15069
15433
  cwd: basePath,
15070
15434
  shell: isWin,
15071
15435
  });
15072
15436
  thoughtLog("Performing poetry install");
15073
15437
  let poetryInstallArgs = ["-m", "poetry", "install", "-n", "--no-root"];
15438
+ if (isSecureMode) {
15439
+ poetryInstallArgs.unshift("-S");
15440
+ }
15074
15441
  // Attempt to perform poetry install
15075
15442
  result = safeSpawnSync(PYTHON_CMD, poetryInstallArgs, {
15076
15443
  cwd: basePath,
@@ -15147,6 +15514,9 @@ export async function getPipFrozenTree(
15147
15514
  "install",
15148
15515
  "--disable-pip-version-check",
15149
15516
  ];
15517
+ if (isSecureMode) {
15518
+ pipInstallArgs.unshift("-S");
15519
+ }
15150
15520
  // Requirements.txt could be called with any name so best to check for not setup.py and not pyproject.toml
15151
15521
  if (
15152
15522
  !reqOrSetupFile.endsWith("setup.py") &&
@@ -15467,7 +15837,11 @@ export function getPipTreeForPackages(
15467
15837
  };
15468
15838
  if (!process.env.VIRTUAL_ENV && !process.env.CONDA_PREFIX) {
15469
15839
  // Create a virtual environment
15470
- result = safeSpawnSync(PYTHON_CMD, ["-m", "venv", tempVenvDir], {
15840
+ const venvCreationArgs = ["-m", "venv", tempVenvDir];
15841
+ if (isSecureMode) {
15842
+ venvCreationArgs.unshift("-S");
15843
+ }
15844
+ result = safeSpawnSync(PYTHON_CMD, venvCreationArgs, {
15471
15845
  shell: isWin,
15472
15846
  });
15473
15847
  if (result.status !== 0 || result.error) {
@@ -15490,6 +15864,9 @@ export function getPipTreeForPackages(
15490
15864
  }
15491
15865
  const python_cmd_for_tree = get_python_command_from_env(env);
15492
15866
  let pipInstallArgs = ["-m", "pip", "install", "--disable-pip-version-check"];
15867
+ if (isSecureMode) {
15868
+ pipInstallArgs.unshift("-S");
15869
+ }
15493
15870
  // Support for passing additional arguments to pip
15494
15871
  // Eg: --python-version 3.10 --ignore-requires-python --no-warn-conflicts
15495
15872
  if (process?.env?.PIP_INSTALL_ARGS) {
@@ -16059,29 +16436,24 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
16059
16436
  }
16060
16437
  }
16061
16438
  } else if (l.includes("dependency(")) {
16062
- let tmpA = l.split("dependency(");
16063
- if (tmpA?.length) {
16064
- if (!l.includes("_dependency") && !l.includes(".dependency")) {
16065
- tmpA = tmpA[tmpA.length - 1]
16066
- .split(", ")
16067
- .map((v) => v.replace(/['" )]/g, ""));
16068
- if (tmpA.length) {
16069
- name_list.push(tmpA[0]);
16070
- if (tmpA.length > 2 && tmpA[1].startsWith("version")) {
16071
- const tmpB = tmpA[1].split("version:");
16072
- if (tmpB && tmpB.length === 2) {
16073
- if (tmpB[1].includes(">") || tmpB[1].includes("<")) {
16074
- // We have a version specifier
16075
- versionSpecifiersMap[tmpA[0]] = tmpB[1];
16076
- } else if (
16077
- /^\d+/.test(tmpB[1]) &&
16078
- !tmpB[1].includes("${") &&
16079
- !tmpB[1].startsWith("@")
16080
- ) {
16081
- // We have a valid version
16082
- versionsMap[tmpA[0]] = tmpB[1];
16083
- }
16084
- }
16439
+ if (!l.includes("_dependency") && !l.includes(".dependency")) {
16440
+ const depMatch = l.match(/dependency\(\s*['"]?([^'",)\s]+)['"]?/);
16441
+ const depName = depMatch?.[1]?.trim();
16442
+ if (depName) {
16443
+ name_list.push(depName);
16444
+ const versionMatch = l.match(/version\s*:\s*['"]?([^'",)\s]+)['"]?/);
16445
+ const depVersion = versionMatch?.[1]?.trim();
16446
+ if (depVersion) {
16447
+ if (depVersion.includes(">") || depVersion.includes("<")) {
16448
+ // We have a version specifier
16449
+ versionSpecifiersMap[depName] = depVersion;
16450
+ } else if (
16451
+ /^\d+/.test(depVersion) &&
16452
+ !depVersion.includes("${") &&
16453
+ !depVersion.startsWith("@")
16454
+ ) {
16455
+ // We have a valid version
16456
+ versionsMap[depName] = depVersion;
16085
16457
  }
16086
16458
  }
16087
16459
  }