@cdxgen/cdxgen 13.2.0 → 13.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 (158) hide show
  1. package/README.md +28 -12
  2. package/bin/cdxgen.js +227 -119
  3. package/bin/evinse.js +5 -0
  4. package/bin/sign.js +88 -14
  5. package/bin/tracebom.js +64 -4
  6. package/bin/validate.js +29 -7
  7. package/bin/verify.js +79 -39
  8. package/data/remediations.json +24 -0
  9. package/lib/cli/bomAssembly.js +105 -15
  10. package/lib/cli/cliOptions.js +93 -1
  11. package/lib/cli/index.js +43 -4
  12. package/lib/cli/jsBom.js +49 -24
  13. package/lib/cli/jvmBom.js +46 -12
  14. package/lib/cli/managedBom.js +112 -41
  15. package/lib/cli/nativeBom.js +125 -14
  16. package/lib/core/env.js +5 -6
  17. package/lib/core/fs.js +174 -6
  18. package/lib/core/metadataProperties.js +97 -0
  19. package/lib/core/projectConfig.js +31 -4
  20. package/lib/ecosystems/bunutils.js +269 -101
  21. package/lib/ecosystems/caxa.js +434 -2
  22. package/lib/ecosystems/cmakeResolver.js +66 -0
  23. package/lib/ecosystems/cppEvidence.js +64 -31
  24. package/lib/ecosystems/ecosystems.js +19 -8
  25. package/lib/ecosystems/gradleutils.js +42 -7
  26. package/lib/ecosystems/jsEvidence.js +19 -20
  27. package/lib/ecosystems/kosi.js +10 -3
  28. package/lib/ecosystems/npmutils.js +110 -9
  29. package/lib/ecosystems/parsers-dotnet.js +51 -10
  30. package/lib/ecosystems/parsers-go.js +47 -9
  31. package/lib/ecosystems/parsers-js.js +171 -50
  32. package/lib/ecosystems/parsers-misc.js +747 -161
  33. package/lib/ecosystems/parsers-python.js +26 -14
  34. package/lib/ecosystems/parsers-rust.js +24 -18
  35. package/lib/ecosystems/rubyutils.js +1426 -166
  36. package/lib/ecosystems/rusi.js +32 -1
  37. package/lib/evinser/evinser.js +436 -128
  38. package/lib/evinser/swiftsem.js +1914 -302
  39. package/lib/helpers/bomSigner.js +652 -221
  40. package/lib/inventory/atomUtils.js +568 -31
  41. package/lib/inventory/cdxrs.js +103 -12
  42. package/lib/inventory/depsUtils.js +183 -11
  43. package/lib/inventory/display.js +4 -5
  44. package/lib/inventory/dosai.js +550 -13
  45. package/lib/inventory/dynamic.js +39 -1
  46. package/lib/inventory/evidenceUtils.js +14 -6
  47. package/lib/inventory/fetchBatch.js +116 -17
  48. package/lib/inventory/protobom.js +8 -1
  49. package/lib/inventory/purl.js +152 -3
  50. package/lib/inventory/sbomDocument.js +2 -70
  51. package/lib/inventory/source.js +37 -19
  52. package/lib/inventory/traceRunner.js +210 -21
  53. package/lib/managers/binary.js +115 -24
  54. package/lib/managers/docker.js +440 -20
  55. package/lib/managers/oci.js +39 -1
  56. package/lib/parsers/cmakeLists.js +452 -0
  57. package/lib/parsers/largeJson.js +454 -0
  58. package/lib/parsers/npmrc.js +19 -1
  59. package/lib/stages/postgen/postgen.js +68 -15
  60. package/lib/validator/bomValidator.js +6 -16
  61. package/lib/validator/index.js +48 -10
  62. package/lib/validator/reporters/console.js +3 -0
  63. package/lib/validator/reporters/json.js +1 -0
  64. package/lib/validator/reporters/sarif.js +7 -0
  65. package/package.json +20 -20
  66. package/types/lib/cli/bomAssembly.d.ts +2 -1
  67. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  68. package/types/lib/cli/cliOptions.d.ts +33 -1
  69. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  70. package/types/lib/cli/index.d.ts.map +1 -1
  71. package/types/lib/cli/jsBom.d.ts.map +1 -1
  72. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  73. package/types/lib/cli/managedBom.d.ts.map +1 -1
  74. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  75. package/types/lib/core/env.d.ts.map +1 -1
  76. package/types/lib/core/fs.d.ts +55 -0
  77. package/types/lib/core/fs.d.ts.map +1 -1
  78. package/types/lib/core/metadataProperties.d.ts +39 -0
  79. package/types/lib/core/metadataProperties.d.ts.map +1 -0
  80. package/types/lib/core/projectConfig.d.ts +6 -2
  81. package/types/lib/core/projectConfig.d.ts.map +1 -1
  82. package/types/lib/ecosystems/bunutils.d.ts +12 -5
  83. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  84. package/types/lib/ecosystems/caxa.d.ts +14 -0
  85. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  86. package/types/lib/ecosystems/cmakeResolver.d.ts +23 -0
  87. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  88. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  89. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  90. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  91. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  92. package/types/lib/ecosystems/gradleutils.d.ts +20 -4
  93. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  94. package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
  95. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  96. package/types/lib/ecosystems/kosi.d.ts +13 -0
  97. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  98. package/types/lib/ecosystems/npmutils.d.ts +42 -0
  99. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  100. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  101. package/types/lib/ecosystems/parsers-go.d.ts +10 -0
  102. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  103. package/types/lib/ecosystems/parsers-js.d.ts +10 -4
  104. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  105. package/types/lib/ecosystems/parsers-misc.d.ts +67 -3
  106. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  107. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  108. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  109. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  110. package/types/lib/ecosystems/rubyutils.d.ts +143 -2
  111. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  112. package/types/lib/ecosystems/rusi.d.ts +13 -0
  113. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  114. package/types/lib/evinser/evinser.d.ts +59 -9
  115. package/types/lib/evinser/evinser.d.ts.map +1 -1
  116. package/types/lib/evinser/swiftsem.d.ts +280 -15
  117. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  118. package/types/lib/helpers/bomSigner.d.ts +106 -13
  119. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  120. package/types/lib/inventory/atomUtils.d.ts +145 -3
  121. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  122. package/types/lib/inventory/cdxrs.d.ts +8 -3
  123. package/types/lib/inventory/cdxrs.d.ts.map +1 -1
  124. package/types/lib/inventory/depsUtils.d.ts +15 -0
  125. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  126. package/types/lib/inventory/display.d.ts.map +1 -1
  127. package/types/lib/inventory/dosai.d.ts +64 -1
  128. package/types/lib/inventory/dosai.d.ts.map +1 -1
  129. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  130. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  131. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  132. package/types/lib/inventory/plugins.d.ts.map +1 -1
  133. package/types/lib/inventory/protobom.d.ts.map +1 -1
  134. package/types/lib/inventory/purl.d.ts +71 -2
  135. package/types/lib/inventory/purl.d.ts.map +1 -1
  136. package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
  137. package/types/lib/inventory/source.d.ts.map +1 -1
  138. package/types/lib/inventory/traceRunner.d.ts +47 -1
  139. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  140. package/types/lib/managers/binary.d.ts +30 -1
  141. package/types/lib/managers/binary.d.ts.map +1 -1
  142. package/types/lib/managers/docker.d.ts +29 -0
  143. package/types/lib/managers/docker.d.ts.map +1 -1
  144. package/types/lib/managers/oci.d.ts +14 -0
  145. package/types/lib/managers/oci.d.ts.map +1 -1
  146. package/types/lib/parsers/cmakeLists.d.ts +76 -0
  147. package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
  148. package/types/lib/parsers/largeJson.d.ts +52 -0
  149. package/types/lib/parsers/largeJson.d.ts.map +1 -0
  150. package/types/lib/parsers/npmrc.d.ts +5 -0
  151. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  152. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  153. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  154. package/types/lib/validator/index.d.ts +10 -1
  155. package/types/lib/validator/index.d.ts.map +1 -1
  156. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  157. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  158. package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
