@cdxgen/cdxgen 13.1.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.
- package/README.md +32 -15
- package/bin/cdxgen.js +243 -122
- package/bin/evinse.js +7 -0
- package/bin/sign.js +88 -14
- package/bin/tracebom.js +64 -4
- package/bin/validate.js +29 -7
- package/bin/verify.js +79 -39
- package/data/crypto-oid.json +88 -12
- package/data/remediations.json +47 -0
- package/data/rules/ai-bom.yaml +78 -0
- package/data/rules/cargo-rust.yaml +132 -0
- package/data/rules/mcp-servers.yaml +122 -0
- package/lib/audit/targets.js +6 -1
- package/lib/cli/bomAssembly.js +117 -15
- package/lib/cli/cliOptions.js +93 -1
- package/lib/cli/index.js +235 -5
- package/lib/cli/jsBom.js +108 -26
- package/lib/cli/jvmBom.js +68 -12
- package/lib/cli/managedBom.js +264 -41
- package/lib/cli/nativeBom.js +923 -15
- package/lib/core/activity.js +120 -2
- package/lib/core/env.js +44 -7
- package/lib/core/fs.js +174 -6
- package/lib/core/metadataProperties.js +97 -0
- package/lib/core/projectConfig.js +31 -4
- package/lib/core/userAgent.js +151 -0
- package/lib/ecosystems/bunutils.js +269 -101
- package/lib/ecosystems/cargoCache.js +364 -0
- package/lib/ecosystems/caxa.js +447 -7
- package/lib/ecosystems/cmakeResolver.js +66 -0
- package/lib/ecosystems/cppEvidence.js +104 -33
- package/lib/ecosystems/ecosystems.js +260 -99
- package/lib/ecosystems/golem.js +11 -1
- package/lib/ecosystems/gradleutils.js +185 -9
- package/lib/ecosystems/jsEvidence.js +19 -20
- package/lib/ecosystems/kosi.js +1263 -0
- package/lib/ecosystems/npmutils.js +110 -9
- package/lib/ecosystems/parsers-crystal.js +179 -0
- package/lib/ecosystems/parsers-dotnet.js +120 -49
- package/lib/ecosystems/parsers-elm.js +816 -0
- package/lib/ecosystems/parsers-gleam.js +1 -1
- package/lib/ecosystems/parsers-go.js +47 -9
- package/lib/ecosystems/parsers-js.js +171 -50
- package/lib/ecosystems/parsers-julia.js +268 -0
- package/lib/ecosystems/parsers-lua.js +218 -0
- package/lib/ecosystems/parsers-misc.js +919 -161
- package/lib/ecosystems/parsers-nim.js +221 -0
- package/lib/ecosystems/parsers-opam.js +180 -0
- package/lib/ecosystems/parsers-perl.js +277 -0
- package/lib/ecosystems/parsers-python.js +27 -15
- package/lib/ecosystems/parsers-r.js +148 -0
- package/lib/ecosystems/parsers-rebar.js +411 -0
- package/lib/ecosystems/parsers-rust.js +954 -33
- package/lib/ecosystems/parsers-spack.js +204 -0
- package/lib/ecosystems/parsers-stack.js +264 -0
- package/lib/ecosystems/parsers-terraform.js +194 -0
- package/lib/ecosystems/rubyutils.js +1426 -166
- package/lib/ecosystems/rusi.js +158 -7
- package/lib/evinser/evinser.js +547 -129
- package/lib/evinser/swiftsem.js +1914 -302
- package/lib/helpers/agenticToolutils.js +925 -0
- package/lib/helpers/bomSigner.js +652 -221
- package/lib/inventory/agentFormulationParser.js +57 -4
- package/lib/inventory/aiCollector.js +504 -10
- package/lib/inventory/aiInventory.js +12 -0
- package/lib/inventory/analyzer.js +488 -30
- package/lib/inventory/astWorker.js +6 -0
- package/lib/inventory/atomUtils.js +568 -31
- package/lib/inventory/auditCategories.js +8 -0
- package/lib/inventory/cbomutils.js +30 -0
- package/lib/inventory/cdxrs.js +110 -14
- package/lib/inventory/communityAiConfigParser.js +215 -2
- package/lib/inventory/depsUtils.js +183 -11
- package/lib/inventory/display.js +4 -5
- package/lib/inventory/dosai.js +627 -13
- package/lib/inventory/dynamic.js +39 -1
- package/lib/inventory/envcontext.js +9 -6
- package/lib/inventory/evidenceUtils.js +26 -5
- package/lib/inventory/fetchBatch.js +190 -19
- package/lib/inventory/fetchRate.js +74 -11
- package/lib/inventory/forgeEnricher.js +3 -2
- package/lib/inventory/jvmToolEnv.js +6 -5
- package/lib/inventory/mcpConfigParser.js +166 -2
- package/lib/inventory/mcpDiscovery.js +11 -0
- package/lib/inventory/mcpServerManifest.js +389 -0
- package/lib/inventory/mcpbBundle.js +419 -0
- package/lib/inventory/plugins.js +3 -0
- package/lib/inventory/protobom.js +62 -89
- package/lib/inventory/protobomLoader.js +4 -0
- package/lib/inventory/purl.js +152 -3
- package/lib/inventory/rustFormulationParser.js +1 -1
- package/lib/inventory/sbomDocument.js +2 -70
- package/lib/inventory/source.js +47 -23
- package/lib/inventory/stylePackageRefs.js +41 -0
- package/lib/inventory/svelteUtils.js +524 -0
- package/lib/inventory/traceRunner.js +210 -21
- package/lib/managers/binary.js +115 -24
- package/lib/managers/docker.js +440 -20
- package/lib/managers/oci.js +39 -1
- package/lib/parsers/cmakeLists.js +452 -0
- package/lib/parsers/huggingfaceUtils.js +15 -7
- package/lib/parsers/largeJson.js +454 -0
- package/lib/parsers/npmrc.js +19 -1
- package/lib/parsers/onnx.js +294 -0
- package/lib/parsers/safetensors.js +114 -0
- package/lib/stages/postgen/introspection/markers.js +29 -6
- package/lib/stages/postgen/postgen.js +237 -16
- package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
- package/lib/validator/bomValidator.js +6 -16
- package/lib/validator/index.js +48 -10
- package/lib/validator/reporters/console.js +3 -0
- package/lib/validator/reporters/json.js +1 -0
- package/lib/validator/reporters/sarif.js +7 -0
- package/package.json +27 -27
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts +2 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +33 -1
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +2 -2
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts +23 -0
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts +33 -0
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts +88 -8
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/activity.d.ts.map +1 -1
- package/types/lib/core/env.d.ts +21 -0
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +55 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/metadataProperties.d.ts +39 -0
- package/types/lib/core/metadataProperties.d.ts.map +1 -0
- package/types/lib/core/projectConfig.d.ts +6 -2
- package/types/lib/core/projectConfig.d.ts.map +1 -1
- package/types/lib/core/userAgent.d.ts +56 -0
- package/types/lib/core/userAgent.d.ts.map +1 -0
- package/types/lib/ecosystems/bunutils.d.ts +12 -5
- package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
- package/types/lib/ecosystems/cargoCache.d.ts +75 -0
- package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
- package/types/lib/ecosystems/caxa.d.ts +14 -0
- package/types/lib/ecosystems/caxa.d.ts.map +1 -1
- package/types/lib/ecosystems/cmakeResolver.d.ts +23 -0
- package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts +2 -2
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts +15 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +40 -4
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
- package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts +86 -0
- package/types/lib/ecosystems/kosi.d.ts.map +1 -0
- package/types/lib/ecosystems/npmutils.d.ts +42 -0
- package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
- package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
- package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-go.d.ts +10 -0
- package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts +10 -4
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
- package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
- package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-misc.d.ts +90 -3
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
- package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
- package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
- package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-r.d.ts +35 -0
- package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
- package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
- package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
- package/types/lib/ecosystems/pythonutils.d.ts +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts +143 -2
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts +13 -0
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +81 -2
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/swiftsem.d.ts +280 -15
- package/types/lib/evinser/swiftsem.d.ts.map +1 -1
- package/types/lib/helpers/agenticToolutils.d.ts +87 -0
- package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
- package/types/lib/helpers/bomSigner.d.ts +106 -13
- package/types/lib/helpers/bomSigner.d.ts.map +1 -1
- package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/aiInventory.d.ts.map +1 -1
- package/types/lib/inventory/analyzer.d.ts.map +1 -1
- package/types/lib/inventory/atomUtils.d.ts +145 -3
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +4 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/cbomutils.d.ts.map +1 -1
- package/types/lib/inventory/cdxrs.d.ts +8 -3
- package/types/lib/inventory/cdxrs.d.ts.map +1 -1
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/depsUtils.d.ts +15 -0
- package/types/lib/inventory/depsUtils.d.ts.map +1 -1
- package/types/lib/inventory/display.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +87 -1
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dynamic.d.ts.map +1 -1
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +34 -0
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/fetchRate.d.ts +9 -6
- package/types/lib/inventory/fetchRate.d.ts.map +1 -1
- package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
- package/types/lib/inventory/jvmToolEnv.d.ts +5 -4
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
- package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
- package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
- package/types/lib/inventory/mcpbBundle.d.ts +19 -0
- package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
- package/types/lib/inventory/plugins.d.ts.map +1 -1
- package/types/lib/inventory/protobom.d.ts +18 -13
- package/types/lib/inventory/protobom.d.ts.map +1 -1
- package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
- package/types/lib/inventory/purl.d.ts +71 -2
- package/types/lib/inventory/purl.d.ts.map +1 -1
- package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
- package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
- package/types/lib/inventory/svelteUtils.d.ts +124 -0
- package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
- package/types/lib/inventory/traceRunner.d.ts +47 -1
- package/types/lib/inventory/traceRunner.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +30 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +29 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts +14 -0
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/parsers/cmakeLists.d.ts +76 -0
- package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
- package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
- package/types/lib/parsers/largeJson.d.ts +52 -0
- package/types/lib/parsers/largeJson.d.ts.map +1 -0
- package/types/lib/parsers/npmrc.d.ts +5 -0
- package/types/lib/parsers/npmrc.d.ts.map +1 -1
- package/types/lib/parsers/onnx.d.ts +28 -0
- package/types/lib/parsers/onnx.d.ts.map +1 -0
- package/types/lib/parsers/safetensors.d.ts +22 -0
- package/types/lib/parsers/safetensors.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts +3 -2
- package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts +33 -0
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/index.d.ts +10 -1
- package/types/lib/validator/index.d.ts.map +1 -1
- package/types/lib/validator/reporters/console.d.ts.map +1 -1
- package/types/lib/validator/reporters/json.d.ts.map +1 -1
- package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
|
@@ -41,11 +41,24 @@ import {
|
|
|
41
41
|
mapConanPkgRefToPurlStringAndNameAndVersion,
|
|
42
42
|
nixGenericPurl,
|
|
43
43
|
purlFromUrlString,
|
|
44
|
+
tryBuildPurl,
|
|
44
45
|
} from "../inventory/purl.js";
|
|
45
46
|
import { getLicenses, spdxLicenses } from "../inventory/spdx.js";
|
|
47
|
+
import { cmakeFetchDependencies } from "../parsers/cmakeLists.js";
|
|
48
|
+
import { submodulePurlCoordinates } from "../parsers/gitmodules.js";
|
|
46
49
|
import { getDartMetadata } from "./ecosystems.js";
|
|
47
50
|
import { parsePkgJson } from "./parsers-js.js";
|
|
48
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
|
+
|
|
49
62
|
/**
|
|
50
63
|
* Method to parse pubspec.lock files.
|
|
51
64
|
*
|
|
@@ -1205,19 +1218,41 @@ export function parseFlakeNix(flakeNixFile) {
|
|
|
1205
1218
|
let match;
|
|
1206
1219
|
while ((match = inputsRegex.exec(flakeContent)) !== null) {
|
|
1207
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
|
+
}
|
|
1208
1232
|
|
|
1209
|
-
// 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.
|
|
1210
1235
|
const inputPatterns = [
|
|
1211
|
-
|
|
1212
|
-
|
|
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
|
+
},
|
|
1213
1245
|
];
|
|
1214
1246
|
|
|
1215
1247
|
const addedPackages = new Set();
|
|
1216
1248
|
|
|
1217
1249
|
for (const pattern of inputPatterns) {
|
|
1250
|
+
if (!inputBlock.includes(pattern.probe)) {
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1218
1253
|
let subMatch;
|
|
1219
|
-
pattern.lastIndex = 0;
|
|
1220
|
-
while ((subMatch = pattern.exec(inputBlock)) !== null) {
|
|
1254
|
+
pattern.regex.lastIndex = 0;
|
|
1255
|
+
while ((subMatch = pattern.regex.exec(inputBlock)) !== null) {
|
|
1221
1256
|
const name = subMatch[1];
|
|
1222
1257
|
const url = subMatch[2] || subMatch[3];
|
|
1223
1258
|
|
|
@@ -1779,7 +1814,7 @@ export function parseSwiftJsonTreeObject(
|
|
|
1779
1814
|
group: purl ? purl.namespace : "",
|
|
1780
1815
|
version: purl ? purl.version : jsonObject.version,
|
|
1781
1816
|
},
|
|
1782
|
-
purl ?
|
|
1817
|
+
purl ? purl.toString() : null,
|
|
1783
1818
|
jsonObject.name,
|
|
1784
1819
|
);
|
|
1785
1820
|
const properties = [];
|
|
@@ -1885,15 +1920,23 @@ export function parseSwiftResolved(resolvedFile) {
|
|
|
1885
1920
|
}
|
|
1886
1921
|
for (const adep of resolvedList) {
|
|
1887
1922
|
const locationOrUrl = adep.location || adep.repositoryURL;
|
|
1888
|
-
const version = adep.state
|
|
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.
|
|
1889
1927
|
const purl = purlFromUrlString("swift", locationOrUrl, version);
|
|
1890
|
-
const
|
|
1928
|
+
const name =
|
|
1929
|
+
purl?.name ||
|
|
1930
|
+
adep.identity ||
|
|
1931
|
+
adep.package ||
|
|
1932
|
+
(locationOrUrl ? basename(locationOrUrl).replace(/\.git$/, "") : "");
|
|
1933
|
+
if (!name) {
|
|
1934
|
+
continue;
|
|
1935
|
+
}
|
|
1891
1936
|
const rootPkg = {
|
|
1892
|
-
name
|
|
1893
|
-
group: purl
|
|
1894
|
-
version: purl.version,
|
|
1895
|
-
purl: purlString,
|
|
1896
|
-
"bom-ref": purlString,
|
|
1937
|
+
name,
|
|
1938
|
+
group: purl?.namespace || "",
|
|
1939
|
+
version: purl ? purl.version : version,
|
|
1897
1940
|
properties: [
|
|
1898
1941
|
{
|
|
1899
1942
|
name: "internal:SrcFile",
|
|
@@ -1902,7 +1945,7 @@ export function parseSwiftResolved(resolvedFile) {
|
|
|
1902
1945
|
],
|
|
1903
1946
|
evidence: {
|
|
1904
1947
|
identity: {
|
|
1905
|
-
field: "purl",
|
|
1948
|
+
field: purl ? "purl" : "name",
|
|
1906
1949
|
confidence: 1,
|
|
1907
1950
|
methods: [
|
|
1908
1951
|
{
|
|
@@ -1914,6 +1957,7 @@ export function parseSwiftResolved(resolvedFile) {
|
|
|
1914
1957
|
},
|
|
1915
1958
|
},
|
|
1916
1959
|
};
|
|
1960
|
+
applyPurl(rootPkg, purl ? purl.toString() : null);
|
|
1917
1961
|
if (locationOrUrl) {
|
|
1918
1962
|
rootPkg.repository = { url: locationOrUrl };
|
|
1919
1963
|
}
|
|
@@ -1926,6 +1970,281 @@ export function parseSwiftResolved(resolvedFile) {
|
|
|
1926
1970
|
return pkgList;
|
|
1927
1971
|
}
|
|
1928
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
|
+
|
|
1929
2248
|
/**
|
|
1930
2249
|
* Parse a CMake-generated dot/graphviz file and extract components and their dependency
|
|
1931
2250
|
* relationships.
|
|
@@ -2046,12 +2365,256 @@ export function parseCmakeDotFile(dotFile, pkgType, options = {}) {
|
|
|
2046
2365
|
};
|
|
2047
2366
|
}
|
|
2048
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
|
+
|
|
2049
2610
|
/**
|
|
2050
2611
|
* Parse a CMake-like build file (CMakeLists.txt, meson.build, etc.) and extract the
|
|
2051
2612
|
* parent component and list of dependency packages.
|
|
2052
2613
|
*
|
|
2053
2614
|
* Handles `set`, `project`, `find_package`, `find_library`, `find_dependency`,
|
|
2054
|
-
* `find_file
|
|
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`.
|
|
2055
2618
|
* Uses the MesonWrapDB to improve name resolution confidence.
|
|
2056
2619
|
*
|
|
2057
2620
|
* @param {string} cmakeListFile Path to the CMake-like build file
|
|
@@ -2060,13 +2623,30 @@ export function parseCmakeDotFile(dotFile, pkgType, options = {}) {
|
|
|
2060
2623
|
* @returns {{ parentComponent: Object, pkgList: Object[] }}
|
|
2061
2624
|
*/
|
|
2062
2625
|
export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
|
|
2063
|
-
|
|
2626
|
+
const rawData = readFileSync(cmakeListFile, { encoding: "utf-8" });
|
|
2627
|
+
let cmakeListData = rawData;
|
|
2064
2628
|
const pkgList = [];
|
|
2065
2629
|
const pkgAddedMap = {};
|
|
2066
2630
|
const versionSpecifiersMap = {};
|
|
2067
2631
|
const versionsMap = {};
|
|
2068
2632
|
let parentComponent = {};
|
|
2069
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
|
+
}
|
|
2070
2650
|
cmakeListData = cmakeListData
|
|
2071
2651
|
.replace(/^ {2}/g, "")
|
|
2072
2652
|
.replace(/\(\r\n/g, "(")
|
|
@@ -2081,138 +2661,129 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
|
|
|
2081
2661
|
const group = "";
|
|
2082
2662
|
const path = undefined;
|
|
2083
2663
|
const name_list = [];
|
|
2084
|
-
|
|
2085
|
-
|
|
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(" ");
|
|
2086
2672
|
if (tmpA && tmpA.length === 2) {
|
|
2087
2673
|
templateValues[tmpA[0]] = tmpA[1];
|
|
2088
2674
|
}
|
|
2089
|
-
} else if (
|
|
2090
|
-
|
|
2091
|
-
!Object.keys(parentComponent).length
|
|
2092
|
-
) {
|
|
2093
|
-
if (l.includes("${")) {
|
|
2675
|
+
} else if (command === "project" && !Object.keys(parentComponent).length) {
|
|
2676
|
+
if (commandArgs.includes("${")) {
|
|
2094
2677
|
for (const tmplKey of Object.keys(templateValues)) {
|
|
2095
|
-
|
|
2678
|
+
commandArgs = commandArgs.replace(
|
|
2679
|
+
`\${${tmplKey}}`,
|
|
2680
|
+
templateValues[tmplKey] || "",
|
|
2681
|
+
);
|
|
2096
2682
|
}
|
|
2097
2683
|
}
|
|
2098
|
-
const
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
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"],
|
|
2114
2725
|
);
|
|
2115
|
-
if (versionIndex > -1 && tmpB.length > versionIndex) {
|
|
2116
|
-
parentVersion = tmpB[versionIndex + 1];
|
|
2117
|
-
}
|
|
2118
|
-
// A `${VAR}` that no `set()` in this file defines stays literal. It is
|
|
2119
|
-
// not a version, and percent-encoding it into a purl publishes the
|
|
2120
|
-
// variable name as the version, so the field is left empty instead.
|
|
2121
|
-
// CMakeCache.txt carries the evaluated value when a build tree exists.
|
|
2122
|
-
if (parentVersion?.includes("$")) {
|
|
2123
|
-
parentVersion = undefined;
|
|
2124
|
-
}
|
|
2125
|
-
if (parentName?.length && !parentName.includes("$")) {
|
|
2126
|
-
parentComponent = {
|
|
2127
|
-
group: options.projectGroup || "",
|
|
2128
|
-
name: parentName,
|
|
2129
|
-
version: parentVersion || options.projectVersion || "",
|
|
2130
|
-
type: "application",
|
|
2131
|
-
};
|
|
2132
|
-
const safePath = path ? path.replace(/^\/+/, "") : null;
|
|
2133
|
-
parentComponent["purl"] = build({
|
|
2134
|
-
type: pkgType,
|
|
2135
|
-
namespace: parentComponent.group || null,
|
|
2136
|
-
name: parentComponent.name,
|
|
2137
|
-
version: parentComponent.version || null,
|
|
2138
|
-
subpath: safePath,
|
|
2139
|
-
});
|
|
2140
|
-
parentComponent["bom-ref"] = decodeURIComponent(
|
|
2141
|
-
parentComponent["purl"],
|
|
2142
|
-
);
|
|
2143
|
-
}
|
|
2144
2726
|
}
|
|
2145
|
-
} else if (
|
|
2146
|
-
let
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
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
|
+
}
|
|
2157
2762
|
}
|
|
2763
|
+
tmpB = names.filter((v) => !v.includes("$") && !v.startsWith("CMAKE_"));
|
|
2158
2764
|
}
|
|
2159
|
-
if (
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
!v.startsWith("CMAKE_") &&
|
|
2175
|
-
v.length,
|
|
2176
|
-
);
|
|
2177
|
-
// find_package(Catch2)
|
|
2178
|
-
// find_package(GTest REQUIRED)
|
|
2179
|
-
// find_package(Boost 1.79 COMPONENTS date_time)
|
|
2180
|
-
// find_library(PTHREADPOOL_LIB pthreadpool REQUIRED)
|
|
2181
|
-
// find_library and find_file name the cache variable that receives the
|
|
2182
|
-
// result in their first argument, so that argument is a destination
|
|
2183
|
-
// rather than a package. Every other command in this list opens with a
|
|
2184
|
-
// package name, including names such as `ZLIB` that read like a
|
|
2185
|
-
// variable.
|
|
2186
|
-
if (l.startsWith("find_library") || l.startsWith("find_file")) {
|
|
2187
|
-
tmpB = tmpB.slice(1);
|
|
2188
|
-
}
|
|
2189
|
-
if (tmpB.length) {
|
|
2190
|
-
const working_name = tmpB[0];
|
|
2191
|
-
name_list.push(working_name);
|
|
2192
|
-
if (l.startsWith("find_package") && tmpB.length > 1) {
|
|
2193
|
-
if (
|
|
2194
|
-
/^\d+/.test(tmpB[1]) &&
|
|
2195
|
-
!tmpB[1].includes("${") &&
|
|
2196
|
-
!tmpB[1].startsWith("@")
|
|
2197
|
-
) {
|
|
2198
|
-
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;
|
|
2199
2780
|
}
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
continue;
|
|
2204
|
-
}
|
|
2205
|
-
if (n.includes(_sep)) {
|
|
2206
|
-
if (
|
|
2207
|
-
n.includes(".so") ||
|
|
2208
|
-
n.includes(".a") ||
|
|
2209
|
-
n.includes(".dll")
|
|
2210
|
-
) {
|
|
2211
|
-
name_list.push(basename(n));
|
|
2212
|
-
}
|
|
2213
|
-
} else {
|
|
2214
|
-
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));
|
|
2215
2784
|
}
|
|
2785
|
+
} else {
|
|
2786
|
+
name_list.push(n);
|
|
2216
2787
|
}
|
|
2217
2788
|
}
|
|
2218
2789
|
}
|
|
@@ -2244,10 +2815,12 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
|
|
|
2244
2815
|
for (let n of name_list) {
|
|
2245
2816
|
const props = [];
|
|
2246
2817
|
let confidence = 0;
|
|
2818
|
+
let wrapReference;
|
|
2247
2819
|
if (
|
|
2248
2820
|
n &&
|
|
2249
2821
|
n.length > 1 &&
|
|
2250
2822
|
!pkgAddedMap[n] &&
|
|
2823
|
+
!fetchedNames.has(n.toLowerCase()) &&
|
|
2251
2824
|
!n.startsWith(_sep) &&
|
|
2252
2825
|
!n.startsWith("@")
|
|
2253
2826
|
) {
|
|
@@ -2262,14 +2835,18 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
|
|
|
2262
2835
|
) {
|
|
2263
2836
|
// Use the new name
|
|
2264
2837
|
n = wrapkey;
|
|
2265
|
-
|
|
2838
|
+
wrapReference = wrapDbReference(awrap);
|
|
2839
|
+
props.push({ name: "cdx:meson:wrapdb:wrap", value: wrapkey });
|
|
2840
|
+
if (awrap.analyzed_version) {
|
|
2266
2841
|
props.push({
|
|
2267
|
-
name:
|
|
2268
|
-
value:
|
|
2269
|
-
? awrap[eprop].join("|")
|
|
2270
|
-
: awrap[eprop],
|
|
2842
|
+
name: "cdx:meson:wrapdb:latestVersion",
|
|
2843
|
+
value: awrap.analyzed_version,
|
|
2271
2844
|
});
|
|
2272
2845
|
}
|
|
2846
|
+
props.push({
|
|
2847
|
+
name: "internal:PkgProvides",
|
|
2848
|
+
value: awrap.PkgProvides.join("|"),
|
|
2849
|
+
});
|
|
2273
2850
|
// Our confidence has improved from 0 since there is a matching wrap so we know the correct name
|
|
2274
2851
|
// and url. We lack the version details.
|
|
2275
2852
|
confidence = 0.5;
|
|
@@ -2320,6 +2897,9 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
|
|
|
2320
2897
|
},
|
|
2321
2898
|
properties: props,
|
|
2322
2899
|
};
|
|
2900
|
+
if (wrapReference) {
|
|
2901
|
+
apkg.externalReferences = [wrapReference];
|
|
2902
|
+
}
|
|
2323
2903
|
apkg["bom-ref"] = decodeURIComponent(apkg["purl"]);
|
|
2324
2904
|
pkgList.push(apkg);
|
|
2325
2905
|
pkgAddedMap[n] = true;
|
|
@@ -2349,41 +2929,48 @@ export function parseCUsageSlice(sliceData) {
|
|
|
2349
2929
|
return undefined;
|
|
2350
2930
|
}
|
|
2351
2931
|
const usageData = {};
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
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");
|
|
2355
2938
|
if (
|
|
2356
|
-
(!
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2939
|
+
(!fileName && !isInclude) ||
|
|
2940
|
+
fileName.startsWith("<includes") ||
|
|
2941
|
+
fileName.startsWith("<global") ||
|
|
2942
|
+
fileName.includes("__")
|
|
2360
2943
|
) {
|
|
2361
2944
|
continue;
|
|
2362
2945
|
}
|
|
2363
|
-
const
|
|
2364
|
-
const allLines = usageData[slFileName] || new Set();
|
|
2946
|
+
const allLines = (fileName && usageData[fileName]) || new Set();
|
|
2365
2947
|
if (slice.fullName && slice.fullName.length > 3) {
|
|
2366
|
-
if (
|
|
2367
|
-
|
|
2948
|
+
if (isInclude) {
|
|
2949
|
+
// an included file keeps the symbols already recorded for it
|
|
2950
|
+
usageData[slice.fullName] = usageData[slice.fullName] || new Set();
|
|
2368
2951
|
} else {
|
|
2369
2952
|
allLines.add(slice.fullName);
|
|
2370
2953
|
}
|
|
2371
2954
|
}
|
|
2372
|
-
for (const ausage of slice.usages) {
|
|
2373
|
-
|
|
2374
|
-
|
|
2955
|
+
for (const ausage of slice.usages || []) {
|
|
2956
|
+
const calls = (ausage?.invokedCalls || []).concat(
|
|
2957
|
+
ausage?.argToCalls || [],
|
|
2958
|
+
);
|
|
2375
2959
|
for (const acall of calls) {
|
|
2376
|
-
|
|
2377
|
-
|
|
2960
|
+
const resolvedMethod = acall?.resolvedMethod;
|
|
2961
|
+
if (resolvedMethod && !resolvedMethod.includes("->")) {
|
|
2962
|
+
allLines.add(resolvedMethod);
|
|
2378
2963
|
}
|
|
2379
2964
|
}
|
|
2380
2965
|
}
|
|
2381
|
-
if (
|
|
2382
|
-
usageData[
|
|
2966
|
+
if (fileName && allLines.size) {
|
|
2967
|
+
usageData[fileName] = allLines;
|
|
2383
2968
|
}
|
|
2969
|
+
} catch (err) {
|
|
2970
|
+
thoughtLog("Skipping a C usage slice that could not be read.", {
|
|
2971
|
+
error: err?.message,
|
|
2972
|
+
});
|
|
2384
2973
|
}
|
|
2385
|
-
} catch (_err) {
|
|
2386
|
-
// ignore
|
|
2387
2974
|
}
|
|
2388
2975
|
return usageData;
|
|
2389
2976
|
}
|
|
@@ -3203,3 +3790,174 @@ async function fullScanCocoaPod(dependency, component, options) {
|
|
|
3203
3790
|
component.externalReferences = externalRefs;
|
|
3204
3791
|
}
|
|
3205
3792
|
}
|
|
3793
|
+
|
|
3794
|
+
/**
|
|
3795
|
+
* Parse an xmake `xmake-requires.lock` file.
|
|
3796
|
+
*
|
|
3797
|
+
* The lock is a Lua table, written when the `package.requires_lock` policy is
|
|
3798
|
+
* enabled. Its top level holds a `__meta__` entry plus one section per build
|
|
3799
|
+
* configuration, keyed `platform|arch`; each section maps a requirement key
|
|
3800
|
+
* (`"zlib#31fecfc4"`, or `"sol2 v3.2.1#72267bd5"` when the requirement
|
|
3801
|
+
* carries a version) to a table with the resolved `version` and the
|
|
3802
|
+
* `repo` revision the package definition came from.
|
|
3803
|
+
*
|
|
3804
|
+
* The same package appears once per configuration, so entries are collapsed
|
|
3805
|
+
* by name and version, and the configurations a package was resolved for are
|
|
3806
|
+
* kept as a property. A requirement key without a `version` field names a
|
|
3807
|
+
* system package xmake did not resolve; it is inventoried without a version.
|
|
3808
|
+
*
|
|
3809
|
+
* No `xmake` purl type is registered, so packages are identified as generic
|
|
3810
|
+
* carrying a `cdx:purl:proposedType=xmake` property, following the
|
|
3811
|
+
* convention used for nix and zig.
|
|
3812
|
+
*
|
|
3813
|
+
* @param {string} lockFile Path to `xmake-requires.lock`
|
|
3814
|
+
* @returns {Object[]} Package records for the pinned requirements
|
|
3815
|
+
*/
|
|
3816
|
+
export function parseXmakeRequiresLock(lockFile) {
|
|
3817
|
+
let text;
|
|
3818
|
+
try {
|
|
3819
|
+
text = readFileSync(lockFile, "utf-8");
|
|
3820
|
+
} catch (error) {
|
|
3821
|
+
console.warn(`Failed to read ${lockFile}: ${error.message}`);
|
|
3822
|
+
return [];
|
|
3823
|
+
}
|
|
3824
|
+
const byKey = new Map();
|
|
3825
|
+
// The outermost `{` is the file's own table, so a configuration section
|
|
3826
|
+
// opens at depth 1 and a requirement inside it at depth 2. Each is dropped
|
|
3827
|
+
// again once the brace depth falls back to where it opened.
|
|
3828
|
+
const CONFIGURATION_DEPTH = 1;
|
|
3829
|
+
const REQUIREMENT_DEPTH = 2;
|
|
3830
|
+
let configuration;
|
|
3831
|
+
let requirement;
|
|
3832
|
+
let depth = 0;
|
|
3833
|
+
for (const rawLine of text.split("\n")) {
|
|
3834
|
+
const line = rawLine.trim();
|
|
3835
|
+
if (!line || line.startsWith("--")) {
|
|
3836
|
+
continue;
|
|
3837
|
+
}
|
|
3838
|
+
// `["macosx|arm64"] = {` opens a configuration; `["zlib#hash"] = {` opens
|
|
3839
|
+
// a requirement inside one.
|
|
3840
|
+
const opener = line.match(/^\[\s*"([^"]+)"\s*\]\s*=\s*\{/u);
|
|
3841
|
+
if (opener && depth === CONFIGURATION_DEPTH) {
|
|
3842
|
+
// `__meta__` is normally written unquoted, but a lock that quotes it
|
|
3843
|
+
// must not become a configuration section either.
|
|
3844
|
+
configuration = opener[1] === "__meta__" ? undefined : opener[1];
|
|
3845
|
+
} else if (opener && depth === REQUIREMENT_DEPTH && configuration) {
|
|
3846
|
+
requirement = xmakeRequirementName(opener[1]);
|
|
3847
|
+
const key = `${configuration}|${requirement}`;
|
|
3848
|
+
if (requirement && !byKey.has(key)) {
|
|
3849
|
+
byKey.set(key, { name: requirement, configuration });
|
|
3850
|
+
}
|
|
3851
|
+
} else if (!opener && configuration && requirement) {
|
|
3852
|
+
const version = line.match(/^version\s*=\s*"([^"]*)"/u);
|
|
3853
|
+
if (version) {
|
|
3854
|
+
const record = byKey.get(`${configuration}|${requirement}`);
|
|
3855
|
+
if (record) {
|
|
3856
|
+
record.version = version[1];
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
depth = Math.max(0, depth + countXmakeBraces(line));
|
|
3861
|
+
if (depth <= REQUIREMENT_DEPTH) {
|
|
3862
|
+
requirement = undefined;
|
|
3863
|
+
}
|
|
3864
|
+
if (depth <= CONFIGURATION_DEPTH) {
|
|
3865
|
+
configuration = undefined;
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3868
|
+
|
|
3869
|
+
// Collapse the per-configuration entries into one component per resolved
|
|
3870
|
+
// coordinate, listing the configurations it was resolved for.
|
|
3871
|
+
const merged = new Map();
|
|
3872
|
+
for (const record of byKey.values()) {
|
|
3873
|
+
const key = `${record.name}@${record.version || ""}`;
|
|
3874
|
+
const existing = merged.get(key);
|
|
3875
|
+
if (existing) {
|
|
3876
|
+
existing.configurations.add(record.configuration);
|
|
3877
|
+
continue;
|
|
3878
|
+
}
|
|
3879
|
+
merged.set(key, {
|
|
3880
|
+
name: record.name,
|
|
3881
|
+
version: record.version,
|
|
3882
|
+
configurations: new Set([record.configuration]),
|
|
3883
|
+
});
|
|
3884
|
+
}
|
|
3885
|
+
|
|
3886
|
+
const pkgList = [];
|
|
3887
|
+
for (const record of merged.values()) {
|
|
3888
|
+
// A malformed coordinate in an untrusted lock costs one entry, not the
|
|
3889
|
+
// whole scan.
|
|
3890
|
+
const purl = tryBuildPurl({
|
|
3891
|
+
type: "generic",
|
|
3892
|
+
namespace: "xmake",
|
|
3893
|
+
name: record.name,
|
|
3894
|
+
version: record.version || null,
|
|
3895
|
+
});
|
|
3896
|
+
if (!purl) {
|
|
3897
|
+
continue;
|
|
3898
|
+
}
|
|
3899
|
+
pkgList.push({
|
|
3900
|
+
name: record.name,
|
|
3901
|
+
...(record.version ? { version: record.version } : {}),
|
|
3902
|
+
type: "library",
|
|
3903
|
+
scope: "required",
|
|
3904
|
+
purl,
|
|
3905
|
+
"bom-ref": decodeURIComponent(purl),
|
|
3906
|
+
properties: [
|
|
3907
|
+
{ name: "internal:SrcFile", value: lockFile },
|
|
3908
|
+
{ name: "cdx:purl:proposedType", value: "xmake" },
|
|
3909
|
+
{
|
|
3910
|
+
name: "cdx:xmake:configurations",
|
|
3911
|
+
value: [...record.configurations].sort().join(", "),
|
|
3912
|
+
},
|
|
3913
|
+
],
|
|
3914
|
+
evidence: {
|
|
3915
|
+
identity: {
|
|
3916
|
+
field: "purl",
|
|
3917
|
+
confidence: 0.5,
|
|
3918
|
+
methods: [
|
|
3919
|
+
{
|
|
3920
|
+
technique: "source-code-analysis",
|
|
3921
|
+
confidence: 0.5,
|
|
3922
|
+
value: `Filename ${lockFile}`,
|
|
3923
|
+
},
|
|
3924
|
+
],
|
|
3925
|
+
},
|
|
3926
|
+
},
|
|
3927
|
+
});
|
|
3928
|
+
}
|
|
3929
|
+
return pkgList;
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3932
|
+
/**
|
|
3933
|
+
* Reduce an xmake requirement key to the package name.
|
|
3934
|
+
*
|
|
3935
|
+
* Keys look like `zlib#31fecfc4` or `sol2 v3.2.1#72267bd5`: the trailing
|
|
3936
|
+
* `#<hash>` identifies the resolved configuration and the optional second
|
|
3937
|
+
* token is the requested version range, neither of which is the name.
|
|
3938
|
+
*
|
|
3939
|
+
* @param {string} key Requirement key as written in the lock
|
|
3940
|
+
* @returns {string|undefined} Package name
|
|
3941
|
+
*/
|
|
3942
|
+
function xmakeRequirementName(key) {
|
|
3943
|
+
const name = key.split("#")[0].trim().split(/\s+/u)[0];
|
|
3944
|
+
return name || undefined;
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3947
|
+
/**
|
|
3948
|
+
* Net brace change contributed by one line of a Lua table.
|
|
3949
|
+
*
|
|
3950
|
+
* @param {string} line Line to scan
|
|
3951
|
+
* @returns {number} Opens minus closes
|
|
3952
|
+
*/
|
|
3953
|
+
function countXmakeBraces(line) {
|
|
3954
|
+
let delta = 0;
|
|
3955
|
+
for (const char of line) {
|
|
3956
|
+
if (char === "{") {
|
|
3957
|
+
delta += 1;
|
|
3958
|
+
} else if (char === "}") {
|
|
3959
|
+
delta -= 1;
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
return delta;
|
|
3963
|
+
}
|