@cyclonedx/cdxgen 12.2.1 → 12.3.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.
Files changed (170) hide show
  1. package/README.md +239 -90
  2. package/bin/audit.js +191 -0
  3. package/bin/cdxgen.js +513 -167
  4. package/bin/convert.js +99 -0
  5. package/bin/evinse.js +23 -0
  6. package/bin/repl.js +339 -8
  7. package/bin/sign.js +8 -0
  8. package/bin/validate.js +8 -0
  9. package/bin/verify.js +8 -0
  10. package/data/container-knowledge-index.json +125 -0
  11. package/data/gtfobins-index.json +6296 -0
  12. package/data/lolbas-index.json +150 -0
  13. package/data/queries-darwin.json +63 -3
  14. package/data/queries-win.json +45 -3
  15. package/data/queries.json +74 -2
  16. package/data/rules/chrome-extensions.yaml +240 -0
  17. package/data/rules/ci-permissions.yaml +478 -18
  18. package/data/rules/container-risk.yaml +270 -0
  19. package/data/rules/obom-runtime.yaml +891 -0
  20. package/data/rules/package-integrity.yaml +49 -0
  21. package/data/spdx-export.schema.json +6794 -0
  22. package/data/spdx-model-v3.0.1.jsonld +15999 -0
  23. package/lib/audit/index.js +1924 -0
  24. package/lib/audit/index.poku.js +1488 -0
  25. package/lib/audit/progress.js +137 -0
  26. package/lib/audit/progress.poku.js +188 -0
  27. package/lib/audit/reporters.js +618 -0
  28. package/lib/audit/scoring.js +310 -0
  29. package/lib/audit/scoring.poku.js +341 -0
  30. package/lib/audit/targets.js +260 -0
  31. package/lib/audit/targets.poku.js +331 -0
  32. package/lib/cli/index.js +154 -11
  33. package/lib/cli/index.poku.js +251 -0
  34. package/lib/helpers/analyzer.js +446 -2
  35. package/lib/helpers/analyzer.poku.js +72 -1
  36. package/lib/helpers/annotationFormatter.js +49 -0
  37. package/lib/helpers/annotationFormatter.poku.js +44 -0
  38. package/lib/helpers/bomUtils.js +36 -0
  39. package/lib/helpers/bomUtils.poku.js +51 -0
  40. package/lib/helpers/caxa.js +2 -2
  41. package/lib/helpers/chromextutils.js +1153 -0
  42. package/lib/helpers/chromextutils.poku.js +493 -0
  43. package/lib/helpers/ciParsers/githubActions.js +1632 -45
  44. package/lib/helpers/ciParsers/githubActions.poku.js +853 -1
  45. package/lib/helpers/containerRisk.js +186 -0
  46. package/lib/helpers/containerRisk.poku.js +52 -0
  47. package/lib/helpers/display.js +241 -59
  48. package/lib/helpers/display.poku.js +162 -2
  49. package/lib/helpers/exportUtils.js +123 -0
  50. package/lib/helpers/exportUtils.poku.js +60 -0
  51. package/lib/helpers/formulationParsers.js +69 -0
  52. package/lib/helpers/formulationParsers.poku.js +44 -0
  53. package/lib/helpers/gtfobins.js +189 -0
  54. package/lib/helpers/gtfobins.poku.js +49 -0
  55. package/lib/helpers/lolbas.js +267 -0
  56. package/lib/helpers/lolbas.poku.js +39 -0
  57. package/lib/helpers/osqueryTransform.js +84 -0
  58. package/lib/helpers/osqueryTransform.poku.js +49 -0
  59. package/lib/helpers/provenanceUtils.js +193 -0
  60. package/lib/helpers/provenanceUtils.poku.js +145 -0
  61. package/lib/helpers/pylockutils.js +281 -0
  62. package/lib/helpers/pylockutils.poku.js +48 -0
  63. package/lib/helpers/registryProvenance.js +793 -0
  64. package/lib/helpers/registryProvenance.poku.js +452 -0
  65. package/lib/helpers/source.js +1267 -0
  66. package/lib/helpers/source.poku.js +771 -0
  67. package/lib/helpers/spdxUtils.js +97 -0
  68. package/lib/helpers/spdxUtils.poku.js +70 -0
  69. package/lib/helpers/unicodeScan.js +147 -0
  70. package/lib/helpers/unicodeScan.poku.js +45 -0
  71. package/lib/helpers/utils.js +700 -128
  72. package/lib/helpers/utils.poku.js +877 -80
  73. package/lib/managers/binary.js +29 -5
  74. package/lib/managers/docker.js +179 -52
  75. package/lib/managers/docker.poku.js +327 -28
  76. package/lib/managers/oci.js +107 -23
  77. package/lib/managers/oci.poku.js +132 -0
  78. package/lib/server/openapi.yaml +17 -0
  79. package/lib/server/server.js +225 -336
  80. package/lib/server/server.poku.js +16 -10
  81. package/lib/stages/postgen/annotator.js +7 -0
  82. package/lib/stages/postgen/annotator.poku.js +40 -0
  83. package/lib/stages/postgen/auditBom.js +19 -3
  84. package/lib/stages/postgen/auditBom.poku.js +1729 -67
  85. package/lib/stages/postgen/postgen.js +40 -0
  86. package/lib/stages/postgen/postgen.poku.js +47 -0
  87. package/lib/stages/postgen/ruleEngine.js +80 -2
  88. package/lib/stages/postgen/spdxConverter.js +796 -0
  89. package/lib/stages/postgen/spdxConverter.poku.js +341 -0
  90. package/lib/validator/bomValidator.js +232 -0
  91. package/lib/validator/bomValidator.poku.js +70 -0
  92. package/lib/validator/complianceRules.js +70 -7
  93. package/lib/validator/complianceRules.poku.js +30 -0
  94. package/lib/validator/reporters/annotations.js +2 -2
  95. package/lib/validator/reporters/console.js +11 -0
  96. package/lib/validator/reporters.poku.js +13 -0
  97. package/package.json +10 -7
  98. package/types/bin/audit.d.ts +3 -0
  99. package/types/bin/audit.d.ts.map +1 -0
  100. package/types/bin/convert.d.ts +3 -0
  101. package/types/bin/convert.d.ts.map +1 -0
  102. package/types/bin/repl.d.ts.map +1 -1
  103. package/types/lib/audit/index.d.ts +115 -0
  104. package/types/lib/audit/index.d.ts.map +1 -0
  105. package/types/lib/audit/progress.d.ts +27 -0
  106. package/types/lib/audit/progress.d.ts.map +1 -0
  107. package/types/lib/audit/reporters.d.ts +35 -0
  108. package/types/lib/audit/reporters.d.ts.map +1 -0
  109. package/types/lib/audit/scoring.d.ts +35 -0
  110. package/types/lib/audit/scoring.d.ts.map +1 -0
  111. package/types/lib/audit/targets.d.ts +63 -0
  112. package/types/lib/audit/targets.d.ts.map +1 -0
  113. package/types/lib/cli/index.d.ts +8 -0
  114. package/types/lib/cli/index.d.ts.map +1 -1
  115. package/types/lib/helpers/analyzer.d.ts +13 -0
  116. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  117. package/types/lib/helpers/annotationFormatter.d.ts +23 -0
  118. package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
  119. package/types/lib/helpers/bomUtils.d.ts +5 -0
  120. package/types/lib/helpers/bomUtils.d.ts.map +1 -0
  121. package/types/lib/helpers/chromextutils.d.ts +97 -0
  122. package/types/lib/helpers/chromextutils.d.ts.map +1 -0
  123. package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
  124. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  125. package/types/lib/helpers/containerRisk.d.ts +17 -0
  126. package/types/lib/helpers/containerRisk.d.ts.map +1 -0
  127. package/types/lib/helpers/display.d.ts +4 -1
  128. package/types/lib/helpers/display.d.ts.map +1 -1
  129. package/types/lib/helpers/exportUtils.d.ts +40 -0
  130. package/types/lib/helpers/exportUtils.d.ts.map +1 -0
  131. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  132. package/types/lib/helpers/gtfobins.d.ts +17 -0
  133. package/types/lib/helpers/gtfobins.d.ts.map +1 -0
  134. package/types/lib/helpers/lolbas.d.ts +16 -0
  135. package/types/lib/helpers/lolbas.d.ts.map +1 -0
  136. package/types/lib/helpers/osqueryTransform.d.ts +7 -0
  137. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
  138. package/types/lib/helpers/provenanceUtils.d.ts +90 -0
  139. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
  140. package/types/lib/helpers/pylockutils.d.ts +51 -0
  141. package/types/lib/helpers/pylockutils.d.ts.map +1 -0
  142. package/types/lib/helpers/registryProvenance.d.ts +17 -0
  143. package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
  144. package/types/lib/helpers/source.d.ts +141 -0
  145. package/types/lib/helpers/source.d.ts.map +1 -0
  146. package/types/lib/helpers/spdxUtils.d.ts +2 -0
  147. package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
  148. package/types/lib/helpers/unicodeScan.d.ts +46 -0
  149. package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
  150. package/types/lib/helpers/utils.d.ts +29 -11
  151. package/types/lib/helpers/utils.d.ts.map +1 -1
  152. package/types/lib/managers/binary.d.ts.map +1 -1
  153. package/types/lib/managers/docker.d.ts.map +1 -1
  154. package/types/lib/managers/oci.d.ts.map +1 -1
  155. package/types/lib/server/server.d.ts +0 -36
  156. package/types/lib/server/server.d.ts.map +1 -1
  157. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  158. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  159. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  160. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  161. package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
  162. package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
  163. package/types/lib/validator/bomValidator.d.ts +1 -0
  164. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  165. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  166. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  167. package/types/bin/dependencies.d.ts +0 -3
  168. package/types/bin/dependencies.d.ts.map +0 -1
  169. package/types/bin/licenses.d.ts +0 -3
  170. package/types/bin/licenses.d.ts.map +0 -1