@@ -44,9 +44,21 @@ import {
44
44
  tryBuildPurl,
45
45
  } from "../inventory/purl.js";
46
46
  import { getLicenses, spdxLicenses } from "../inventory/spdx.js";
47
+ import { cmakeFetchDependencies } from "../parsers/cmakeLists.js";
48
+ import { submodulePurlCoordinates } from "../parsers/gitmodules.js";
47
49
  import { getDartMetadata } from "./ecosystems.js";
48
50
  import { parsePkgJson } from "./parsers-js.js";
49
51
 
52
+ /**
53
+ * Upper bound for a flake.nix `inputs = { ... }` block before the input-name
54
+ * patterns are applied. The patterns backtrack quadratically, so an unbounded
55
+ * attacker-crafted block is a CPU denial of service; genuine inputs blocks are
56
+ * a few kilobytes at most.
57
+ *
58
+ * @type {number}
59
+ */
60
+ const MAX_FLAKE_INPUTS_BLOCK_LENGTH = 16 * 1024;
61
+
50
62
  /**
51
63
  * Method to parse pubspec.lock files.
52
64
  *
@@ -1206,19 +1218,41 @@ export function parseFlakeNix(flakeNixFile) {
1206
1218
  let match;
1207
1219
  while ((match = inputsRegex.exec(flakeContent)) !== null) {
1208
1220
  const inputBlock = match[0];
1221
+ // The name-run patterns below backtrack quadratically over unbounded
1222
+ // blocks, so a crafted flake.nix could burn minutes of CPU. Real flake
1223
+ // inputs blocks are a few KB; anything larger is skipped.
1224
+ if (inputBlock.length > MAX_FLAKE_INPUTS_BLOCK_LENGTH) {
1225
+ if (DEBUG_MODE) {
1226
+ console.log(
1227
+ `Skipping a suspiciously large inputs block (${inputBlock.length} chars) in ${flakeNixFile}.`,
1228
+ );
1229
+ }
1230
+ continue;
1231
+ }
1209
1232
 
1210
- // Match different input patterns including nested inputs
1233
+ // Match different input patterns including nested inputs. The cheap
1234
+ // substring pre-checks keep URL-less blocks linear.
1211
1235
  const inputPatterns = [
1212
- /([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)\.url\s*=\s*"([^"]+)"/g,
1213
- /([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)\s*=\s*\{\s*url\s*=\s*"([^"]+)"[^}]*}/gs,
1236
+ {
1237
+ regex: /([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)\.url\s*=\s*"([^"]+)"/g,
1238
+ probe: ".url",
1239
+ },
1240
+ {
1241
+ regex:
1242
+ /([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)\s*=\s*\{\s*url\s*=\s*"([^"]+)"[^}]*}/gs,
1243
+ probe: "url",
1244
+ },
1214
1245
  ];
1215
1246
 
1216
1247
  const addedPackages = new Set();
1217
1248
 
1218
1249
  for (const pattern of inputPatterns) {
1250
+ if (!inputBlock.includes(pattern.probe)) {
1251
+ continue;
1252
+ }
1219
1253
  let subMatch;
1220
- pattern.lastIndex = 0;
1221
- while ((subMatch = pattern.exec(inputBlock)) !== null) {
1254
+ pattern.regex.lastIndex = 0;
1255
+ while ((subMatch = pattern.regex.exec(inputBlock)) !== null) {
1222
1256
  const name = subMatch[1];
1223
1257
  const url = subMatch[2] || subMatch[3];
1224
1258
 
@@ -1780,7 +1814,7 @@ export function parseSwiftJsonTreeObject(
1780
1814
  group: purl ? purl.namespace : "",
1781
1815
  version: purl ? purl.version : jsonObject.version,
1782
1816
  },
1783
- purl ? decodeURIComponent(purl.toString()) : null,
1817
+ purl ? purl.toString() : null,
1784
1818
  jsonObject.name,
1785
1819
  );
1786
1820
  const properties = [];
@@ -1886,15 +1920,23 @@ export function parseSwiftResolved(resolvedFile) {
1886
1920
  }
1887
1921
  for (const adep of resolvedList) {
1888
1922
  const locationOrUrl = adep.location || adep.repositoryURL;
1889
- const version = adep.state.version || adep.state.revision;
1923
+ const version = adep.state?.version || adep.state?.revision;
1924
+ // A pin with no remote URL (a local checkout) has no valid swift
1925
+ // namespace, so it becomes a component without a purl instead of
1926
+ // aborting the rest of the file.
1890
1927
  const purl = purlFromUrlString("swift", locationOrUrl, version);
1891
- const purlString = decodeURIComponent(purl.toString());
1928
+ const name =
1929
+ purl?.name ||
1930
+ adep.identity ||
1931
+ adep.package ||
1932
+ (locationOrUrl ? basename(locationOrUrl).replace(/\.git$/, "") : "");
1933
+ if (!name) {
1934
+ continue;
1935
+ }
1892
1936
  const rootPkg = {
1893
- name: purl.name,
1894
- group: purl.namespace,
1895
- version: purl.version,
1896
- purl: purlString,
1897
- "bom-ref": purlString,
1937
+ name,
1938
+ group: purl?.namespace || "",
1939
+ version: purl ? purl.version : version,
1898
1940
  properties: [
1899
1941
  {
1900
1942
  name: "internal:SrcFile",
@@ -1903,7 +1945,7 @@ export function parseSwiftResolved(resolvedFile) {
1903
1945
  ],
1904
1946
  evidence: {
1905
1947
  identity: {
1906
- field: "purl",
1948
+ field: purl ? "purl" : "name",
1907
1949
  confidence: 1,
1908
1950
  methods: [
1909
1951
  {
@@ -1915,6 +1957,7 @@ export function parseSwiftResolved(resolvedFile) {
1915
1957
  },
1916
1958
  },
1917
1959
  };
1960
+ applyPurl(rootPkg, purl ? purl.toString() : null);
1918
1961
  if (locationOrUrl) {
1919
1962
  rootPkg.repository = { url: locationOrUrl };
1920
1963
  }
@@ -1927,6 +1970,281 @@ export function parseSwiftResolved(resolvedFile) {
1927
1970
  return pkgList;
1928
1971
  }
1929
1972
 
1973
+ /**
1974
+ * Compute the bom-ref of a `swift package show-dependencies` node the same
1975
+ * way parseSwiftJsonTreeObject does.
1976
+ *
1977
+ * @param {Object} node show-dependencies node
1978
+ * @returns {undefined|string} bom-ref
1979
+ */
1980
+ function swiftTreeNodeRef(node) {
1981
+ if (node?.url?.startsWith("http")) {
1982
+ const purl = purlFromUrlString("swift", node.url, node.version);
1983
+ if (purl) {
1984
+ return decodeURIComponent(purl.toString());
1985
+ }
1986
+ }
1987
+ return node?.name;
1988
+ }
1989
+
1990
+ function swiftTreeNodeKeys(node) {
1991
+ const repoName = (node?.url || "")
1992
+ .split("?")[0]
1993
+ .replace(/\/+$/, "")
1994
+ .split("/")
1995
+ .pop()
1996
+ ?.replace(/\.git$/, "");
1997
+ return [node?.identity, node?.name, repoName]
1998
+ .filter(Boolean)
1999
+ .map((k) => String(k).toLowerCase());
2000
+ }
2001
+
2002
+ // Target types that are part of what the package builds and ships
2003
+ const SWIFT_PRODUCTION_TARGET_TYPES = new Set([
2004
+ "executable",
2005
+ "macro",
2006
+ "plugin",
2007
+ ]);
2008
+
2009
+ /**
2010
+ * Identify the targets of a Swift package that are part of what it ships.
2011
+ *
2012
+ * Production targets are the non-test targets that are part of a product,
2013
+ * executables, macros, plugins, targets nothing else depends on, and every
2014
+ * target they depend on. The remaining non-test targets only serve tests,
2015
+ * such as snapshot or fixture helper libraries.
2016
+ *
2017
+ * @param {Object} dumpJson `swift package dump-package` document
2018
+ * @returns {Set<string>} Lowercase names of the production targets
2019
+ */
2020
+ export function collectSwiftProductionTargets(dumpJson) {
2021
+ const targets = dumpJson?.targets || [];
2022
+ const targetsByName = new Map(
2023
+ targets.map((t) => [String(t?.name || "").toLowerCase(), t]),
2024
+ );
2025
+ const localTargetRefs = (target) => {
2026
+ const refs = [];
2027
+ for (const adep of target?.dependencies || []) {
2028
+ const name = adep?.target?.[0] ?? adep?.byName?.[0];
2029
+ if (name && targetsByName.has(String(name).toLowerCase())) {
2030
+ refs.push(String(name).toLowerCase());
2031
+ }
2032
+ }
2033
+ return refs;
2034
+ };
2035
+ const productTargets = new Set(
2036
+ (dumpJson?.products || []).flatMap((p) =>
2037
+ (p?.targets || []).map((t) => String(t).toLowerCase()),
2038
+ ),
2039
+ );
2040
+ const referencedTargets = new Set(targets.flatMap((t) => localTargetRefs(t)));
2041
+ const productionTargets = new Set();
2042
+ const stack = targets
2043
+ .filter(
2044
+ (t) =>
2045
+ !isSwiftTestTarget(t) &&
2046
+ (productTargets.has(String(t.name).toLowerCase()) ||
2047
+ SWIFT_PRODUCTION_TARGET_TYPES.has(t.type) ||
2048
+ !referencedTargets.has(String(t.name).toLowerCase())),
2049
+ )
2050
+ .map((t) => String(t.name).toLowerCase());
2051
+ while (stack.length) {
2052
+ const name = stack.pop();
2053
+ if (productionTargets.has(name)) {
2054
+ continue;
2055
+ }
2056
+ productionTargets.add(name);
2057
+ stack.push(...localTargetRefs(targetsByName.get(name)));
2058
+ }
2059
+ return productionTargets;
2060
+ }
2061
+
2062
+ /**
2063
+ * Classify the dependencies of a Swift package into the packages its shipped
2064
+ * code needs and the packages only its tests need.
2065
+ *
2066
+ * Production targets are the non-test targets that are part of a product,
2067
+ * executables, macros, plugins, targets nothing else depends on, and every
2068
+ * target they depend on. The remaining non-test targets only serve tests
2069
+ * (test-support libraries). A package is test-only when only test and
2070
+ * test-support targets reference its products or use its plugins. With the dependency graph from
2071
+ * `swift package show-dependencies`, the packages reachable only through
2072
+ * test-only packages are test-only as well; a package reachable from a
2073
+ * production dependency, or from a declared dependency no target references,
2074
+ * stays required.
2075
+ *
2076
+ * Target dependencies reference products either as
2077
+ * `{"product": [name, package]}` or `{"byName": [name]}`; SwiftPM resolves a
2078
+ * `byName` product through the package of the same name. When a production
2079
+ * `byName` reference names neither a local target nor a package, the
2080
+ * classification is abandoned rather than risking a required package being
2081
+ * scoped optional.
2082
+ *
2083
+ * @param {Object|String} dumpJson `swift package dump-package` document, parsed or as text
2084
+ * @param {Object|String} [treeJson] `swift package show-dependencies --format json` document, parsed or as text
2085
+ * @returns {Object} `{ optionalRefs, requiredRefs, optionalNames, requiredNames }`:
2086
+ * bom-refs from the dependency graph, and lowercase package identities,
2087
+ * names, and repository names (the only result without a graph)
2088
+ */
2089
+ export function classifySwiftDependencyScopes(dumpJson, treeJson) {
2090
+ // Command output may be handed over unparsed
2091
+ const parseJson = (value) => {
2092
+ if (typeof value !== "string") {
2093
+ return value;
2094
+ }
2095
+ try {
2096
+ return JSON.parse(value);
2097
+ } catch (_e) {
2098
+ return undefined;
2099
+ }
2100
+ };
2101
+ dumpJson = parseJson(dumpJson);
2102
+ treeJson = parseJson(treeJson);
2103
+ const empty = {
2104
+ optionalRefs: new Set(),
2105
+ requiredRefs: new Set(),
2106
+ optionalNames: new Set(),
2107
+ requiredNames: new Set(),
2108
+ };
2109
+ const targets = dumpJson?.targets || [];
2110
+ if (!targets.length) {
2111
+ return empty;
2112
+ }
2113
+ const targetsByName = new Map(
2114
+ targets.map((t) => [String(t?.name || "").toLowerCase(), t]),
2115
+ );
2116
+ const productionTargets = collectSwiftProductionTargets(dumpJson);
2117
+ const packageKeys = (target) => {
2118
+ const keys = [];
2119
+ const unresolved = [];
2120
+ for (const adep of target?.dependencies || []) {
2121
+ if (adep?.product) {
2122
+ keys.push(String(adep.product[1] ?? adep.product[0]).toLowerCase());
2123
+ } else if (adep?.byName?.[0]) {
2124
+ const name = String(adep.byName[0]).toLowerCase();
2125
+ if (!targetsByName.has(name)) {
2126
+ unresolved.push(name);
2127
+ }
2128
+ }
2129
+ }
2130
+ // Build tool plugins count for the target that uses them, so a plugin
2131
+ // used only by test targets is test-only
2132
+ for (const ausage of target?.pluginUsages || []) {
2133
+ if (ausage?.plugin?.[1]) {
2134
+ keys.push(String(ausage.plugin[1]).toLowerCase());
2135
+ }
2136
+ }
2137
+ return { keys, unresolved };
2138
+ };
2139
+ const directNodes = treeJson?.dependencies || [];
2140
+ const nodesByKey = new Map();
2141
+ for (const node of directNodes) {
2142
+ for (const key of swiftTreeNodeKeys(node)) {
2143
+ if (!nodesByKey.has(key)) {
2144
+ nodesByKey.set(key, node);
2145
+ }
2146
+ }
2147
+ }
2148
+ const prodKeys = new Set();
2149
+ const testKeys = new Set();
2150
+ for (const target of targets) {
2151
+ const isProduction =
2152
+ !isSwiftTestTarget(target) &&
2153
+ productionTargets.has(String(target.name).toLowerCase());
2154
+ const { keys, unresolved } = packageKeys(target);
2155
+ const bucket = isProduction ? prodKeys : testKeys;
2156
+ for (const key of keys) {
2157
+ bucket.add(key);
2158
+ }
2159
+ for (const name of unresolved) {
2160
+ // byName products resolve through the package of the same name
2161
+ if (!treeJson || nodesByKey.has(name)) {
2162
+ bucket.add(name);
2163
+ } else if (isProduction) {
2164
+ return empty;
2165
+ }
2166
+ }
2167
+ }
2168
+ if (!treeJson) {
2169
+ return {
2170
+ ...empty,
2171
+ optionalNames: new Set([...testKeys].filter((k) => !prodKeys.has(k))),
2172
+ requiredNames: prodKeys,
2173
+ };
2174
+ }
2175
+ // Package graph keyed by bom-ref
2176
+ const childrenByRef = new Map();
2177
+ const keysByRef = new Map();
2178
+ const collectGraph = (node) => {
2179
+ const ref = swiftTreeNodeRef(node);
2180
+ if (!ref || childrenByRef.has(ref)) {
2181
+ return ref;
2182
+ }
2183
+ childrenByRef.set(ref, new Set());
2184
+ keysByRef.set(ref, swiftTreeNodeKeys(node));
2185
+ for (const child of node?.dependencies || []) {
2186
+ const childRef = collectGraph(child);
2187
+ if (childRef) {
2188
+ childrenByRef.get(ref).add(childRef);
2189
+ }
2190
+ }
2191
+ return ref;
2192
+ };
2193
+ const prodRoots = [];
2194
+ const testRoots = [];
2195
+ for (const node of directNodes) {
2196
+ const ref = collectGraph(node);
2197
+ const keys = swiftTreeNodeKeys(node);
2198
+ const usedByProduction = keys.some((k) => prodKeys.has(k));
2199
+ const usedByTests = keys.some((k) => testKeys.has(k));
2200
+ if (usedByTests && !usedByProduction) {
2201
+ testRoots.push(ref);
2202
+ } else {
2203
+ // Production dependencies, and declared dependencies no target uses
2204
+ prodRoots.push(ref);
2205
+ }
2206
+ }
2207
+ const reach = (roots) => {
2208
+ const seen = new Set();
2209
+ const pending = [...roots];
2210
+ while (pending.length) {
2211
+ const ref = pending.pop();
2212
+ if (!ref || seen.has(ref)) {
2213
+ continue;
2214
+ }
2215
+ seen.add(ref);
2216
+ pending.push(...(childrenByRef.get(ref) || []));
2217
+ }
2218
+ return seen;
2219
+ };
2220
+ const requiredRefs = reach(prodRoots);
2221
+ const optionalRefs = new Set(
2222
+ [...reach(testRoots)].filter((ref) => !requiredRefs.has(ref)),
2223
+ );
2224
+ // Components from Package.resolved can carry a different version or URL
2225
+ // spelling than the graph, so the package names are reported as well
2226
+ const namesOf = (refs) =>
2227
+ new Set([...refs].flatMap((ref) => keysByRef.get(ref) || []));
2228
+ return {
2229
+ optionalRefs,
2230
+ requiredRefs,
2231
+ optionalNames: namesOf(optionalRefs),
2232
+ requiredNames: namesOf(requiredRefs),
2233
+ };
2234
+ }
2235
+
2236
+ /**
2237
+ * Decide whether a dump-package target is a test target. The `type` field is
2238
+ * usually the string `"test"`, but manifest APIs have also been observed
2239
+ * representing it as an object.
2240
+ *
2241
+ * @param {Object} atarget dump-package target
2242
+ * @returns {boolean} `true` for test targets
2243
+ */
2244
+ export function isSwiftTestTarget(atarget) {
2245
+ return atarget?.type === "test" || atarget?.type?.test === true;
2246
+ }
2247
+
1930
2248
  /**
1931
2249
  * Parse a CMake-generated dot/graphviz file and extract components and their dependency
1932
2250
  * relationships.
@@ -2047,12 +2365,256 @@ export function parseCmakeDotFile(dotFile, pkgType, options = {}) {
2047
2365
  };
2048
2366
  }
2049
2367
 
2368
+ /** CMake `URL_HASH` algorithm names, their CycloneDX spelling and hex length. */
2369
+ const CMAKE_HASH_ALGORITHMS = {
2370
+ MD5: ["MD5", 32],
2371
+ SHA1: ["SHA-1", 40],
2372
+ SHA256: ["SHA-256", 64],
2373
+ SHA384: ["SHA-384", 96],
2374
+ SHA512: ["SHA-512", 128],
2375
+ SHA3_256: ["SHA3-256", 64],
2376
+ SHA3_384: ["SHA3-384", 96],
2377
+ SHA3_512: ["SHA3-512", 128],
2378
+ };
2379
+
2380
+ /** Keywords of the CMake `find_*` commands; they are never package names. */
2381
+ const CMAKE_FIND_KEYWORDS = new Set([
2382
+ "BYPASS_PROVIDER",
2383
+ "CMAKE_FIND_ROOT_PATH_BOTH",
2384
+ "COMPONENTS",
2385
+ "CONFIG",
2386
+ "CONFIGS",
2387
+ "DOC",
2388
+ "ENV",
2389
+ "EXACT",
2390
+ "GLOBAL",
2391
+ "HINTS",
2392
+ "MODULE",
2393
+ "NAMES",
2394
+ "NAMES_PER_DIR",
2395
+ "NO_CACHE",
2396
+ "NO_CMAKE_BUILDS_PATH",
2397
+ "NO_CMAKE_ENVIRONMENT_PATH",
2398
+ "NO_CMAKE_FIND_ROOT_PATH",
2399
+ "NO_CMAKE_INSTALL_PREFIX",
2400
+ "NO_CMAKE_PACKAGE_REGISTRY",
2401
+ "NO_CMAKE_PATH",
2402
+ "NO_CMAKE_SYSTEM_PACKAGE_REGISTRY",
2403
+ "NO_CMAKE_SYSTEM_PATH",
2404
+ "NO_DEFAULT_PATH",
2405
+ "NO_MODULE",
2406
+ "NO_PACKAGE_ROOT_PATH",
2407
+ "NO_POLICY_SCOPE",
2408
+ "NO_SYSTEM_ENVIRONMENT_PATH",
2409
+ "ONLY_CMAKE_FIND_ROOT_PATH",
2410
+ "OPTIONAL",
2411
+ "OPTIONAL_COMPONENTS",
2412
+ "PATHS",
2413
+ "PATH_SUFFIXES",
2414
+ "QUIET",
2415
+ "REGISTRY_VIEW",
2416
+ "REQUIRED",
2417
+ "VALIDATOR",
2418
+ ]);
2419
+
2420
+ const CMAKE_FIND_COMMANDS = new Set([
2421
+ "find_package",
2422
+ "find_library",
2423
+ "find_dependency",
2424
+ "find_file",
2425
+ ]);
2426
+
2427
+ /**
2428
+ * A download URL that is safe to publish: http(s) or ftp only, with any user
2429
+ * information, query string and fragment removed.
2430
+ *
2431
+ * @param {string} url URL as written in the build file
2432
+ * @returns {string|undefined} The cleaned URL, or undefined when it is not one
2433
+ */
2434
+ function publishableDownloadUrl(url) {
2435
+ if (!url || url.includes("${")) {
2436
+ return undefined;
2437
+ }
2438
+ try {
2439
+ const parsed = new URL(url);
2440
+ if (!["http:", "https:", "ftp:"].includes(parsed.protocol)) {
2441
+ return undefined;
2442
+ }
2443
+ parsed.username = "";
2444
+ parsed.password = "";
2445
+ parsed.search = "";
2446
+ parsed.hash = "";
2447
+ return parsed.toString();
2448
+ } catch {
2449
+ return undefined;
2450
+ }
2451
+ }
2452
+
2453
+ /**
2454
+ * The repository and version of a GitHub source archive URL:
2455
+ * `https://github.com/<owner>/<repo>/archive/<ref>.zip` (also `refs/tags/...`
2456
+ * and `.tar.gz`) and
2457
+ * `https://github.com/<owner>/<repo>/releases/download/<tag>/<file>`.
2458
+ *
2459
+ * @param {string} url Download URL
2460
+ * @returns {{owner: string, repo: string, version: string}|undefined}
2461
+ */
2462
+ function githubArchiveCoordinates(url) {
2463
+ let parsed;
2464
+ try {
2465
+ parsed = new URL(url);
2466
+ } catch {
2467
+ return undefined;
2468
+ }
2469
+ if (parsed.hostname !== "github.com") {
2470
+ return undefined;
2471
+ }
2472
+ const segments = parsed.pathname.split("/").filter(Boolean);
2473
+ if (segments.length < 4) {
2474
+ return undefined;
2475
+ }
2476
+ const [owner, repo, section, ...rest] = segments;
2477
+ let version;
2478
+ if (section === "archive") {
2479
+ let ref = rest.join("/");
2480
+ for (const prefix of ["refs/tags/", "refs/heads/"]) {
2481
+ if (ref.startsWith(prefix)) {
2482
+ ref = ref.slice(prefix.length);
2483
+ }
2484
+ }
2485
+ for (const ext of [".tar.gz", ".tar.bz2", ".tar.xz", ".tgz", ".zip"]) {
2486
+ if (ref.endsWith(ext)) {
2487
+ ref = ref.slice(0, -ext.length);
2488
+ break;
2489
+ }
2490
+ }
2491
+ version = ref;
2492
+ } else if (section === "releases" && rest[0] === "download" && rest[1]) {
2493
+ version = rest[1];
2494
+ }
2495
+ if (!version) {
2496
+ return undefined;
2497
+ }
2498
+ return { owner, repo: repo.replace(/\.git$/, ""), version };
2499
+ }
2500
+
2501
+ /**
2502
+ * The source archive of the WrapDB release a Meson dependency name resolved to,
2503
+ * as a distribution reference carrying the archive's hash. The hash describes
2504
+ * that release's archive, which is not known to be the version the project
2505
+ * builds, so it belongs to the reference rather than to the component.
2506
+ *
2507
+ * @param {Object} awrap WrapDB entry
2508
+ * @returns {Object|undefined} External reference, or undefined without a URL
2509
+ */
2510
+ function wrapDbReference(awrap) {
2511
+ const url = publishableDownloadUrl(awrap?.analyzed_source_url);
2512
+ if (!url) {
2513
+ return undefined;
2514
+ }
2515
+ const reference = {
2516
+ type: "distribution",
2517
+ url,
2518
+ comment:
2519
+ `WrapDB release ${awrap.name_with_version || awrap.analyzed_version || ""}`.trim(),
2520
+ };
2521
+ if (/^[0-9a-fA-F]{64}$/.test(awrap.analyzed_source_hash || "")) {
2522
+ reference.hashes = [
2523
+ { alg: "SHA-256", content: awrap.analyzed_source_hash.toLowerCase() },
2524
+ ];
2525
+ }
2526
+ return reference;
2527
+ }
2528
+
2529
+ /**
2530
+ * The component for a dependency a CMake file downloads at configure time
2531
+ * (`FetchContent_Declare`, `ExternalProject_Add`, `CPMAddPackage`).
2532
+ *
2533
+ * A git repository becomes a `github` purl, or a `generic` one with a
2534
+ * `vcs_url`, at its tag or commit. A GitHub archive URL becomes a `github` purl
2535
+ * at the ref it names. Any other URL becomes a `generic` purl with a
2536
+ * `download_url`, and a `URL_HASH` becomes a hash. A version that still holds an
2537
+ * unresolved `${VAR}` is left out rather than published.
2538
+ *
2539
+ * @param {Object} dep Dependency facts from `cmakeFetchDependencies`
2540
+ * @param {string} srcFile The CMake file the dependency was read from
2541
+ * @returns {Object} The component
2542
+ */
2543
+ export function cmakeFetchDependencyComponent(dep, srcFile) {
2544
+ const resolved = (value) =>
2545
+ value && !value.includes("${") ? value : undefined;
2546
+ let version = resolved(dep.gitTag) || resolved(dep.version);
2547
+ let coords;
2548
+ const gitUrl = publishableDownloadUrl(dep.gitRepository);
2549
+ const downloadUrl = publishableDownloadUrl(dep.url);
2550
+ if (gitUrl) {
2551
+ coords = submodulePurlCoordinates(gitUrl, version);
2552
+ } else if (downloadUrl) {
2553
+ const archive = githubArchiveCoordinates(downloadUrl);
2554
+ if (archive) {
2555
+ version = version || archive.version;
2556
+ coords = {
2557
+ type: "github",
2558
+ namespace: archive.owner,
2559
+ name: archive.repo,
2560
+ version,
2561
+ };
2562
+ } else {
2563
+ coords = {
2564
+ type: "generic",
2565
+ name: dep.name,
2566
+ version: version || null,
2567
+ qualifiers: { download_url: downloadUrl },
2568
+ };
2569
+ }
2570
+ }
2571
+ const purl =
2572
+ (coords && tryBuildPurl(coords)) ||
2573
+ tryBuildPurl({ type: "generic", name: dep.name, version: version || null });
2574
+ const confidence = version ? 0.6 : 0.3;
2575
+ const component = {
2576
+ name: dep.name,
2577
+ version: version || "",
2578
+ type: "library",
2579
+ evidence: {
2580
+ identity: {
2581
+ field: "purl",
2582
+ confidence,
2583
+ methods: [
2584
+ {
2585
+ technique: "manifest-analysis",
2586
+ confidence,
2587
+ value: `Filename ${srcFile}`,
2588
+ },
2589
+ ],
2590
+ },
2591
+ },
2592
+ properties: [
2593
+ { name: "cdx:cmake:depKind", value: dep.kind },
2594
+ { name: "cdx:cmake:resolvedVia", value: "cmake-lists" },
2595
+ ],
2596
+ };
2597
+ applyPurl(component, purl, `library:${dep.name}:${version || ""}`);
2598
+ const [alg, content] = (dep.urlHash || "").split("=");
2599
+ const [cdxAlg, hexLength] = CMAKE_HASH_ALGORITHMS[alg?.toUpperCase()] || [];
2600
+ if (
2601
+ cdxAlg &&
2602
+ content?.length === hexLength &&
2603
+ /^[0-9a-fA-F]+$/.test(content)
2604
+ ) {
2605
+ component.hashes = [{ alg: cdxAlg, content: content.toLowerCase() }];
2606
+ }
2607
+ return component;
2608
+ }
2609
+
2050
2610
  /**
2051
2611
  * Parse a CMake-like build file (CMakeLists.txt, meson.build, etc.) and extract the
2052
2612
  * parent component and list of dependency packages.
2053
2613
  *
2054
2614
  * Handles `set`, `project`, `find_package`, `find_library`, `find_dependency`,
2055
- * `find_file`, `FetchContent_MakeAvailable`, and `dependency()` directives.
2615
+ * `find_file` and `dependency()` directives (CMake command names in any case),
2616
+ * and, for CMake files, the dependencies downloaded at configure time by
2617
+ * `FetchContent_Declare`, `ExternalProject_Add` and `CPMAddPackage`.
2056
2618
  * Uses the MesonWrapDB to improve name resolution confidence.
2057
2619
  *
2058
2620
  * @param {string} cmakeListFile Path to the CMake-like build file
@@ -2061,13 +2623,30 @@ export function parseCmakeDotFile(dotFile, pkgType, options = {}) {
2061
2623
  * @returns {{ parentComponent: Object, pkgList: Object[] }}
2062
2624
  */
