@cyclonedx/cdxgen 11.2.0 → 11.2.2

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.
@@ -1,6 +1,6 @@
1
1
  import { Buffer } from "node:buffer";
2
2
  import { spawnSync } from "node:child_process";
3
- import { createHash } from "node:crypto";
3
+ import { createHash, randomUUID } from "node:crypto";
4
4
  import {
5
5
  constants,
6
6
  chmodSync,
@@ -175,6 +175,16 @@ export let metadata_cache = {};
175
175
  // Speed up lookup namespaces for a given jar
176
176
  const jarNSMapping_cache = {};
177
177
 
178
+ // Temporary files written by cdxgen, will be removed on exit
179
+ const temporaryFiles = new Set();
180
+ process.on("exit", () =>
181
+ temporaryFiles.forEach((tempFile) => {
182
+ if (existsSync(tempFile)) {
183
+ unlinkSync(tempFile);
184
+ }
185
+ }),
186
+ );
187
+
178
188
  // Whether test scope shall be included for java/maven projects; default, if unset shall be 'true'
179
189
  export const includeMavenTestScope =
180
190
  !process.env.CDX_MAVEN_INCLUDE_TEST_SCOPE ||
@@ -410,6 +420,14 @@ export const PROJECT_TYPE_ALIASES = {
410
420
  "dotnet-framework48",
411
421
  "vb",
412
422
  "fsharp",
423
+ "twincat",
424
+ "csproj",
425
+ "tsproj",
426
+ "vbproj",
427
+ "sln",
428
+ "fsproj",
429
+ "plcproj",
430
+ "hmiproj",
413
431
  ],
414
432
  dart: ["dart", "flutter", "pub"],
415
433
  haskell: ["haskell", "hackage", "cabal"],
@@ -448,6 +466,7 @@ export const PROJECT_TYPE_ALIASES = {
448
466
  ],
449
467
  binary: ["binary", "blint"],
450
468
  oci: ["docker", "oci", "container", "podman"],
469
+ cocoa: ["cocoa", "cocoapods", "objective-c", "swift", "ios"],
451
470
  };
452
471
 
453
472
  // Package manager aliases