package/lib/cli/index.js CHANGED
@@ -24,9 +24,16 @@ import { parse as loadYaml } from "yaml";
24
24
 
25
25
  import { findJSImportsExports } from "../helpers/analyzer.js";
26
26
  import { parseCaxaMetadata } from "../helpers/caxa.js";
27
+ import {
28
+ CHROME_EXTENSION_PURL_TYPE,
29
+ collectChromeExtensionsFromPath,
30
+ collectInstalledChromeExtensions,
31
+ discoverChromiumExtensionDirs,
32
+ } from "../helpers/chromextutils.js";
27
33
  import { mergeDependencies, trimComponents } from "../helpers/depsUtils.js";
28
34
  import { GIT_COMMAND } from "../helpers/envcontext.js";
29
35
  import { thoughtLog } from "../helpers/logger.js";
36
+ import { isPyLockFile } from "../helpers/pylockutils.js";
30
37
  import {
31
38
  buildDependencyTrackBomPayload,
32
39
  getDependencyTrackBomUrl,
@@ -3019,7 +3026,7 @@ export async function createNodejsBom(path, options) {
3019
3026
  if (!wpkgJsonFiles?.length) {
3020
3027
  if (!workspaceWarningShown) {
3021
3028
  workspaceWarningShown = true;
3022
- console.log(
3029
+ console.warn(
3023
3030
  `Unable to find any package.json files belonging to the workspace '${awp}' referred in ${f}. To improve SBOM precision, run cdxgen from the directory containing the complete source code.`,
3024
3031
  );
3025
3032
  }
@@ -3109,7 +3116,7 @@ export async function createNodejsBom(path, options) {
3109
3116
  }
3110
3117
  if (!Object.keys(parentComponent).length) {
3111
3118
  if (safeExistsSync(packageJsonF)) {
3112
- const pcs = await parsePkgJson(packageJsonF, true);
3119
+ const pcs = await parsePkgJson(packageJsonF, true, true);
3113
3120
  if (pcs.length && Object.keys(pcs[0]).length) {
3114
3121
  parentComponent = { ...pcs[0] };
3115
3122
  parentComponent.type = "application";
@@ -3195,7 +3202,7 @@ export async function createNodejsBom(path, options) {
3195
3202
  const basePath = dirname(f);
3196
3203
  const packageJsonF = join(basePath, "package.json");
3197
3204
  if (safeExistsSync(packageJsonF)) {
3198
- const pcs = await parsePkgJson(packageJsonF, true);
3205
+ const pcs = await parsePkgJson(packageJsonF, true, true);
3199
3206
  if (pcs.length && Object.keys(pcs[0]).length) {
3200
3207
  tmpParentComponent = { ...pcs[0] };
3201
3208
  tmpParentComponent.type = "application";
@@ -3365,7 +3372,7 @@ export async function createNodejsBom(path, options) {
3365
3372
  if (!wpkgJsonFiles?.length) {
3366
3373
  if (!workspaceWarningShown) {
3367
3374
  workspaceWarningShown = true;
3368
- console.log(
3375
+ console.warn(
3369
3376
  `Unable to find any package.json files belonging to the workspace '${awp}' referred in ${packageJsonFile}. To improve SBOM precision, run cdxgen from the directory containing the complete source code.`,
3370
3377
  );
3371
3378
  }
@@ -3419,7 +3426,7 @@ export async function createNodejsBom(path, options) {
3419
3426
  // Determine the parent component
3420
3427
  const packageJsonF = join(basePath, "package.json");
3421
3428
  if (safeExistsSync(packageJsonF)) {
3422
- const pcs = await parsePkgJson(packageJsonF, true);
3429
+ const pcs = await parsePkgJson(packageJsonF, true, true);
3423
3430
  if (pcs.length && Object.keys(pcs[0]).length) {
3424
3431
  const tmpParentComponent = { ...pcs[0] };
3425
3432
  tmpParentComponent.type = "application";
@@ -3528,7 +3535,7 @@ export async function createNodejsBom(path, options) {
3528
3535
  }
3529
3536
  if (!parentComponent || !Object.keys(parentComponent).length) {
3530
3537
  if (safeExistsSync(join(path, "package.json"))) {
3531
- const pcs = await parsePkgJson(join(path, "package.json"), true);
3538
+ const pcs = await parsePkgJson(join(path, "package.json"), true, true);
3532
3539
  if (pcs.length && Object.keys(pcs[0]).length) {
3533
3540
  parentComponent = { ...pcs[0] };
3534
3541
  parentComponent.type = "application";
@@ -3796,6 +3803,14 @@ export async function createPythonBom(path, options) {
3796
3803
  if (uvLockFiles?.length) {
3797
3804
  poetryFiles = poetryFiles.concat(uvLockFiles);
3798
3805
  }
3806
+ const pyLockFiles = getAllFiles(
3807
+ path,
3808
+ `${options.multiProject ? "**/" : ""}pylock*.toml`,
3809
+ options,
3810
+ )?.filter((f) => isPyLockFile(f));
3811
+ if (pyLockFiles?.length) {
3812
+ poetryFiles = poetryFiles.concat(pyLockFiles);
3813
+ }
3799
3814
  let reqFiles = getAllFiles(
3800
3815
  path,
3801
3816
  `${options.multiProject ? "**/" : ""}*requirements*.txt`,
@@ -3861,7 +3876,9 @@ export async function createPythonBom(path, options) {
3861
3876
  }
3862
3877
  // When we identify uv lock files, do not parse requirements files
3863
3878
  const requirementsMode =
3864
- (reqFiles?.length || reqDirFiles?.length) && !uvLockFiles.length;
3879
+ (reqFiles?.length || reqDirFiles?.length) &&
3880
+ !uvLockFiles.length &&
3881
+ !pyLockFiles?.length;
3865
3882
  const poetryMode = poetryFiles?.length;
3866
3883
 
3867
3884
  // TODO: Support for nested directories
@@ -3880,6 +3897,12 @@ export async function createPythonBom(path, options) {
3880
3897
  if (retMap?.workspaceWarningShown) {
3881
3898
  options.failOnError && process.exit(1);
3882
3899
  }
3900
+ if (retMap?.pyLockProperties?.length) {
3901
+ parentComponent.properties = parentComponent.properties || [];
3902
+ parentComponent.properties = parentComponent.properties.concat(
3903
+ retMap.pyLockProperties,
3904
+ );
3905
+ }
3883
3906
  if (retMap.pkgList?.length) {
3884
3907
  pkgList = pkgList.concat(retMap.pkgList);
3885
3908
  pkgList = trimComponents(pkgList);
@@ -3899,7 +3922,11 @@ export async function createPythonBom(path, options) {
3899
3922
  parentComponent,
3900
3923
  );
3901
3924
  }
3902
- if ((options.deep || !dependencies.length) && !f.endsWith("uv.lock")) {
3925
+ if (
3926
+ (options.deep || !dependencies.length) &&
3927
+ !f.endsWith("uv.lock") &&
3928
+ !isPyLockFile(f)
3929
+ ) {
3903
3930
  if (options.installDeps) {
3904
3931
  retMap = await getPipFrozenTree(
3905
3932
  basePath,
@@ -5692,15 +5719,27 @@ export async function createCocoaBom(path, options) {
5692
5719
  for (const podFile of cocoaFiles) {
5693
5720
  const projectPath = dirname(podFile);
5694
5721
  const lockFile = `${podFile}.lock`;
5695
- if (!safeExistsSync(lockFile) || options.deep) {
5722
+ let missingLockWarningShown = false;
5723
+ if (!safeExistsSync(lockFile)) {
5696
5724
  if (options.installDeps) {
5697
5725
  executePodCommand(["install"], projectPath, options);
5698
5726
  } else {
5699
5727
  console.log(
5700
5728
  "No 'Podfile.lock' found and '--no-install-deps' is set -- A Podfile.lock is needed to parse dependencies!",
5701
5729
  );
5730
+ missingLockWarningShown = true;
5702
5731
  options.failOnError && process.exit(1);
5703
5732
  }
5733
+ } else if (options.deep && options.installDeps) {
5734
+ executePodCommand(["install"], projectPath, options);
5735
+ }
5736
+ if (!safeExistsSync(lockFile)) {
5737
+ if (!missingLockWarningShown) {
5738
+ console.log(
5739
+ `No 'Podfile.lock' found for ${projectPath}. Skipping CocoaPods dependency parsing for this project.`,
5740
+ );
5741
+ }
5742
+ continue;
5704
5743
  }
5705
5744
  const parentComponent = await buildObjectForCocoaPod(
5706
5745
  {
@@ -7288,6 +7327,61 @@ export async function createVscodeExtensionBom(path, options) {
7288
7327
  });
7289
7328
  }
7290
7329
 
7330
+ /**
7331
+ * Function to create BOM for installed Chrome and Chromium-based browser extensions.
7332
+ *
7333
+ * @param {string} path to the project path or a directly provided extension path
7334
+ * @param {Object} options Parse options from the cli
7335
+ * @returns {Promise<Object>} Promise resolving to BOM object
7336
+ */
7337
+ export async function createChromeExtensionBom(path, options) {
7338
+ let dependencies = [];
7339
+ let sourcePaths = [];
7340
+ const directResult = collectChromeExtensionsFromPath(path);
7341
+ const chromeDirs = discoverChromiumExtensionDirs();
7342
+ let pkgList = directResult.components || [];
7343
+ if (directResult.extensionDirs?.length) {
7344
+ sourcePaths = directResult.extensionDirs.slice();
7345
+ for (const extDir of directResult.extensionDirs) {
7346
+ const deepResult = await analyzeExtensionDir(extDir, options);
7347
+ if (deepResult.pkgList.length) {
7348
+ pkgList = pkgList.concat(deepResult.pkgList);
7349
+ }
7350
+ if (deepResult.dependencies.length) {
7351
+ dependencies = mergeDependencies(dependencies, deepResult.dependencies);
7352
+ }
7353
+ }
7354
+ }
7355
+ if (pkgList.length && DEBUG_MODE) {
7356
+ thoughtLog(
7357
+ `Found ${pkgList.length} component(s) from direct Chrome extension path scan`,
7358
+ );
7359
+ }
7360
+ if (chromeDirs.length) {
7361
+ if (DEBUG_MODE) {
7362
+ thoughtLog(
7363
+ `Discovered Chromium extension directories: ${chromeDirs.map((d) => `${d.browser} (${d.channel}): ${d.dir}`).join(", ")}`,
7364
+ );
7365
+ }
7366
+ if (!pkgList.length) {
7367
+ pkgList = collectInstalledChromeExtensions(chromeDirs);
7368
+ sourcePaths = chromeDirs.map((d) => d.dir);
7369
+ }
7370
+ if (DEBUG_MODE && pkgList.length && !directResult.components?.length) {
7371
+ thoughtLog(
7372
+ `Found ${pkgList.length} Chrome/Chromium extension(s) from ${chromeDirs.length} browser location(s)`,
7373
+ );
7374
+ }
7375
+ }
7376
+ pkgList = trimComponents(pkgList);
7377
+ return buildBomNSData(options, pkgList, CHROME_EXTENSION_PURL_TYPE, {
7378
+ src: path,
7379
+ filename: sourcePaths.join(", "),
7380
+ nsMapping: {},
7381
+ dependencies,
7382
+ });
7383
+ }
7384
+
7291
7385
  /**
7292
7386
  * Analyze an extracted extension directory for bundled dependencies.
7293
7387
  * Looks for npm lock files, node_modules, package.json files, minified JS,
@@ -8208,6 +8302,46 @@ export async function createMultiXBom(pathList, options) {
8208
8302
  }
8209
8303
  }
8210
8304
  }
8305
+ if (hasAnyProjectType(["chrome-extension"], options)) {
8306
+ let isExplicitChromeExtensionPath = false;
8307
+ if (safeExistsSync(path)) {
8308
+ if (basename(path) === "manifest.json") {
8309
+ isExplicitChromeExtensionPath = true;
8310
+ } else {
8311
+ try {
8312
+ isExplicitChromeExtensionPath =
8313
+ statSync(path).isDirectory() &&
8314
+ safeExistsSync(join(path, "manifest.json"));
8315
+ } catch (_err) {
8316
+ isExplicitChromeExtensionPath = false;
8317
+ }
8318
+ }
8319
+ }
8320
+ if (isExplicitChromeExtensionPath || !options.__didScanChromeExtensions) {
8321
+ if (!isExplicitChromeExtensionPath) {
8322
+ options.__didScanChromeExtensions = true;
8323
+ }
8324
+ bomData = await createChromeExtensionBom(path, options);
8325
+ if (bomData?.bomJson?.components?.length) {
8326
+ if (DEBUG_MODE) {
8327
+ console.log(
8328
+ `Found ${bomData.bomJson.components.length} Chrome extension(s) on this host`,
8329
+ );
8330
+ }
8331
+ components = components.concat(bomData.bomJson.components);
8332
+ dependencies = mergeDependencies(
8333
+ dependencies,
8334
+ bomData.bomJson.dependencies,
8335
+ );
8336
+ if (
8337
+ bomData.parentComponent &&
8338
+ Object.keys(bomData.parentComponent).length
8339
+ ) {
8340
+ parentSubComponents.push(bomData.parentComponent);
8341
+ }
8342
+ }
8343
+ }
8344
+ }
8211
8345
  // Collect any crypto keys
8212
8346
  if (options.specVersion >= 1.6 && options.includeCrypto) {
8213
8347
  if (!hasAnyProjectType(["oci"], options, false)) {
@@ -8362,10 +8496,16 @@ export async function createXBom(path, options) {
8362
8496
  // python
8363
8497
  const pipenvMode = safeExistsSync(join(path, "Pipfile"));
8364
8498
  const poetryMode = safeExistsSync(join(path, "poetry.lock"));
8499
+ const pyLockFiles = getAllFiles(
8500
+ path,
8501
+ `${options.multiProject ? "**/" : ""}pylock*.toml`,
8502
+ options,
8503
+ ).filter((f) => isPyLockFile(f));
8504
+ const pyLockMode = pyLockFiles.length > 0;
8365
8505
  const pyProjectMode =
8366
- !poetryMode && safeExistsSync(join(path, "pyproject.toml"));
8506
+ !poetryMode && !pyLockMode && safeExistsSync(join(path, "pyproject.toml"));
8367
8507
  const setupPyMode = safeExistsSync(join(path, "setup.py"));
8368
- if (pipenvMode || poetryMode || pyProjectMode || setupPyMode) {
8508
+ if (pipenvMode || poetryMode || pyLockMode || pyProjectMode || setupPyMode) {
8369
8509
  return await createPythonBom(path, options);
8370
8510
  }
8371
8511
  const reqFiles = getAllFiles(
@@ -8927,6 +9067,9 @@ export async function createBom(path, options) {
8927
9067
  if (PROJECT_TYPE_ALIASES["vscode-extension"].includes(projectType[0])) {
8928
9068
  return await createVscodeExtensionBom(path, options);
8929
9069
  }
9070
+ if (PROJECT_TYPE_ALIASES["chrome-extension"].includes(projectType[0])) {
9071
+ return await createChromeExtensionBom(path, options);
9072
+ }
8930
9073
  switch (projectType[0]) {
8931
9074
  case "jar":
8932
9075
  return createJarBom(path, options);
@@ -1,7 +1,30 @@
1
+ import {
2
+ mkdirSync,
3
+ mkdtempSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { dirname, join } from "node:path";
10
+ import process from "node:process";
11
+ import { fileURLToPath } from "node:url";
12
+
1
13
  import esmock from "esmock";
2
14
  import { assert, describe, it } from "poku";
3
15
  import sinon from "sinon";
4
16
 
17
+ import { createChromeExtensionBom } from "./index.js";
18
+
19
+ const fixtureDir = join(
20
+ dirname(fileURLToPath(import.meta.url)),
21
+ "..",
22
+ "..",
23
+ "test",
24
+ "data",
25
+ "chrome-extensions",
26
+ );
27
+
5
28
  describe("CLI tests", () => {
6
29
  describe("submitBom()", () => {
7
30
  it("should successfully report the SBOM with given project id, name, version and a single tag", async () => {
@@ -238,4 +261,232 @@ describe("CLI tests", () => {
238
261
  sinon.assert.notCalled(gotStub);
239
262
  });
240
263
  });
264
+
265
+ describe("createCocoaBom()", () => {
266
+ it("should skip missing Podfile.lock when failOnError is false", async () => {
267
+ const { createCocoaBom } = await import("./index.js");
268
+ const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-cocoa-"));
269
+ const podFile = join(tempDir, "Podfile");
270
+ writeFileSync(
271
+ podFile,
272
+ "platform :ios, '14.0'\n\ntarget 'TestApp' do\nend\n",
273
+ "utf-8",
274
+ );
275
+ const consoleLogStub = sinon.stub(console, "log");
276
+ try {
277
+ const bomData = await createCocoaBom(tempDir, {
278
+ deep: false,
279
+ failOnError: false,
280
+ installDeps: false,
281
+ multiProject: false,
282
+ });
283
+ assert.equal(bomData, undefined);
284
+ sinon.assert.calledWithMatch(
285
+ consoleLogStub,
286
+ sinon.match("No 'Podfile.lock' found"),
287
+ );
288
+ } finally {
289
+ consoleLogStub.restore();
290
+ rmSync(tempDir, { force: true, recursive: true });
291
+ }
292
+ });
293
+
294
+ it("should not warn or exit for deep mode when Podfile.lock exists", async () => {
295
+ const { createCocoaBom } = await import("./index.js");
296
+ const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-cocoa-deep-"));
297
+ const podFile = join(tempDir, "Podfile");
298
+ const lockFile = join(tempDir, "Podfile.lock");
299
+ writeFileSync(
300
+ podFile,
301
+ "platform :ios, '14.0'\n\ntarget 'TestApp' do\nend\n",
302
+ "utf-8",
303
+ );
304
+ writeFileSync(lockFile, "PODS: []\nDEPENDENCIES: []\n", "utf-8");
305
+ const processExitStub = sinon.stub(process, "exit");
306
+ try {
307
+ await createCocoaBom(tempDir, {
308
+ deep: true,
309
+ failOnError: true,
310
+ installDeps: false,
311
+ multiProject: false,
312
+ });
313
+ sinon.assert.notCalled(processExitStub);
314
+ } finally {
315
+ processExitStub.restore();
316
+ rmSync(tempDir, { force: true, recursive: true });
317
+ }
318
+ });
319
+ });
320
+
321
+ describe("createChromeExtensionBom()", () => {
322
+ it("should catalog a directly provided extension and its node dependencies", async () => {
323
+ const tempRoot = mkdtempSync(join(tmpdir(), "cdxgen-chrome-ext-cli-"));
324
+ const extensionId = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
325
+ const extensionIdDir = join(tempRoot, extensionId);
326
+ const extensionVersionDir = join(extensionIdDir, "1.2.3");
327
+ try {
328
+ mkdirSync(extensionVersionDir, { recursive: true });
329
+ writeFileSync(
330
+ join(extensionVersionDir, "manifest.json"),
331
+ JSON.stringify({
332
+ manifest_version: 3,
333
+ name: "CLI Test Extension",
334
+ description: "Direct path test",
335
+ version: "1.2.3",
336
+ }),
337
+ "utf-8",
338
+ );
339
+ writeFileSync(
340
+ join(extensionVersionDir, "package.json"),
341
+ JSON.stringify({
342
+ name: "chrome-extension-cli-test",
343
+ version: "1.2.3",
344
+ dependencies: {
345
+ "left-pad": "1.3.0",
346
+ },
347
+ }),
348
+ "utf-8",
349
+ );
350
+ writeFileSync(
351
+ join(extensionVersionDir, "package-lock.json"),
352
+ JSON.stringify({
353
+ name: "chrome-extension-cli-test",
354
+ version: "1.2.3",
355
+ lockfileVersion: 3,
356
+ requires: true,
357
+ packages: {
358
+ "": {
359
+ name: "chrome-extension-cli-test",
360
+ version: "1.2.3",
361
+ dependencies: {
362
+ "left-pad": "1.3.0",
363
+ },
364
+ },
365
+ "node_modules/left-pad": {
366
+ version: "1.3.0",
367
+ },
368
+ },
369
+ }),
370
+ "utf-8",
371
+ );
372
+ const bomData = await createChromeExtensionBom(extensionIdDir, {
373
+ projectType: ["chrome-extension"],
374
+ multiProject: false,
375
+ });
376
+ const components = bomData?.bomJson?.components || [];
377
+ assert.ok(
378
+ components.some(
379
+ (component) =>
380
+ component.purl === `pkg:chrome-extension/${extensionId}@1.2.3`,
381
+ ),
382
+ );
383
+ assert.ok(
384
+ components.some(
385
+ (component) =>
386
+ component.name === "left-pad" &&
387
+ component.purl?.startsWith("pkg:npm/left-pad@1.3.0"),
388
+ ),
389
+ );
390
+ } finally {
391
+ rmSync(tempRoot, { recursive: true, force: true });
392
+ }
393
+ });
394
+
395
+ it("should parse an AI-targeted community extension manifest from direct version path", async () => {
396
+ const tempRoot = mkdtempSync(join(tmpdir(), "cdxgen-chrome-ext-cli-ai-"));
397
+ const extensionId = "llllllllllllllllllllllllllllllll";
398
+ const extensionVersion = "1.0.0";
399
+ const extensionVersionDir = join(tempRoot, extensionId, extensionVersion);
400
+ try {
401
+ mkdirSync(extensionVersionDir, { recursive: true });
402
+ writeFileSync(
403
+ join(extensionVersionDir, "manifest.json"),
404
+ readFileSync(
405
+ join(fixtureDir, "chrome-copilottts-manifest.json"),
406
+ "utf-8",
407
+ ),
408
+ "utf-8",
409
+ );
410
+ const bomData = await createChromeExtensionBom(extensionVersionDir, {
411
+ projectType: ["chrome-extension"],
412
+ multiProject: false,
413
+ });
414
+ const extensionComponent = (bomData?.bomJson?.components || []).find(
415
+ (component) =>
416
+ component.purl ===
417
+ `pkg:chrome-extension/${extensionId}@${extensionVersion}`,
418
+ );
419
+ assert.ok(extensionComponent, "expected direct extension component");
420
+ const properties = extensionComponent.properties || [];
421
+ assert.ok(
422
+ properties.some(
423
+ (prop) =>
424
+ prop.name === "cdx:chrome-extension:permissions" &&
425
+ prop.value.includes("scripting"),
426
+ ),
427
+ );
428
+ assert.ok(
429
+ properties.some(
430
+ (prop) =>
431
+ prop.name === "cdx:chrome-extension:capability:codeInjection" &&
432
+ prop.value === "true",
433
+ ),
434
+ );
435
+ assert.ok(
436
+ properties.some(
437
+ (prop) =>
438
+ prop.name === "cdx:chrome-extension:hostPermissions" &&
439
+ prop.value.includes("https://github.com/copilot/tasks/*"),
440
+ ),
441
+ );
442
+ } finally {
443
+ rmSync(tempRoot, { recursive: true, force: true });
444
+ }
445
+ });
446
+ });
447
+
448
+ describe("createMultiXBom()", () => {
449
+ it("should scan installed chrome extensions only once across multiple non-extension paths", async () => {
450
+ const tempRoot = mkdtempSync(join(tmpdir(), "cdxgen-chrome-ext-multi-"));
451
+ const pathA = join(tempRoot, "project-a");
452
+ const pathB = join(tempRoot, "project-b");
453
+ mkdirSync(pathA, { recursive: true });
454
+ mkdirSync(pathB, { recursive: true });
455
+ const collectInstalledChromeExtensions = sinon.stub().returns([
456
+ {
457
+ type: "application",
458
+ name: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
459
+ version: "1.0.0",
460
+ purl: "pkg:chrome-extension/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@1.0.0",
461
+ "bom-ref":
462
+ "pkg:chrome-extension/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@1.0.0",
463
+ },
464
+ ]);
465
+ try {
466
+ const { createMultiXBom } = await esmock("./index.js", {
467
+ "../helpers/chromextutils.js": {
468
+ CHROME_EXTENSION_PURL_TYPE: "chrome-extension",
469
+ collectChromeExtensionsFromPath: sinon
470
+ .stub()
471
+ .returns({ components: [], extensionDirs: [] }),
472
+ collectInstalledChromeExtensions,
473
+ discoverChromiumExtensionDirs: sinon.stub().returns([
474
+ {
475
+ browser: "Google Chrome",
476
+ channel: "stable",
477
+ dir: join(tempRoot, "fake-browser-dir"),
478
+ },
479
+ ]),
480
+ },
481
+ });
482
+ await createMultiXBom([pathA, pathB], {
483
+ projectType: ["chrome-extension"],
484
+ multiProject: true,
485
+ });
486
+ sinon.assert.calledOnce(collectInstalledChromeExtensions);
487
+ } finally {
488
+ rmSync(tempRoot, { recursive: true, force: true });
489
+ }
490
+ });
491
+ });
241
492
  });