2063
2625
  export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
2064
- let cmakeListData = readFileSync(cmakeListFile, { encoding: "utf-8" });
2626
+ const rawData = readFileSync(cmakeListFile, { encoding: "utf-8" });
2627
+ let cmakeListData = rawData;
2065
2628
  const pkgList = [];
2066
2629
  const pkgAddedMap = {};
2067
2630
  const versionSpecifiersMap = {};
2068
2631
  const versionsMap = {};
2069
2632
  let parentComponent = {};
2070
2633
  const templateValues = {};
2634
+ // A dependency fetched at configure time is the same package a find_package()
2635
+ // of the same name asks for: the fetched component, with its source and pin,
2636
+ // stands for both.
2637
+ const fetchedNames = new Set();
2638
+ if (!basename(cmakeListFile).startsWith("meson")) {
2639
+ const fetchedRefs = new Set();
2640
+ for (const dep of cmakeFetchDependencies(rawData)) {
2641
+ const component = cmakeFetchDependencyComponent(dep, cmakeListFile);
2642
+ if (fetchedRefs.has(component["bom-ref"])) {
2643
+ continue;
2644
+ }
2645
+ fetchedRefs.add(component["bom-ref"]);
2646
+ fetchedNames.add(dep.name.toLowerCase());
2647
+ pkgList.push(component);
2648
+ }
2649
+ }
2071
2650
  cmakeListData = cmakeListData