@@ -595,12 +614,21 @@ export function isPackageManagerAllowed(name, conflictingManagers, options) {
595
614
  // HTTP cache
596
615
  const gotHttpCache = new Map();
597
616
 
617
+ function isCacheDisabled() {
618
+ return (
619
+ process.env.CDXGEN_NO_CACHE &&
620
+ ["true", "1"].includes(process.env.CDXGEN_NO_CACHE)
621
+ );
622
+ }
623
+
624
+ const cache = isCacheDisabled() ? undefined : gotHttpCache;
625
+
598
626
  // Custom user-agent for cdxgen
599
627
  export const cdxgenAgent = got.extend({
600
628
  headers: {
601
629
  "user-agent": `@CycloneDX/cdxgen ${_version}`,
602
630
  },
603
- cache: gotHttpCache,
631
+ cache,
604
632
  retry: {
605
633
  limit: 0,
606
634
  },
@@ -1638,7 +1666,7 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
1638
1666
  }
1639
1667
 
1640
1668
  /**
1641
- * Given a lock file this method would return an Object with the identiy as the key and parsed name and value
1669
+ * Given a lock file this method would return an Object with the identity as the key and parsed name and value
1642
1670
  * eg: "@actions/core@^1.2.6", "@actions/core@^1.6.0":
1643
1671
  * version "1.6.0"
1644
1672
  * would result in two entries
@@ -1650,7 +1678,7 @@ export function yarnLockToIdentMap(lockData) {
1650
1678
  let currentIdents = [];
1651
1679
  lockData.split("\n").forEach((l) => {
1652
1680
  l = l.replace("\r", "");
1653
- if (l === "\n" || l.startsWith("#")) {
1681
+ if (l === "\n" || !l.length || l.startsWith("#")) {
1654
1682
  return;
1655
1683
  }
1656
1684
  // "@actions/core@^1.2.6", "@actions/core@^1.6.0":
@@ -1707,14 +1735,22 @@ export function yarnLockToIdentMap(lockData) {
1707
1735
  function _parseYarnLine(l) {
1708
1736
  let name = "";
1709
1737
  let group = "";
1710
- const prefixAtSymbol = l.startsWith("@");
1738
+ let prefixAtSymbol = l.startsWith("@");
1711
1739
  const tmpA = l.split("@");
1712
1740
  // ignore possible leading empty strings
1713
1741
  if (tmpA[0] === "") {
1714
1742
  tmpA.shift();
1715
1743
  }
1744
+ let fullName;
1716
1745
  if (tmpA.length >= 2) {
1717
- const fullName = tmpA[0];
1746
+ if (tmpA.length === 4) {
1747
+ if (tmpA[1] === "npm:") {
1748
+ prefixAtSymbol = true;
1749
+ }
1750
+ fullName = tmpA[2];
1751
+ } else {
1752
+ fullName = tmpA[0];
1753
+ }
1718
1754
  if (fullName.indexOf("/") > -1) {
1719
1755
  const parts = fullName.split("/");
1720
1756
  group = (prefixAtSymbol ? "@" : "") + parts[0];
@@ -1891,16 +1927,26 @@ export async function parseYarnLock(yarnLockFile) {
1891
1927
  if (dgroupname.endsWith(":")) {
1892
1928
  dgroupname = dgroupname.substring(0, dgroupname.length - 1);
1893
1929
  }
1894
- let range = tmpA[1].replace(/["']/g, "");
1930
+ let dgroupnameToUse = dgroupname;
1931
+ const range = tmpA[1].replace(/["']/g, "");
1932
+ let versionRange = range;
1895
1933
  // Deal with range with npm: prefix such as npm:string-width@^4.2.0, npm:@types/ioredis@^4.28.10
1896
1934
  if (range.startsWith("npm:")) {
1897
- range = range.split("@").splice(-1)[0];
1935
+ versionRange = range.split("@").splice(-1)[0];
1936
+ dgroupnameToUse = range
1937
+ .replace("npm:", "")
1938
+ .replace(`@${versionRange}`, "");
1898
1939
  }
1899
- const resolvedVersion = identMap[`${dgroupname}|${range}`];
1940
+ const resolvedVersion =
1941
+ identMap[`${dgroupname}|${versionRange}`] ||
1942
+ identMap[`${dgroupnameToUse}|${versionRange}`];
1943
+ // Handle case where the dependency name is really an alias.
1944
+ // Eg: legacy-swc-helpers "npm:@swc/helpers@=0.4.14". Here the dgroupname=@swc/helpers
1945
+
1900
1946
  const depPurlString = new PackageURL(
1901
1947
  "npm",
1902
1948
  null,
1903
- dgroupname,
1949
+ dgroupnameToUse,
1904
1950
  resolvedVersion,
1905
1951
  null,
1906
1952
  null,
@@ -4152,7 +4198,10 @@ export async function getMvnMetadata(
4152
4198
  }
4153
4199
  const group = p.group || "";
4154
4200
  // If the package already has key metadata skip querying maven
4155
- if (group && p.name && p.version && !shouldFetchLicense() && !force) {
4201
+ if (
4202
+ !p.version ||
4203
+ (group && p.name && p.version && !shouldFetchLicense() && !force)
4204
+ ) {
4156
4205
  cdepList.push(p);
4157
4206
  continue;
4158
4207
  }
@@ -6932,8 +6981,11 @@ export async function parseGemspecData(gemspecData, gemspecFile) {
6932
6981
  if (["name", "version"].includes(aprop)) {
6933
6982
  value = value.replace(/["']/g, "");
6934
6983
  }
6935
- pkg[aprop] = value;
6936
- return;
6984
+ // Do not set name=name or version=version
6985
+ if (value !== aprop) {
6986
+ pkg[aprop] = value;
6987
+ break;
6988
+ }
6937
6989
  }
6938
6990
  }
6939
6991
  // Handle common problems
@@ -7598,11 +7650,11 @@ export async function parseCargoTomlData(
7598
7650
  pkg.evidence = {
7599
7651
  identity: {
7600
7652
  field: "purl",
7601
- confidence: 0.5,
7653
+ confidence: pkg.version ? 0.5 : 0,
7602
7654
  methods: [
7603
7655
  {
7604
7656
  technique: "manifest-analysis",
7605
- confidence: 0.5,
7657
+ confidence: pkg.version ? 0.5 : 0,
7606
7658
  value: cargoTomlFile,
7607
7659
  },
7608
7660
  ],
@@ -9228,7 +9280,10 @@ export function parseCsProjData(csProjData, projFile, pkgNameVersions = {}) {
9228
9280
  let gacVersionWarningShown = false;
9229
9281
  // First make up a parentcomponent name based on the .csproj file name
9230
9282
  if (projFile) {
9231
- parentComponent.name = basename(projFile).replaceAll(".csproj", "");
9283
+ parentComponent.name = basename(projFile).replaceAll(
9284
+ /.(cs|fs|vb|ts|plc|hmi)proj$/g,
9285
+ "",
9286
+ );
9232
9287
  }
9233
9288
  // Collect details about the parent component
9234
9289
  if (project?.PropertyGroup?.length) {
@@ -9240,6 +9295,13 @@ export function parseCsProjData(csProjData, projFile, pkgNameVersions = {}) {
9240
9295
  Array.isArray(apg.AssemblyName[0]._)
9241
9296
  ) {
9242
9297
  parentComponent.name = apg.AssemblyName[0]._[0];
9298
+ } else if (
9299
+ apg?.Name &&
9300
+ Array.isArray(apg.Name) &&
9301
+ apg.Name[0]._ &&
9302
+ Array.isArray(apg.Name[0]._)
9303
+ ) {
9304
+ parentComponent.name = apg.Name[0]._[0];
9243
9305
  }
9244
9306
  if (
9245
9307
  apg?.ProductVersion &&
@@ -9248,6 +9310,20 @@ export function parseCsProjData(csProjData, projFile, pkgNameVersions = {}) {
9248
9310
  Array.isArray(apg.ProductVersion[0]._)
9249
9311
  ) {
9250
9312
  parentComponent.version = apg.ProductVersion[0]._[0];
9313
+ } else if (
9314
+ apg?.ProgramVersion &&
9315
+ Array.isArray(apg.ProgramVersion) &&
9316
+ apg.ProgramVersion[0]._ &&
9317
+ Array.isArray(apg.ProgramVersion[0]._)
9318
+ ) {
9319
+ parentComponent.version = apg.ProgramVersion[0]._[0];
9320
+ } else if (
9321
+ apg?.HmiVersion &&
9322
+ Array.isArray(apg.HmiVersion) &&
9323
+ apg.HmiVersion[0]._ &&
9324
+ Array.isArray(apg.HmiVersion[0]._)
9325
+ ) {
9326
+ parentComponent.version = apg.HmiVersion[0]._[0];
9251
9327
  }
9252
9328
  if (
9253
9329
  apg?.OutputType &&
@@ -9328,6 +9404,17 @@ export function parseCsProjData(csProjData, projFile, pkgNameVersions = {}) {
9328
9404
  });
9329
9405
  }
9330
9406
  }
9407
+ if (
9408
+ apg?.AzureFunctionsVersion &&
9409
+ Array.isArray(apg.AzureFunctionsVersion) &&
9410
+ apg.AzureFunctionsVersion[0]._ &&
9411
+ Array.isArray(apg.AzureFunctionsVersion[0]._)
9412
+ ) {
9413
+ parentComponent.properties.push({
9414
+ name: "cdx:dotnet:azure_functions_version",
9415
+ value: apg.AzureFunctionsVersion[0]._[0],
9416
+ });
9417
+ }
9331
9418
  if (
9332
9419
  apg?.Description &&
9333
9420
  Array.isArray(apg.Description) &&
@@ -9476,11 +9563,13 @@ export function parseCsProjData(csProjData, projFile, pkgNameVersions = {}) {
9476
9563
  }
9477
9564
  }
9478
9565
  }
9479
- if (
9480
- parentComponent &&
9481
- Object.keys(parentComponent).length &&
9482
- parentComponent.purl
9483
- ) {
9566
+ // If the parent still lacks a purl, add one based on the name and version
9567
+ if (parentComponent && !parentComponent.purl && parentComponent.name) {
9568
+ parentComponent.purl = `pkg:nuget/${parentComponent.name}@${
9569
+ parentComponent.version || "latest"
9570
+ }`;
9571
+ }
9572
+ if (parentComponent?.purl) {
9484
9573
  parentComponent["bom-ref"] = parentComponent.purl;
9485
9574
  }
9486
9575
  let dependencies = [];
@@ -11431,9 +11520,10 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
11431
11520
  }
11432
11521
  }
11433
11522
  }
11523
+ let jarMetadata;
11434
11524
  if ((!group || !name || !version) && safeExistsSync(manifestFile)) {
11435
11525
  confidence = 0.8;
11436
- const jarMetadata = parseJarManifest(
11526
+ jarMetadata = parseJarManifest(
11437
11527
  readFileSync(manifestFile, {
11438
11528
  encoding: "utf-8",
11439
11529
  }),
@@ -11506,7 +11596,10 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
11506
11596
  // if group is empty use name as group
11507
11597
  group = group === "." ? name : group || name;
11508
11598
  }
11509
- if (name && version) {
11599
+ if (name) {
11600
+ if (!version) {
11601
+ confidence = 0;
11602
+ }
11510
11603
  const apkg = {
11511
11604
  group: group ? encodeForPurl(group) : "",
11512
11605
  name: name ? encodeForPurl(name) : "",
@@ -11535,7 +11628,7 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
11535
11628
  properties: [
11536
11629
  {
11537
11630
  name: "SrcFile",
11538
- value: jarname,
11631
+ value: jf,
11539
11632
  },
11540
11633
  ],
11541
11634
  };
@@ -11895,6 +11988,607 @@ export function splitOutputByGradleProjects(rawOutput, relevantTasks) {
11895
11988
  return outputSplitBySubprojects;
11896
11989
  }
11897
11990
 
11991
+ /**
11992
+ * Parse the contents of a 'Podfile.lock'
11993
+ *
11994
+ * @param {Object} podfileLock The content of the podfile.lock as an Object
11995
+ * @param {String} projectPath The path to the project root
11996
+ * @returns {Map} Map of all dependencies with their direct dependencies
11997
+ */
11998
+ export async function parsePodfileLock(podfileLock, projectPath) {
11999
+ const dependencies = new Map();
12000
+ for (const pod of podfileLock["PODS"]) {
12001
+ const dependency = {};
12002
+ if (pod.constructor === Object) {
12003
+ for (const key in pod) {
12004
+ dependency.metadata = parseCocoaDependency(key);
12005
+ const subDependencies = new Set();
12006
+ for (const subPod of pod[key]) {
12007
+ subDependencies.add(parseCocoaDependency(subPod, false));
12008
+ }
12009
+ dependency.dependencies = Array.from(subDependencies);
12010
+ }
12011
+ } else {
12012
+ dependency.metadata = parseCocoaDependency(pod);
12013
+ }
12014
+ const podName = dependency.metadata.name.includes("/")
12015
+ ? dependency.metadata.name.substring(
12016
+ 0,
12017
+ dependency.metadata.name.indexOf("/"),
12018
+ )
12019
+ : dependency.metadata.name;
12020
+ if (podfileLock["EXTERNAL SOURCES"]?.[podName]) {
12021
+ const externalPod = podfileLock["EXTERNAL SOURCES"][podName];
12022
+ if (externalPod[":podspec"]) {
12023
+ const podspecLocation = resolve(projectPath, externalPod[":podspec"]);
12024
+ dependency.metadata.properties = [
12025
+ {
12026
+ name: "cdx:pods:projectDir",
12027
+ value: dirname(podspecLocation),
12028
+ },
12029
+ {
12030
+ name: "cdx:pods:podspecLocation",
12031
+ value: podspecLocation,
12032
+ },
12033
+ ];
12034
+ } else {
12035
+ const projectLocation = resolve(projectPath, externalPod[":path"]);
12036
+ dependency.metadata.properties = [
12037
+ {
12038
+ name: "cdx:pods:projectDir",
12039
+ value: projectLocation,
12040
+ },
12041
+ ];
12042
+ let podspec = join(projectLocation, `${podName}.podspec`);
12043
+ if (!existsSync(podspec)) {
12044
+ podspec = `${podspec}.json`;
12045
+ }
12046
+ if (existsSync(podspec)) {
12047
+ dependency.metadata.properties.push({
12048
+ name: "cdx:pods:podspecLocation",
12049
+ value: podspec,
12050
+ });
12051
+ }
12052
+ }
12053
+ }
12054
+ dependencies.set(dependency.metadata.name, dependency);
12055
+ }
12056
+ if (!["false", "0"].includes(process.env.COCOA_MERGE_SUBSPECS)) {
12057
+ for (const subspecComponentName of [...dependencies.keys()].filter((name) =>
12058
+ name.includes("/"),
12059
+ )) {
12060
+ const subspecComponent = dependencies.get(subspecComponentName);
12061
+ const mainComponentName = subspecComponentName.split("/")[0];
12062
+ let mainComponent = dependencies.get(mainComponentName);
12063
+ if (!mainComponent) {
12064
+ mainComponent = {
12065
+ metadata: {
12066
+ name: mainComponentName,
12067
+ version: subspecComponent.metadata.version,
12068
+ },
12069
+ };
12070
+ dependencies.set(mainComponentName, mainComponent);
12071
+ }
12072
+ if (subspecComponent.dependencies) {
12073
+ if (mainComponent.dependencies) {
12074
+ mainComponent.dependencies = [
12075
+ ...mainComponent.dependencies,
12076
+ ...subspecComponent.dependencies,
12077
+ ];
12078
+ } else {
12079
+ mainComponent.dependencies = subspecComponent.dependencies;
12080
+ }
12081
+ }
12082
+ mainComponent.metadata.properties = [
12083
+ ...(mainComponent.metadata.properties
12084
+ ? mainComponent.metadata.properties
12085
+ : []),
12086
+ {
12087
+ name: "cdx:pods:Subspec",
12088
+ value: subspecComponentName.substring(
12089
+ subspecComponentName.indexOf("/") + 1,
12090
+ ),
12091
+ },
12092
+ ...(subspecComponent.metadata.propertie
12093
+ ? subspecComponent.metadata.properties
12094
+ : []),
12095
+ ];
12096
+ dependencies.delete(subspecComponentName);
12097
+ }
12098
+ for (const [dependencyName, dependency] of dependencies) {
12099
+ if (dependency.dependencies) {
12100
+ dependency.dependencies.forEach(
12101
+ (dep) => (dep.name = dep.name.split("/")[0]),
12102
+ );
12103
+ dependency.dependencies = [
12104
+ ...new Map(
12105
+ dependency.dependencies
12106
+ .filter((dep) => dep.name !== dependencyName)
12107
+ .map((dep) => [dep.name, dep]),
12108
+ ).values(),
12109
+ ];
12110
+ if (dependency.dependencies.length === 0) {
12111
+ delete dependency.dependencies;
12112
+ }
12113
+ }
12114
+ }
12115
+ }
12116
+ return dependencies;
12117
+ }
12118
+
12119
+ /**
12120
+ * Parse all targets and their direct dependencies from the 'Podfile'
12121
+ *
12122
+ * @param {Object} target A JSON-object representing a target
12123
+ * @param {Map} allDependencies The map containing all parsed direct dependencies for a target
12124
+ * @param {String} [prefix=undefined] Prefix to add to the targets name
12125
+ */
12126
+ export function parsePodfileTargets(
12127
+ target,
12128
+ allDependencies,
12129
+ prefix = undefined,
12130
+ ) {
12131
+ const targetName = (prefix ? `${prefix}/` : "") + target.name;
12132
+ const targetDependencies = new Set(
12133
+ prefix && allDependencies.has(prefix)
12134
+ ? allDependencies.get(prefix)
12135
+ : targetName !== "Pods"
12136
+ ? allDependencies.get("Pods")
12137
+ : [],
12138
+ );
12139
+ if (target["dependencies"]) {
12140
+ for (const targetDependency of target["dependencies"]) {
12141
+ if (targetDependency.constructor === Object) {
12142
+ targetDependencies.add(Object.keys(targetDependency)[0]);
12143
+ } else {
12144
+ targetDependencies.add(targetDependency);
12145
+ }
12146
+ }
12147
+ }
12148
+ allDependencies.set(targetName, Array.from(targetDependencies));
12149
+ if (target.children) {
12150
+ const childPrefix = targetName === "Pods" ? undefined : targetName;
12151
+ for (const childTarget of target.children) {
12152
+ parsePodfileTargets(childTarget, allDependencies, childPrefix);
12153
+ }
12154
+ }
12155
+ }
12156
+
12157
+ /**
12158
+ * Parse a single line representing a dependency
12159
+ *
12160
+ * @param {String} dependencyLine The line that should be parsed as a dependency
12161
+ * @param {boolean} [parseVersion=true] Include parsing the version of the dependency
12162
+ * @returns {Object} Object representing a dependency
12163
+ */
12164
+ export function parseCocoaDependency(dependencyLine, parseVersion = true) {
12165
+ const dependencyData = dependencyLine.split(" (");
12166
+ const dependency = { name: dependencyData[0] };
12167
+ if (parseVersion) {
12168
+ dependency.version = dependencyData[1].substring(
12169
+ 0,
12170
+ dependencyData[1].length - 1,
12171
+ );
12172
+ }
12173
+ return dependency;
12174
+ }
12175
+
12176
+ /**
12177
+ * Execute the 'pod'-command with parameters
12178
+ *
12179
+ * @param {String[]} parameters The parameters for the command
12180
+ * @param {String} path The path where the command should be executed
12181
+ * @param {Object} options CLI options
12182
+ * @returns {Object} The result of running the command
12183
+ */
12184
+ export function executePodCommand(parameters, path, options) {
12185
+ if (DEBUG_MODE) {
12186
+ if (path) {
12187
+ console.log("Executing pod", parameters.join(" "), "in", path);
12188
+ } else {
12189
+ console.log("Executing pod", parameters.join(" "));
12190
+ }
12191
+ }
12192
+ const result = spawnSync(process.env.POD_CMD || "pod", parameters, {
12193
+ cwd: path,
12194
+ encoding: "utf-8",
12195
+ shell: isWin,
12196
+ maxBuffer: MAX_BUFFER,
12197
+ });
12198
+ if (result.status !== 0 || result.error) {
12199
+ if (result?.stderr?.includes("Unable to find a pod")) {
12200
+ console.log(
12201
+ "Try again by running 'pod install' before invoking 'cdxgen'.",
12202
+ );
12203
+ }
12204
+ if (process.env?.CDXGEN_IN_CONTAINER !== "true") {
12205
+ console.log(
12206
+ "Consider using the cdxgen container image (`ghcr.io/cyclonedx/cdxgen`), which includes cocoapods and additional build tools.",
12207
+ );
12208
+ } else if (!DEBUG_MODE) {
12209
+ console.log(
12210
+ "Something went wrong when trying to execute cocoapods -- Set the environment variable 'CDXGEN_DEBUG_MODE=debug' to troubleshoot cocoapods related errors",
12211
+ );
12212
+ }
12213
+ if (options.failOnError || DEBUG_MODE) {
12214
+ if (result.stdout) {
12215
+ console.log(result.stdout);
12216
+ }
12217
+ if (result.stderr) {
12218
+ console.log(result.stderr);
12219
+ }
12220
+ options.failOnError && process.exit(1);
12221
+ }
12222
+ }
12223
+ return result;
12224
+ }
12225
+
12226
+ /**
12227
+ * Method that handles object creation for cocoa pods.
12228
+ *
12229
+ * @param {Object} dependency The dependency that is to be transformed into an SBOM object
12230
+ * @param {Object} options CLI options
12231
+ * @param {String} [type="library"] The type of Object to create
12232
+ * @returns {Object} An object representing the pod in SBOM-format
12233
+ */
12234
+ export async function buildObjectForCocoaPod(
12235
+ dependency,
12236
+ options,
12237
+ type = "library",
12238
+ ) {
12239
+ let component;
12240
+ if (
12241
+ !["false", "0"].includes(process.env.COCOA_RESOLVE_FROM_NODE) &&
12242
+ dependency.properties?.find(({ name }) => name === "cdx:pods:projectDir")
12243
+ ) {
12244
+ let tmpDir = dependency.properties.find(
12245
+ ({ name }) => name === "cdx:pods:projectDir",
12246
+ ).value;
12247
+ const exclusionDirs = process.env.COCOA_RESOLVE_FROM_NODE_EXCLUSION_DIRS
12248
+ ? process.env.COCOA_RESOLVE_FROM_NODE_EXCLUSION_DIRS.split(",")
12249
+ : [];
12250
+ if (
12251
+ tmpDir &&
12252
+ !exclusionDirs.some((dir) =>
12253
+ `${tmpDir.replaceAll("\\", "/")}/`.includes(
12254
+ `/${dir.replaceAll("\\", "/")}/`.replaceAll("//", "/"),
12255
+ ),
12256
+ ) &&
12257
+ tmpDir.indexOf("node_modules") !== -1
12258
+ ) {
12259
+ do {
12260
+ const npmPackages = await parsePkgJson(join(tmpDir, "package.json"));
12261
+ if (npmPackages.length === 1) {
12262
+ component = npmPackages[0];
12263
+ component.type = "library";
12264
+ component.properties = component.properties.concat(
12265
+ {
12266
+ name: "cdx:pods:PodName",
12267
+ value: dependency.name,
12268
+ },
12269
+ dependency.properties,
12270
+ );
12271
+ tmpDir = undefined;
12272
+ } else {
12273
+ tmpDir = dirname(tmpDir);
12274
+ }
12275
+ } while (tmpDir && tmpDir.indexOf("node_modules") !== -1);
12276
+ }
12277
+ }
12278
+ if (!component) {
12279
+ let name = dependency.name;
12280
+ let subspec = null;
12281
+ const locationOfSubspec = dependency.name.indexOf("/");
12282
+ if (locationOfSubspec !== -1) {
12283
+ name = dependency.name.substring(0, locationOfSubspec);
12284
+ subspec = dependency.name.substring(locationOfSubspec + 1);
12285
+ }
12286
+ component = {
12287
+ ...dependency,
12288
+ type,
12289
+ };
12290
+ if (subspec) {
12291
+ if (!component.properties) {
12292
+ component.properties = [];
12293
+ }
12294
+ component.properties.push({
12295
+ name: "cdx:pods:Subspec",
12296
+ value: subspec,
12297
+ });
12298
+ }
12299
+ const purl = new PackageURL(
12300
+ "cocoapods",
12301
+ "",
12302
+ name,
12303
+ component.version,
12304
+ null,
12305
+ subspec,
12306
+ ).toString();
12307
+ component["purl"] = purl;
12308
+ component["bom-ref"] = decodeURIComponent(purl);
12309
+ if (options && !["false", "0"].includes(process.env.COCOA_FULL_SCAN)) {
12310
+ fullScanCocoaPod(dependency, component, options);
12311
+ }
12312
+ }
12313
+ return component;
12314
+ }
12315
+
12316
+ function fullScanCocoaPod(dependency, component, options) {
12317
+ let result;
12318
+ if (
12319
+ component.properties?.find(({ name }) => name === "cdx:pods:projectDir")
12320
+ ) {
12321
+ if (
12322
+ component.properties.find(
12323
+ ({ name }) => name === "cdx:pods:podspecLocation",
12324
+ )
12325
+ ) {
12326
+ let podspecLocation = component.properties.find(
12327
+ ({ name }) => name === "cdx:pods:podspecLocation",
12328
+ ).value;
12329
+ component.properties.push({
12330
+ name: "SrcFile",
12331
+ value: podspecLocation,
12332
+ });
12333
+ let replacements;
12334
+ if (
12335
+ podspecLocation.endsWith(".podspec") &&
12336
+ process.env.COCOA_PODSPEC_REPLACEMENTS
12337
+ ) {
12338
+ replacements = process.env.COCOA_PODSPEC_REPLACEMENTS.split(";");
12339
+ } else if (
12340
+ podspecLocation.endsWith(".json") &&
12341
+ process.env.COCOA_PODSPEC_JSON_REPLACEMENTS
12342
+ ) {
12343
+ replacements = process.env.COCOA_PODSPEC_JSON_REPLACEMENTS.split(";");
12344
+ }
12345
+ if (replacements) {
12346
+ let podspecContent = readFileSync(podspecLocation, "utf-8");
12347
+ for (const replacement of replacements) {
12348
+ const replacementPair = replacement.split("=");
12349
+ let match = replacementPair[0].replaceAll("<NEWLINE>", "\n");
12350
+ if (match.startsWith("/") && match.endsWith("/")) {
12351
+ match = new RegExp(match.substring(1, match.length - 1), "g");
12352
+ }
12353
+ const repl = replacementPair[1].replaceAll("<NEWLINE>", "\n");
12354
+ podspecContent = podspecContent.replaceAll(match, repl);
12355
+ }
12356
+ podspecLocation = join(
12357
+ dirname(podspecLocation),
12358
+ `${randomUUID()}.${podspecLocation.substring(podspecLocation.lastIndexOf(".") + 1)}`,
12359
+ );
12360
+ writeFileSync(podspecLocation, podspecContent);
12361
+ temporaryFiles.add(podspecLocation);
12362
+ }
12363
+ result = executePodCommand(
12364
+ ["ipc", "spec", "--silent", podspecLocation],
12365
+ undefined,
12366
+ options,
12367
+ );
12368
+ } else {
12369
+ console.warn(
12370
+ `Unable to do a full scan of local pod '${dependency.name}', because either its podspec doesn't exist, or it wasn't found with the currently implemented algorithms.\nIf you think this is an error, please file an issue on GitHub!`,
12371
+ );
12372
+ return;
12373
+ }
12374
+ } else {
12375
+ let dependencyName = dependency.name;
12376
+ if (dependencyName.includes("/")) {
12377
+ dependencyName = dependencyName.substring(0, dependencyName.indexOf("/"));
12378
+ }
12379
+ const srcFileProperty = {
12380
+ name: "SrcFile",
12381
+ value: executePodCommand(
12382
+ ["spec", "which", dependencyName, `--version=${dependency.version}`],
12383
+ undefined,
12384
+ options,
12385
+ ).stdout.trim(),
12386
+ };
12387
+ if (component.properties) {
12388
+ component.properties.push(srcFileProperty);
12389
+ } else {
12390
+ component.properties = [srcFileProperty];
12391
+ }
12392
+ result = executePodCommand(
12393
+ ["spec", "cat", dependencyName, `--version=${dependency.version}`],
12394
+ undefined,
12395
+ options,
12396
+ );
12397
+ }
12398
+ const podspecText = result.stdout;
12399
+ let podspec;
12400
+ try {
12401
+ podspec = JSON.parse(
12402
+ podspecText.substring(
12403
+ podspecText.indexOf("{"),
12404
+ podspecText.lastIndexOf("}") + 1,
12405
+ ),
12406
+ );
12407
+ } catch (e) {
12408
+ return;
12409
+ }
12410
+ const externalRefs = [];
12411
+ if (podspec.authors) {
12412
+ component.authors = [];
12413
+ if (podspec.authors.constructor === Object) {
12414
+ Object.entries(podspec.authors).forEach(([name, email]) =>
12415
+ component.authors.push({ name, email }),
12416
+ );
12417
+ } else if (podspec.authors.constructor === Array) {
12418
+ podspec.authors.forEach((name) => component.authors.push({ name }));
12419
+ } else {
12420
+ component.authors.push({ name: podspec.authors });
12421
+ }
12422
+ }
12423
+ if (podspec.description) {
12424
+ component.description = podspec.description;
12425
+ } else if (podspec.summary) {
12426
+ component.description = podspec.summary;
12427
+ }
12428
+ if (podspec.documentation_url) {
12429
+ externalRefs.push({
12430
+ type: "documentation",
12431
+ url: podspec.documentation_url,
12432
+ });
12433
+ } else if (podspec.readme) {
12434
+ externalRefs.push({
12435
+ type: "documentation",
12436
+ url: podspec.readme,
12437
+ });
12438
+ }
12439
+ if (podspec.homepage) {
12440
+ externalRefs.push({
12441
+ type: "website",
12442
+ url: podspec.homepage,
12443
+ });
12444
+ }
12445
+ if (podspec.license) {
12446
+ if (podspec.license.constructor === Object) {
12447
+ if (podspec.license.type === "Copyright") {
12448
+ component.copyright = podspec.license.text;
12449
+ } else {
12450
+ component.licenses = [{ license: {} }];
12451
+ if (spdxLicenses.includes(podspec.license.type)) {
12452
+ component.licenses[0].license.id = podspec.license.type;
12453
+ } else {
12454
+ component.licenses[0].license.name = podspec.license.type;
12455
+ }
12456
+ const licenseText = [];
12457
+ if (podspec.license.text) {
12458
+ if (podspec.license.text.startsWith("http")) {
12459
+ component.licenses[0].license.url = podspec.license.text;
12460
+ } else {
12461
+ licenseText.push(podspec.license.text);
12462
+ }
12463
+ }
12464
+ if (podspec.license.file) {
12465
+ if (podspec.license.file.startsWith("http")) {
12466
+ if (component.licenses[0].license.url) {
12467
+ if (licenseText.length !== 0) {
12468
+ licenseText.push("");
12469
+ }
12470
+ licenseText.push(
12471
+ `See also: ${component.licenses[0].license.url}`,
12472
+ );
12473
+ }
12474
+ component.licenses[0].license.url = podspec.license.file;
12475
+ } else {
12476
+ if (licenseText.length !== 0) {
12477
+ licenseText.push("");
12478
+ }
12479
+ licenseText.push(`See license in file '${podspec.license.file}'`);
12480
+ }
12481
+ }
12482
+ if (licenseText.length !== 0) {
12483
+ component.licenses[0].license.text = {
12484
+ content: licenseText.join("\n"),
12485
+ };
12486
+ }
12487
+ }
12488
+ } else {
12489
+ if (spdxLicenses.includes(podspec.license)) {
12490
+ component.licenses = [{ license: { id: podspec.license } }];
12491
+ } else {
12492
+ component.licenses = [{ license: { name: podspec.license } }];
12493
+ }
12494
+ }
12495
+ }
12496
+ if (podspec.social_media_url) {
12497
+ externalRefs.push({
12498
+ type: "social",
12499
+ url: podspec.social_media_url,
12500
+ });
12501
+ }
12502
+ if (podspec.source) {
12503
+ const comment = [];
12504
+ if (podspec.source.http) {
12505
+ const sourceDistro = {
12506
+ type: "source-distribution",
12507
+ url: podspec.source.http,
12508
+ };
12509
+ const hashes = [];
12510
+ if (podspec.source.http.sha1) {
12511
+ hashes.push({
12512
+ alg: "SHA-1",
12513
+ content: podspec.source.http.sha1,
12514
+ });
12515
+ }
12516
+ if (podspec.source.http.sha256) {
12517
+ hashes.push({
12518
+ alg: "SHA-256",
12519
+ content: podspec.source.http.sha256,
12520
+ });
12521
+ }
12522
+ if (hashes.length !== 0) {
12523
+ sourceDistro.hashes = hashes;
12524
+ }
12525
+ if (podspec.source.flatten) {
12526
+ comment.push(`Flatten: ${podspec.source.flatten}`);
12527
+ }
12528
+ if (podspec.source.type) {
12529
+ comment.push(`Type: ${podspec.source.type}`);
12530
+ }
12531
+ if (podspec.source.headers) {
12532
+ comment.push(`Headers: ${podspec.source.headers}`);
12533
+ }
12534
+ if (comment.length !== 0) {
12535
+ sourceDistro.comment = comment.join("\n");
12536
+ }
12537
+ externalRefs.push(sourceDistro);
12538
+ } else {
12539
+ let url;
12540
+ if (podspec.source.git) {
12541
+ url = podspec.source.git;
12542
+ comment.push("Type: git");
12543
+ if (podspec.source.branch) {
12544
+ comment.push(`Branch: ${podspec.source.branch}`);
12545
+ }
12546
+ if (podspec.source.commit) {
12547
+ comment.push(`Commit: ${podspec.source.commit}`);
12548
+ }
12549
+ if (podspec.source.tag) {
12550
+ comment.push(`Tag: ${podspec.source.tag}`);
12551
+ }
12552
+ if (podspec.source.submodules) {
12553
+ comment.push(`Submodules: ${podspec.source.submodules}`);
12554
+ }
12555
+ } else if (podspec.source.hg) {
12556
+ url = podspec.source.hg;
12557
+ comment.push("Type: hg");
12558
+ if (podspec.source.revision) {
12559
+ comment.push(`Revision: ${podspec.source.revision}`);
12560
+ }
12561
+ } else if (podspec.source.svn) {
12562
+ url = podspec.source.svn;
12563
+ comment.push("Type: svn");
12564
+ if (podspec.source.folder) {
12565
+ comment.push(`Folder: ${podspec.source.folder}`);
12566
+ }
12567
+ if (podspec.source.revision) {
12568
+ comment.push(`Revision: ${podspec.source.revision}`);
12569
+ }
12570
+ if (podspec.source.tag) {
12571
+ comment.push(`Tag: ${podspec.source.tag}`);
12572
+ }
12573
+ }
12574
+ if (url) {
12575
+ externalRefs.push({
12576
+ type: "vcs",
12577
+ url: url,
12578
+ comment: comment.join("\n"),
12579
+ });
12580
+ } else {
12581
+ console.warn(
12582
+ `${dependency.name} has property 'source' defined, but it does not contain a URL -- ignoring...`,
12583
+ );
12584
+ }
12585
+ }
12586
+ }
12587
+ if (externalRefs.length !== 0) {
12588
+ component.externalReferences = externalRefs;
12589
+ }
12590
+ }
12591
+
11898
12592
  /**
11899
12593
  * Method that handles object creation for gradle modules.
11900
12594
  *
@@ -11904,8 +12598,11 @@ export function splitOutputByGradleProjects(rawOutput, relevantTasks) {
11904
12598
  */
11905
12599
  export async function buildObjectForGradleModule(name, metadata) {
11906
12600
  let component;
11907
- if (["true", "1"].includes(process.env.GRADLE_RESOLVE_FROM_NODE)) {
11908
- let tmpDir = metadata.properties.find(
12601
+ if (
12602
+ !["false", "0"].includes(process.env.GRADLE_RESOLVE_FROM_NODE) &&
12603
+ metadata.properties?.find(({ name }) => name === "projectDir")
12604
+ ) {
12605
+ let tmpDir = metadata.properties?.find(
11909
12606
  ({ name }) => name === "projectDir",
11910
12607
  ).value;
11911
12608
  if (tmpDir.indexOf("node_modules") !== -1) {
@@ -14183,6 +14880,9 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
14183
14880
  }
14184
14881
  const slicesData = JSON.parse(readFileSync(slicesFile, "utf-8"));
14185
14882
  if (slicesData && Object.keys(slicesData)) {
14883
+ thoughtLog(
14884
+ "Let's thoroughly inspect the dependency slice to identify where and how the components are used.",
14885
+ );
14186
14886
  if (slicesData.Dependencies) {
14187
14887
  for (const adep of slicesData.Dependencies) {
14188
14888
  // Case 1: Dependencies slice has the .dll file
@@ -14274,6 +14974,10 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
14274
14974
  });
14275
14975
  }
14276
14976
  }
14977
+ } else if (slicesData?.Dependencies || slicesData?.MethodCalls) {
14978
+ thoughtLog(
14979
+ "I didn't find any occurrence evidence or detailed imported modules, even though there is good dependency slice data from dosai. This is surprising.",
14980
+ );
14277
14981
  }
14278
14982
  return pkgList;
14279
14983
  }
@@ -14408,3 +15112,168 @@ export function recomputeScope(pkgList, dependencies) {
14408
15112
  }
14409
15113
  return pkgList;
14410
15114
  }
15115
+
15116
+ /**
15117
+ * Function to parse a list of environment variables to identify the paths containing executable binaries
15118
+ *
15119
+ * @param envValues {Array[String]} Environment variables list
15120
+ * @returns {Array[String]} Binary Paths identified from the environment variables
15121
+ */
15122
+ export function extractPathEnv(envValues) {
15123
+ if (!envValues) {
15124
+ return [];
15125
+ }
15126
+ let binPaths = new Set();
15127
+ const shellVariables = {};
15128
+ // Let's focus only on linux container images for now
15129
+ for (const env of envValues) {
15130
+ if (env.startsWith("PATH=")) {
15131
+ binPaths = new Set(env.replace("PATH=", "").split(":"));
15132
+ } else {
15133
+ const tmpA = env.split("=");
15134
+ if (tmpA.length === 2) {
15135
+ shellVariables[`$${tmpA[0]}`] = tmpA[1];
15136
+ shellVariables[`\${${tmpA[0]}}`] = tmpA[1];
15137
+ }
15138
+ }
15139
+ }
15140
+ binPaths = Array.from(binPaths);
15141
+ const expandedBinPaths = [];
15142
+ for (let apath of binPaths) {
15143
+ // Filter empty paths
15144
+ if (!apath.length) {
15145
+ continue;
15146
+ }
15147
+ if (apath.includes("$")) {
15148
+ for (const k of Object.keys(shellVariables)) {
15149
+ apath = apath.replace(k, shellVariables[k]);
15150
+ }
15151
+ }
15152
+ // We're here, but not all paths got substituted
15153
+ // Let's ignore them for now instead of risking substitution based on host values.
15154
+ // Eg: ${GITHUB_TOKEN} could get expanded with the values from the host
15155
+ if (apath.length && !apath.includes("$")) {
15156
+ expandedBinPaths.push(apath);
15157
+ }
15158
+ }
15159
+ return expandedBinPaths;
15160
+ }
15161
+
15162
+ /**
15163
+ * Collect all executable files from the given list of binary paths
15164
+ *
15165
+ * @param basePath Base directory
15166
+ * @param binPaths {Array[String]} Paths containing potential binaries
15167
+ * @return {Array[String]} List of executables
15168
+ */
15169
+ export function collectExecutables(basePath, binPaths) {
15170
+ if (!binPaths) {
15171
+ return [];
15172
+ }
15173
+ let executables = [];
15174
+ const ignoreList = [
15175
+ "**/*.{h,c,cpp,hpp,man,txt,md,htm,html,jar,ear,war,zip,tar,egg,keepme,gitignore,json,js,py,pyc}",
15176
+ "[",
15177
+ ];
15178
+ for (const apath of binPaths) {
15179
+ try {
15180
+ const files = globSync(`**${apath}/*`, {
15181
+ cwd: basePath,
15182
+ absolute: false,
15183
+ nocase: true,
15184
+ nodir: true,
15185
+ dot: true,
15186
+ follow: true,
15187
+ ignore: ignoreList,
15188
+ });
15189
+ executables = executables.concat(files);
15190
+ } catch (err) {
15191
+ // ignore
15192
+ }
15193
+ }
15194
+ return Array.from(new Set(executables)).sort();
15195
+ }
15196
+
15197
+ /**
15198
+ * Collect all shared library files from the given list of paths
15199
+ *
15200
+ * @param basePath Base directory
15201
+ * @param libPaths {Array[String]} Paths containing potential libraries
15202
+ * @param ldConf {String} Config file used by ldconfig to locate additional paths
15203
+ * @param ldConfDirPattern {String} Config directory that can contain more .conf files for ldconfig
15204
+ *
15205
+ * @return {Array[String]} List of executables
15206
+ */
15207
+ export function collectSharedLibs(
15208
+ basePath,
15209
+ libPaths,
15210
+ ldConf,
15211
+ ldConfDirPattern,
15212
+ ) {
15213
+ if (!libPaths) {
15214
+ return [];
15215
+ }
15216
+ let sharedLibs = [];
15217
+ const ignoreList = [
15218
+ "**/*.{h,c,cpp,hpp,man,txt,md,htm,html,jar,ear,war,zip,tar,egg,keepme,gitignore,json,js,py,pyc}",
15219
+ ];
15220
+ const allLdConfDirs = ldConfDirPattern ? [ldConfDirPattern] : [];
15221
+ collectAllLdConfs(basePath, ldConf, allLdConfDirs, libPaths);
15222
+ if (allLdConfDirs.length) {
15223
+ for (const aldconfPattern of allLdConfDirs) {
15224
+ const confFiles = globSync(aldconfPattern, {
15225
+ cwd: basePath,
15226
+ absolute: false,
15227
+ nocase: true,
15228
+ nodir: true,
15229
+ dot: true,
15230
+ follow: false,
15231
+ });
15232
+ for (const moreConf of confFiles) {
15233
+ collectAllLdConfs(basePath, moreConf, allLdConfDirs, libPaths);
15234
+ }
15235
+ }
15236
+ }
15237
+ for (const apath of Array.from(new Set(libPaths))) {
15238
+ try {
15239
+ const files = globSync(`**${apath}/*.{so,so.*,a,lib,dll}`, {
15240
+ cwd: basePath,
15241
+ absolute: false,
15242
+ nocase: true,
15243
+ nodir: true,
15244
+ dot: true,
15245
+ follow: true,
15246
+ ignore: ignoreList,
15247
+ });
15248
+ sharedLibs = sharedLibs.concat(files);
15249
+ } catch (err) {
15250
+ // ignore
15251
+ }
15252
+ }
15253
+ return Array.from(new Set(sharedLibs)).sort();
15254
+ }
15255
+
15256
+ function collectAllLdConfs(basePath, ldConf, allLdConfDirs, libPaths) {
15257
+ if (ldConf && existsSync(join(basePath, ldConf))) {
15258
+ const ldConfData = readFileSync(join(basePath, ldConf), "utf-8");
15259
+ for (let line of ldConfData.split("\n")) {
15260
+ line = line.replace("\r", "").trim();
15261
+ if (!line.length || line.startsWith("#")) {
15262
+ continue;
15263
+ }
15264
+ if (line.startsWith("include ")) {
15265
+ let apattern = line.replace("include ", "");
15266
+ if (!apattern.includes("*")) {
15267
+ apattern = `${apattern}/*.conf`;
15268
+ }
15269
+ if (!allLdConfDirs.includes(apattern)) {
15270
+ allLdConfDirs.push(apattern);
15271
+ }
15272
+ } else if (line.startsWith("/")) {
15273
+ if (!libPaths.includes(line)) {
15274
+ libPaths.push(line);
15275
+ }
15276
+ }
15277
+ }
15278
+ }
15279
+ }