2072
2651
  .replace(/^ {2}/g, "")
2073
2652
  .replace(/\(\r\n/g, "(")
@@ -2082,138 +2661,129 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
2082
2661
  const group = "";
2083
2662
  const path = undefined;
2084
2663
  const name_list = [];
2085
- if (l.startsWith("set")) {
2086
- const tmpA = l.replace("set(", "").replace(")", "").trim().split(" ");
2664
+ // CMake command names are case-insensitive: `PROJECT(...)` and
2665
+ // `Find_Package(...)` are the same commands as their lower-case spellings.
2666
+ const openParen = l.indexOf("(");
2667
+ const command =
2668
+ openParen > 0 ? l.slice(0, openParen).trim().toLowerCase() : "";
2669
+ let commandArgs = openParen > 0 ? l.slice(openParen + 1) : "";
2670
+ if (command === "set") {
2671
+ const tmpA = commandArgs.replace(")", "").trim().split(" ");
2087
2672
  if (tmpA && tmpA.length === 2) {
2088
2673
  templateValues[tmpA[0]] = tmpA[1];
2089
2674
  }
2090
- } else if (
2091
- l.startsWith("project") &&
2092
- !Object.keys(parentComponent).length
2093
- ) {
2094
- if (l.includes("${")) {
2675
+ } else if (command === "project" && !Object.keys(parentComponent).length) {
2676
+ if (commandArgs.includes("${")) {
2095
2677
  for (const tmplKey of Object.keys(templateValues)) {
2096
- l = l.replace(`\${${tmplKey}}`, templateValues[tmplKey] || "");
2678
+ commandArgs = commandArgs.replace(
2679
+ `\${${tmplKey}}`,
2680
+ templateValues[tmplKey] || "",
2681
+ );
2097
2682
  }
2098
2683
  }
2099
- const tmpA = l.replace("project (", "project(").split("project(");
2100
- if (tmpA && tmpA.length > 1) {
2101
- const tmpB = (tmpA[1] || "")
2102
- .trim()
2103
- .replace(/["']/g, "")
2104
- .replace(/ /g, ",")
2105
- .split(")")[0]
2106
- .split(",")
2107
- .filter((v) => v.length > 1);
2108
- const parentName =
2109
- tmpB.length > 0 ? tmpB[0].replace(":", "").trim() : "";
2110
- let parentVersion;
2111
- // In case of meson.build we can find the version number after the word version
2112
- // thanks to our replaces and splits
2113
- const versionIndex = tmpB.findIndex(
2114
- (v) => v?.toLowerCase() === "version",
2684
+ const tmpB = commandArgs
2685
+ .trim()
2686
+ .replace(/["']/g, "")
2687
+ .replace(/ /g, ",")
2688
+ .split(")")[0]
2689
+ .split(",")
2690
+ .filter((v) => v.length > 1);
2691
+ const parentName = tmpB.length > 0 ? tmpB[0].replace(":", "").trim() : "";
2692
+ let parentVersion;
2693
+ // In case of meson.build we can find the version number after the word version
2694
+ // thanks to our replaces and splits
2695
+ const versionIndex = tmpB.findIndex(
2696
+ (v) => v?.toLowerCase() === "version",
2697
+ );
2698
+ if (versionIndex > -1 && tmpB.length > versionIndex) {
2699
+ parentVersion = tmpB[versionIndex + 1];
2700
+ }
2701
+ // A `${VAR}` that no `set()` in this file defines stays literal. It is
2702
+ // not a version, and percent-encoding it into a purl publishes the
2703
+ // variable name as the version, so the field is left empty instead.
2704
+ // CMakeCache.txt carries the evaluated value when a build tree exists.
2705
+ if (parentVersion?.includes("$")) {
2706
+ parentVersion = undefined;
2707
+ }
2708
+ if (parentName?.length && !parentName.includes("$")) {
2709
+ parentComponent = {
2710
+ group: options.projectGroup || "",
2711
+ name: parentName,
2712
+ version: parentVersion || options.projectVersion || "",
2713
+ type: "application",
2714
+ };
2715
+ const safePath = path ? path.replace(/^\/+/, "") : null;
2716
+ parentComponent["purl"] = build({
2717
+ type: pkgType,
2718
+ namespace: parentComponent.group || null,
2719
+ name: parentComponent.name,
2720
+ version: parentComponent.version || null,
2721
+ subpath: safePath,
2722
+ });
2723
+ parentComponent["bom-ref"] = decodeURIComponent(
2724
+ parentComponent["purl"],
2115
2725
  );
2116
- if (versionIndex > -1 && tmpB.length > versionIndex) {
2117
- parentVersion = tmpB[versionIndex + 1];
2118
- }
2119
- // A `${VAR}` that no `set()` in this file defines stays literal. It is
2120
- // not a version, and percent-encoding it into a purl publishes the
2121
- // variable name as the version, so the field is left empty instead.
2122
- // CMakeCache.txt carries the evaluated value when a build tree exists.
2123
- if (parentVersion?.includes("$")) {
2124
- parentVersion = undefined;
2125
- }
2126
- if (parentName?.length && !parentName.includes("$")) {
2127
- parentComponent = {
2128
- group: options.projectGroup || "",
2129
- name: parentName,
2130
- version: parentVersion || options.projectVersion || "",
2131
- type: "application",
2132
- };
2133
- const safePath = path ? path.replace(/^\/+/, "") : null;
2134
- parentComponent["purl"] = build({
2135
- type: pkgType,
2136
- namespace: parentComponent.group || null,
2137
- name: parentComponent.name,
2138
- version: parentComponent.version || null,
2139
- subpath: safePath,
2140
- });
2141
- parentComponent["bom-ref"] = decodeURIComponent(
2142
- parentComponent["purl"],
2143
- );
2144
- }
2145
2726
  }
2146
- } else if (l.startsWith("find_")) {
2147
- let tmpA = [];
2148
- for (const fm of [
2149
- "find_package(",
2150
- "find_library(",
2151
- "find_dependency(",
2152
- "find_file(",
2153
- "FetchContent_MakeAvailable(",
2154
- ]) {
2155
- if (l.startsWith(fm)) {
2156
- tmpA = l.split(fm);
2157
- break;
2727
+ } else if (CMAKE_FIND_COMMANDS.has(command)) {
2728
+ let tmpB = commandArgs.split(")")[0].split(" ");
2729
+ tmpB = tmpB.filter(
2730
+ (v) =>
2731
+ !CMAKE_FIND_KEYWORDS.has(v) &&
2732
+ !v.includes("$") &&
2733
+ !v.startsWith("CMAKE_") &&
2734
+ v.length,
2735
+ );
2736
+ // find_package(Catch2)
2737
+ // find_package(GTest REQUIRED)
2738
+ // find_package(Boost 1.79 COMPONENTS date_time)
2739
+ // find_library(PTHREADPOOL_LIB pthreadpool REQUIRED)
2740
+ // find_library and find_file name the cache variable that receives the
2741
+ // result in their first argument, so that argument is a destination
2742
+ // rather than a package. Every other command in this list opens with a
2743
+ // package name, including names such as `ZLIB` that read like a
2744
+ // variable.
2745
+ // Their names are the values after NAMES, or else the one argument after
2746
+ // the variable; the remaining arguments are search paths and options.
2747
+ if (command === "find_library" || command === "find_file") {
2748
+ const tokens = commandArgs
2749
+ .split(")")[0]
2750
+ .split(" ")
2751
+ .filter((v) => v.length);
2752
+ const namesAt = tokens.indexOf("NAMES");
2753
+ let names = tokens.slice(1, 2);
2754
+ if (namesAt > -1) {
2755
+ names = [];
2756
+ for (const token of tokens.slice(namesAt + 1)) {
2757
+ if (CMAKE_FIND_KEYWORDS.has(token)) {
2758
+ break;
2759
+ }
2760
+ names.push(token);
2761
+ }
2158
2762
  }
2763
+ tmpB = names.filter((v) => !v.includes("$") && !v.startsWith("CMAKE_"));
2159
2764
  }
2160
- if (tmpA && tmpA.length > 1) {
2161
- let tmpB = tmpA[1].split(")")[0].split(" ");
2162
- tmpB = tmpB.filter(
2163
- (v) =>
2164
- ![
2165
- "REQUIRED",
2166
- "COMPONENTS",
2167
- "QUIET",
2168
- "NAMES",
2169
- "PATHS",
2170
- "ENV",
2171
- "NO_MODULE",
2172
- "NO_DEFAULT_PATH",
2173
- ].includes(v) &&
2174
- !v.includes("$") &&
2175
- !v.startsWith("CMAKE_") &&
2176
- v.length,
2177
- );
2178
- // find_package(Catch2)
2179
- // find_package(GTest REQUIRED)
2180
- // find_package(Boost 1.79 COMPONENTS date_time)
2181
- // find_library(PTHREADPOOL_LIB pthreadpool REQUIRED)
2182
- // find_library and find_file name the cache variable that receives the
2183
- // result in their first argument, so that argument is a destination
2184
- // rather than a package. Every other command in this list opens with a
2185
- // package name, including names such as `ZLIB` that read like a
2186
- // variable.
2187
- if (l.startsWith("find_library") || l.startsWith("find_file")) {
2188
- tmpB = tmpB.slice(1);
2189
- }
2190
- if (tmpB.length) {
2191
- const working_name = tmpB[0];
2192
- name_list.push(working_name);
2193
- if (l.startsWith("find_package") && tmpB.length > 1) {
2194
- if (
2195
- /^\d+/.test(tmpB[1]) &&
2196
- !tmpB[1].includes("${") &&
2197
- !tmpB[1].startsWith("@")
2198
- ) {
2199
- versionsMap[working_name] = tmpB[1];
2765
+ if (tmpB.length) {
2766
+ const working_name = tmpB[0];
2767
+ name_list.push(working_name);
2768
+ if (command === "find_package" && tmpB.length > 1) {
2769
+ if (
2770
+ /^\d+/.test(tmpB[1]) &&
2771
+ !tmpB[1].includes("${") &&
2772
+ !tmpB[1].startsWith("@")
2773
+ ) {
2774
+ versionsMap[working_name] = tmpB[1];
2775
+ }
2776
+ } else {
2777
+ for (const n of tmpB) {
2778
+ if (n.match(/^\d/)) {
2779
+ continue;
2200
2780
  }
2201
- } else {
2202
- for (const n of tmpB) {
2203
- if (n.match(/^\d/)) {
2204
- continue;
2205
- }
2206
- if (n.includes(_sep)) {
2207
- if (
2208
- n.includes(".so") ||
2209
- n.includes(".a") ||
2210
- n.includes(".dll")
2211
- ) {
2212
- name_list.push(basename(n));
2213
- }
2214
- } else {
2215
- name_list.push(n);
2781
+ if (n.includes(_sep)) {
2782
+ if (n.includes(".so") || n.includes(".a") || n.includes(".dll")) {
2783
+ name_list.push(basename(n));
2216
2784
  }
2785
+ } else {
2786
+ name_list.push(n);
2217
2787
  }
2218
2788
  }
2219
2789
  }
@@ -2245,10 +2815,12 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
2245
2815
  for (let n of name_list) {
2246
2816
  const props = [];
2247
2817
  let confidence = 0;
2818
+ let wrapReference;
2248
2819
  if (
2249
2820
  n &&
2250
2821
  n.length > 1 &&
2251
2822
  !pkgAddedMap[n] &&
2823
+ !fetchedNames.has(n.toLowerCase()) &&
2252
2824
  !n.startsWith(_sep) &&
2253
2825
  !n.startsWith("@")
2254
2826
  ) {
@@ -2263,14 +2835,18 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
2263
2835
  ) {
2264
2836
  // Use the new name
2265
2837
  n = wrapkey;
2266
- for (const eprop of Object.keys(awrap)) {
2838
+ wrapReference = wrapDbReference(awrap);
2839
+ props.push({ name: "cdx:meson:wrapdb:wrap", value: wrapkey });
2840
+ if (awrap.analyzed_version) {
2267
2841
  props.push({
2268
- name: eprop,
2269
- value: Array.isArray(awrap[eprop])
2270
- ? awrap[eprop].join("|")
2271
- : awrap[eprop],
2842
+ name: "cdx:meson:wrapdb:latestVersion",
2843
+ value: awrap.analyzed_version,
2272
2844
  });
2273
2845
  }
2846
+ props.push({
2847
+ name: "internal:PkgProvides",
2848
+ value: awrap.PkgProvides.join("|"),
2849
+ });
2274
2850
  // Our confidence has improved from 0 since there is a matching wrap so we know the correct name
2275
2851
  // and url. We lack the version details.
2276
2852
  confidence = 0.5;
@@ -2321,6 +2897,9 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
2321
2897
  },
2322
2898
  properties: props,
2323
2899
  };
2900
+ if (wrapReference) {
2901
+ apkg.externalReferences = [wrapReference];
2902
+ }
2324
2903
  apkg["bom-ref"] = decodeURIComponent(apkg["purl"]);
2325
2904
  pkgList.push(apkg);
2326
2905
  pkgAddedMap[n] = true;
@@ -2350,41 +2929,48 @@ export function parseCUsageSlice(sliceData) {
2350
2929
  return undefined;
2351
2930
  }
2352
2931
  const usageData = {};
2353
- try {
2354
- const objectSlices = sliceData.objectSlices || [];
2355
- for (const slice of objectSlices) {
2932
+ const objectSlices = sliceData.objectSlices || [];
2933
+ for (const slice of objectSlices) {
2934
+ // One malformed slice must not cost the slices after it.
2935
+ try {
2936
+ const fileName = slice?.fileName || "";
2937
+ const isInclude = (slice?.code || "").startsWith("#include");
2356
2938
  if (
2357
- (!slice.fileName && !slice.code.startsWith("#include")) ||
2358
- slice.fileName.startsWith("<includes") ||
2359
- slice.fileName.startsWith("<global") ||
2360
- slice.fileName.includes("__")
2939
+ (!fileName && !isInclude) ||
2940
+ fileName.startsWith("<includes") ||
2941
+ fileName.startsWith("<global") ||
2942
+ fileName.includes("__")
2361
2943
  ) {
2362
2944
  continue;
2363
2945
  }
2364
- const slFileName = slice.fileName;
2365
- const allLines = usageData[slFileName] || new Set();
2946
+ const allLines = (fileName && usageData[fileName]) || new Set();
2366
2947
  if (slice.fullName && slice.fullName.length > 3) {
2367
- if (slice.code?.startsWith("#include")) {
2368
- usageData[slice.fullName] = new Set();
2948
+ if (isInclude) {
2949
+ // an included file keeps the symbols already recorded for it
2950
+ usageData[slice.fullName] = usageData[slice.fullName] || new Set();
2369
2951
  } else {
2370
2952
  allLines.add(slice.fullName);
2371
2953
  }
2372
2954
  }
2373
- for (const ausage of slice.usages) {
2374
- let calls = ausage?.invokedCalls || [];
2375
- calls = calls.concat(ausage?.argToCalls || []);
2955
+ for (const ausage of slice.usages || []) {
2956
+ const calls = (ausage?.invokedCalls || []).concat(
2957
+ ausage?.argToCalls || [],
2958
+ );
2376
2959
  for (const acall of calls) {
2377
- if (!acall.resolvedMethod.includes("->")) {
2378
- allLines.add(acall.resolvedMethod);
2960
+ const resolvedMethod = acall?.resolvedMethod;
2961
+ if (resolvedMethod && !resolvedMethod.includes("->")) {
2962
+ allLines.add(resolvedMethod);
2379
2963
  }
2380
2964
  }
2381
2965
  }
2382
- if (Array.from(allLines).length) {
2383
- usageData[slFileName] = allLines;
2966
+ if (fileName && allLines.size) {
2967
+ usageData[fileName] = allLines;
2384
2968
  }
2969
+ } catch (err) {
2970
+ thoughtLog("Skipping a C usage slice that could not be read.", {
2971
+ error: err?.message,
2972
+ });
2385
2973
  }
2386
- } catch (_err) {
2387
- // ignore
2388
2974
  }
2389
2975
  return usageData;
2390
2976
  }