@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
package/lib/evinser/evinser.js
CHANGED
|
@@ -25,16 +25,30 @@ import {
|
|
|
25
25
|
getGradleCommand,
|
|
26
26
|
getMavenCommand,
|
|
27
27
|
} from "../ecosystems/gradleutils.js";
|
|
28
|
+
import {
|
|
29
|
+
analyzeKosiProject,
|
|
30
|
+
collectKosiApiEndpoints,
|
|
31
|
+
collectKosiEvidence,
|
|
32
|
+
collectKosiServices,
|
|
33
|
+
isKosiKotlinLanguage,
|
|
34
|
+
kosiWorkspaceComponent,
|
|
35
|
+
mergeKosiEvidence,
|
|
36
|
+
} from "../ecosystems/kosi.js";
|
|
28
37
|
import {
|
|
29
38
|
analyzeRusiProject,
|
|
30
39
|
collectRusiEvidence,
|
|
31
40
|
isRusiRustLanguage,
|
|
32
41
|
} from "../ecosystems/rusi.js";
|
|
33
42
|
import {
|
|
43
|
+
atomCompileCommandsArgs,
|
|
34
44
|
buildAtomCommandEnv,
|
|
35
45
|
executeAtom,
|
|
36
46
|
filterAtomSlicesByExcludePatterns,
|
|
47
|
+
readReachablesSlices,
|
|
48
|
+
readSlicesFile,
|
|
49
|
+
removeReachablesChunkFiles,
|
|
37
50
|
} from "../inventory/atomUtils.js";
|
|
51
|
+
import { isCycloneDxComponentTypeEnabled } from "../inventory/bomUtils.js";
|
|
38
52
|
import {
|
|
39
53
|
collectDosaiCryptoComponents,
|
|
40
54
|
findCryptoAlgos,
|
|
@@ -42,6 +56,7 @@ import {
|
|
|
42
56
|
import { collectMvnDependencies } from "../inventory/deps.js";
|
|
43
57
|
import { mergeServices } from "../inventory/depsUtils.js";
|
|
44
58
|
import {
|
|
59
|
+
applyDosaiReachabilityEvidence,
|
|
45
60
|
collectDosaiAiComponents,
|
|
46
61
|
collectDosaiDataFlowFrames,
|
|
47
62
|
collectDosaiPurlEvidence,
|
|
@@ -52,9 +67,12 @@ import {
|
|
|
52
67
|
isDosaiDotnetLanguage,
|
|
53
68
|
normalizeDosaiServiceMap,
|
|
54
69
|
persistDosaiSemanticsReport,
|
|
70
|
+
readDosaiDataFlowReport,
|
|
71
|
+
readDosaiMethodsReport,
|
|
55
72
|
} from "../inventory/dosai.js";
|
|
56
73
|
import { parseOccurrenceEvidenceLocation } from "../inventory/evidenceUtils.js";
|
|
57
74
|
import { executeOsQuery } from "../managers/binary.js";
|
|
75
|
+
import { MAX_JSON_TEXT_BYTES, readJsonFile } from "../parsers/largeJson.js";
|
|
58
76
|
import { postProcess } from "../stages/postgen/postgen.js";
|
|
59
77
|
import { createOrLoad } from "./db.js";
|
|
60
78
|
import { findPurlLocations } from "./scalasem.js";
|
|
@@ -86,10 +104,88 @@ function appendComponentProperties(component, properties = []) {
|
|
|
86
104
|
}
|
|
87
105
|
}
|
|
88
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Merges one analyzer run's metadata properties into the BOM's metadata
|
|
109
|
+
* component. Analyzer metadata describes a single run, so when the input BOM
|
|
110
|
+
* already carries facts from an earlier run (cdxgen --profile research, then
|
|
111
|
+
* evinse), everything under the analyzer's own `cdx:<analyzer>:` prefix is
|
|
112
|
+
* replaced. Replacing by exact name would keep facts the new run no longer
|
|
113
|
+
* emits, such as a cdx:rusi:requestedBackend left from a fallback run.
|
|
114
|
+
*
|
|
115
|
+
* @param {Object} component Metadata component to update in place
|
|
116
|
+
* @param {Object[]} metadataProperties Properties from the analyzer run
|
|
117
|
+
*/
|
|
118
|
+
export function mergeAnalyzerMetadataProperties(
|
|
119
|
+
component,
|
|
120
|
+
metadataProperties = [],
|
|
121
|
+
) {
|
|
122
|
+
if (!component || !metadataProperties.length) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (component.properties?.length) {
|
|
126
|
+
const analyzerPrefixes = [
|
|
127
|
+
...new Set(
|
|
128
|
+
metadataProperties
|
|
129
|
+
.map((p) => /^cdx:[^:]+:/.exec(p.name)?.[0])
|
|
130
|
+
.filter(Boolean),
|
|
131
|
+
),
|
|
132
|
+
];
|
|
133
|
+
component.properties = component.properties.filter(
|
|
134
|
+
(p) => !analyzerPrefixes.some((prefix) => p.name?.startsWith(prefix)),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
appendComponentProperties(component, metadataProperties);
|
|
138
|
+
}
|
|
139
|
+
|
|
89
140
|
function filterAtomSliceData(sliceData, options = {}) {
|
|
90
141
|
return filterAtomSlicesByExcludePatterns(sliceData, options.exclude);
|
|
91
142
|
}
|
|
92
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Build the options for the evinse phase (prepareDB, analyzeProject, and
|
|
146
|
+
* createEvinseFile) from the cdxgen CLI options.
|
|
147
|
+
*
|
|
148
|
+
* This object used to be assembled inline in bin/cdxgen.js, which dropped
|
|
149
|
+
* fields the analyzers read - most notably `exclude`, so every dosai and atom
|
|
150
|
+
* run triggered by `--evidence` ignored the user's `--exclude` patterns. Keep
|
|
151
|
+
* the wiring in one place so the forwarded fields stay testable.
|
|
152
|
+
*
|
|
153
|
+
* @param {Object} options CLI options
|
|
154
|
+
* @param {Object} args Parsed CLI arguments; `args._[0]` is the source directory
|
|
155
|
+
* @param {string} input Path of the BOM file the evinse phase enriches
|
|
156
|
+
* @returns {Object} Options for prepareDB, analyzeProject, and createEvinseFile
|
|
157
|
+
*/
|
|
158
|
+
export function buildEvinseOptions(options, args, input) {
|
|
159
|
+
const sourceDir = args._?.[0] || ".";
|
|
160
|
+
return {
|
|
161
|
+
_: args._,
|
|
162
|
+
input,
|
|
163
|
+
output: options.evinseOutput,
|
|
164
|
+
language: options.projectType,
|
|
165
|
+
skipMavenCollector: false,
|
|
166
|
+
force: false,
|
|
167
|
+
withReachables: options.deep,
|
|
168
|
+
usagesSlicesFile: options.usagesSlicesFile,
|
|
169
|
+
dataFlowSlicesFile: options.dataFlowSlicesFile,
|
|
170
|
+
reachablesSlicesFile: options.reachablesSlicesFile,
|
|
171
|
+
semanticsSlicesFile: options.semanticsSlicesFile,
|
|
172
|
+
openapiSpecFile: options.openapiSpecFile,
|
|
173
|
+
componentType: options.componentType,
|
|
174
|
+
includeCrypto:
|
|
175
|
+
options.includeCrypto &&
|
|
176
|
+
isCycloneDxComponentTypeEnabled("cryptographic-asset", options),
|
|
177
|
+
specVersion: options.specVersion,
|
|
178
|
+
profile: options.profile,
|
|
179
|
+
jsonPretty: options.jsonPretty,
|
|
180
|
+
exclude: options.exclude,
|
|
181
|
+
// Absolute path of the deps slice createCsharpBom writes for --deep runs,
|
|
182
|
+
// so analyzeProject can reuse it instead of rerunning dosai methods.
|
|
183
|
+
depsSlicesFile: options.depsSlicesFile
|
|
184
|
+
? resolve(sourceDir, options.depsSlicesFile)
|
|
185
|
+
: undefined,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
93
189
|
/**
|
|
94
190
|
* Function to create the db for the libraries referred in the sbom.
|
|
95
191
|
*
|
|
@@ -434,6 +530,11 @@ export async function createSlice(
|
|
|
434
530
|
`Creating ${sliceType} slice for ${resolve(filePath)}. Please wait ...`,
|
|
435
531
|
);
|
|
436
532
|
const atomFile = join(sliceOutputDir, `${language}-app.atom`);
|
|
533
|
+
if (sliceType === "reachables") {
|
|
534
|
+
// atom writes reachables in numbered chunks next to the slices file; one
|
|
535
|
+
// left by an earlier run must not be read back as part of this one.
|
|
536
|
+
removeReachablesChunkFiles(slicesFile);
|
|
537
|
+
}
|
|
437
538
|
let args = [sliceType];
|
|
438
539
|
// Support for crypto slices aka CBOM
|
|
439
540
|
if (sliceType === "reachables" && options.includeCrypto) {
|
|
@@ -478,6 +579,7 @@ export async function createSlice(
|
|
|
478
579
|
args.push("--slice-depth");
|
|
479
580
|
args.push(readEnvironmentVariable("ATOM_SLICE_DEPTH"));
|
|
480
581
|
}
|
|
582
|
+
args.push(...atomCompileCommandsArgs(filePath, language, options));
|
|
481
583
|
args.push(resolve(filePath));
|
|
482
584
|
const atomExcludeEnv = buildAtomCommandEnv(options, language);
|
|
483
585
|
// Execute atom
|
|
@@ -588,8 +690,9 @@ export function purlToLanguage(purl, filePath) {
|
|
|
588
690
|
* Seed purl-location and import maps from SBOM component evidence.
|
|
589
691
|
*
|
|
590
692
|
* Walks the supplied components reading `internal:ImportedModules` (or
|
|
591
|
-
* `internal:Namespaces` for php/ruby
|
|
592
|
-
*
|
|
693
|
+
* `internal:Namespaces` for php/ruby, plus the `internal:ImportedSymbols` the
|
|
694
|
+
* C/C++ collector records) and `evidence.occurrences` properties to construct
|
|
695
|
+
* lookup maps used during slice analysis.
|
|
593
696
|
*
|
|
594
697
|
* @param {Object[]} components CycloneDX components from the input SBOM
|
|
595
698
|
* @param {string} language Application language used to select the import property name
|
|
@@ -614,10 +717,27 @@ export function initFromSbom(components, language) {
|
|
|
614
717
|
.forEach((v) => {
|
|
615
718
|
purlImportsMap[comp.purl] = (v.value || "").split(",");
|
|
616
719
|
});
|
|
720
|
+
// A C/C++ component records the symbols the project uses from it, which
|
|
721
|
+
// are what C usages are matched against. The list is pipe-separated:
|
|
722
|
+
// C++ symbols may contain commas.
|
|
723
|
+
if (["c", "cpp", "c++"].includes(language)) {
|
|
724
|
+
(comp.properties || [])
|
|
725
|
+
.filter((v) => v.name === "internal:ImportedSymbols")
|
|
726
|
+
.forEach((v) => {
|
|
727
|
+
purlImportsMap[comp.purl] = (
|
|
728
|
+
purlImportsMap[comp.purl] || []
|
|
729
|
+
).concat((v.value || "").split("|").filter(Boolean));
|
|
730
|
+
});
|
|
731
|
+
}
|
|
617
732
|
}
|
|
618
733
|
if (comp.evidence.occurrences) {
|
|
734
|
+
// Keep the `location#line` shape so a re-run over a cdxgen BOM does not
|
|
735
|
+
// degrade seeded occurrences to file-only entries and duplicate the
|
|
736
|
+
// richer locations the fresh slice merge adds to the same set.
|
|
619
737
|
purlLocationMap[comp.purl] = new Set(
|
|
620
|
-
comp.evidence.occurrences.map((v) =>
|
|
738
|
+
comp.evidence.occurrences.map((v) =>
|
|
739
|
+
v.line ? `${v.location}#${v.line}` : v.location,
|
|
740
|
+
),
|
|
621
741
|
);
|
|
622
742
|
}
|
|
623
743
|
}
|
|
@@ -639,6 +759,65 @@ function usableSlicesFile(slicesFile) {
|
|
|
639
759
|
return fileSizeInBytes > 1024;
|
|
640
760
|
}
|
|
641
761
|
|
|
762
|
+
/**
|
|
763
|
+
* Load a semantics slices file for reuse when it belongs to this project.
|
|
764
|
+
*
|
|
765
|
+
* `--semantics-slices-file` defaults to `semantics.slices.json` in the working
|
|
766
|
+
* directory, which is shared by every project scanned from there and also
|
|
767
|
+
* receives the reports of other analyzers (dosai, rusi, golem). A file is
|
|
768
|
+
* only reused when its shape matches the language. Swift slices must also
|
|
769
|
+
* describe the same project directory and must not predate the input SBOM:
|
|
770
|
+
* `cdxgen --evidence` writes a fresh SBOM before evinse runs, so an older
|
|
771
|
+
* slice was made from an earlier state of the sources and is regenerated.
|
|
772
|
+
* Scala slices cannot be regenerated by evinse and are reused whenever their
|
|
773
|
+
* shape matches.
|
|
774
|
+
*
|
|
775
|
+
* @param {string} language Project language
|
|
776
|
+
* @param {string} slicesFile Candidate semantics slices file
|
|
777
|
+
* @param {string} bomFile Input SBOM
|
|
778
|
+
* @param {string} dirPath Project directory
|
|
779
|
+
* @returns {undefined|Object} Parsed slice when it can be reused
|
|
780
|
+
*/
|
|
781
|
+
export function loadReusableSemanticsSlice(
|
|
782
|
+
language,
|
|
783
|
+
slicesFile,
|
|
784
|
+
bomFile,
|
|
785
|
+
dirPath,
|
|
786
|
+
) {
|
|
787
|
+
if (!usableSlicesFile(slicesFile)) {
|
|
788
|
+
return undefined;
|
|
789
|
+
}
|
|
790
|
+
let parsed;
|
|
791
|
+
try {
|
|
792
|
+
parsed = readJsonFile(slicesFile);
|
|
793
|
+
} catch (_e) {
|
|
794
|
+
return undefined;
|
|
795
|
+
}
|
|
796
|
+
if (!parsed || typeof parsed !== "object") {
|
|
797
|
+
return undefined;
|
|
798
|
+
}
|
|
799
|
+
if (!PROJECT_TYPE_ALIASES.swift.includes(language)) {
|
|
800
|
+
// Scala semantics slices are keyed by .scala source file names
|
|
801
|
+
return Object.keys(parsed).some((key) => key.endsWith(".scala"))
|
|
802
|
+
? parsed
|
|
803
|
+
: undefined;
|
|
804
|
+
}
|
|
805
|
+
if (!(parsed.fileIndexes || parsed.buildSymbols || parsed.moduleInfos)) {
|
|
806
|
+
return undefined;
|
|
807
|
+
}
|
|
808
|
+
if (parsed.projectPath && resolve(parsed.projectPath) !== resolve(dirPath)) {
|
|
809
|
+
return undefined;
|
|
810
|
+
}
|
|
811
|
+
try {
|
|
812
|
+
if (fs.statSync(slicesFile).mtimeMs < fs.statSync(bomFile).mtimeMs) {
|
|
813
|
+
return undefined;
|
|
814
|
+
}
|
|
815
|
+
} catch (_e) {
|
|
816
|
+
// Without timestamps the shape and project checks decide
|
|
817
|
+
}
|
|
818
|
+
return parsed;
|
|
819
|
+
}
|
|
820
|
+
|
|
642
821
|
/**
|
|
643
822
|
* Function to analyze the project
|
|
644
823
|
*
|
|
@@ -724,6 +903,84 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
724
903
|
};
|
|
725
904
|
}
|
|
726
905
|
|
|
906
|
+
if (isKosiKotlinLanguage(language)) {
|
|
907
|
+
const kosiReports = analyzeKosiProject(dirPath, options);
|
|
908
|
+
if (kosiReports?.report && Object.keys(kosiReports.report).length) {
|
|
909
|
+
// The workspace component: cdxgen's Kotlin BOM carries no project
|
|
910
|
+
// component, so every workspace-anchored slice (reachability, the
|
|
911
|
+
// app's own flows) would have nothing to attach to. createEvinseFile
|
|
912
|
+
// re-reads the BOM from disk, so the candidate rides the artefacts
|
|
913
|
+
// instead of a mutation here — applied when the BOM lacks a project
|
|
914
|
+
// component OR carries one with no purl: every kosi evidence join is
|
|
915
|
+
// keyed by purl, so a purl-less component anchors nothing and is, for
|
|
916
|
+
// this purpose, no anchor at all (found on the sample app: Gradle
|
|
917
|
+
// identity with empty group and version "latest" emits no purl, and
|
|
918
|
+
// the reachableFromRoots/cryptoFlow evidence silently attached to
|
|
919
|
+
// nothing while the dependency-joined kinds landed).
|
|
920
|
+
const metadataComponentHasPurl = Boolean(
|
|
921
|
+
bomJson.metadata?.component?.purl,
|
|
922
|
+
);
|
|
923
|
+
let workspaceComponent;
|
|
924
|
+
if (!metadataComponentHasPurl) {
|
|
925
|
+
// kosi's modules[] holds the project's OWN Gradle/Maven modules
|
|
926
|
+
// (dependencies never appear there), so the project component is
|
|
927
|
+
// its root: the shortest modulePath, ties broken by name so the
|
|
928
|
+
// choice is deterministic. `workspaceMember` is not the selector —
|
|
929
|
+
// it is a String naming the owning workspace member in kosi's
|
|
930
|
+
// schema, empty for a single-module build and then omitted from the
|
|
931
|
+
// JSON entirely, so testing it against `undefined` picked nothing
|
|
932
|
+
// on exactly the single-module projects this fallback exists for.
|
|
933
|
+
const workspaceModule = (kosiReports.report.modules || [])
|
|
934
|
+
.filter((candidate) => candidate?.purl)
|
|
935
|
+
.sort(
|
|
936
|
+
(a, b) =>
|
|
937
|
+
(a.modulePath || "").length - (b.modulePath || "").length ||
|
|
938
|
+
(a.name || "").localeCompare(b.name || ""),
|
|
939
|
+
)[0];
|
|
940
|
+
if (workspaceModule?.purl) {
|
|
941
|
+
workspaceComponent = kosiWorkspaceComponent(workspaceModule);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
const analysisComponents = [
|
|
945
|
+
bomJson.metadata?.component,
|
|
946
|
+
workspaceComponent,
|
|
947
|
+
...components,
|
|
948
|
+
].filter(Boolean);
|
|
949
|
+
const kosiEvidence = collectKosiEvidence(
|
|
950
|
+
kosiReports.report,
|
|
951
|
+
analysisComponents,
|
|
952
|
+
);
|
|
953
|
+
mergeKosiEvidence(
|
|
954
|
+
kosiEvidence,
|
|
955
|
+
collectKosiEvidence(
|
|
956
|
+
kosiReports.reachableReport || {},
|
|
957
|
+
analysisComponents,
|
|
958
|
+
),
|
|
959
|
+
);
|
|
960
|
+
servicesMap = collectKosiServices(kosiReports.report, servicesMap);
|
|
961
|
+
// The INBOUND surface: the routes the app serves, with the
|
|
962
|
+
// authentication kosi found declared on them.
|
|
963
|
+
servicesMap = collectKosiApiEndpoints(kosiReports.report, servicesMap);
|
|
964
|
+
return {
|
|
965
|
+
purlLocationMap: kosiEvidence.purlLocationMap,
|
|
966
|
+
dataFlowFrames: kosiEvidence.dataFlowFrames,
|
|
967
|
+
componentPropertiesMap: kosiEvidence.componentPropertiesMap,
|
|
968
|
+
metadataProperties: kosiEvidence.metadataProperties,
|
|
969
|
+
cryptoComponents: kosiEvidence.cryptoComponents,
|
|
970
|
+
cryptoGeneratePurls: kosiEvidence.cryptoGeneratePurls,
|
|
971
|
+
servicesMap,
|
|
972
|
+
userDefinedTypesMap,
|
|
973
|
+
workspaceComponent,
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
return {
|
|
977
|
+
purlLocationMap,
|
|
978
|
+
servicesMap,
|
|
979
|
+
dataFlowFrames,
|
|
980
|
+
userDefinedTypesMap,
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
|
|
727
984
|
if (isDosaiDotnetLanguage(language)) {
|
|
728
985
|
if (options.profile === "research") {
|
|
729
986
|
options.withDataFlow = true;
|
|
@@ -733,19 +990,32 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
733
990
|
options.usagesSlicesFile &&
|
|
734
991
|
usableSlicesFile(options.usagesSlicesFile)
|
|
735
992
|
) {
|
|
736
|
-
usageSlice =
|
|
737
|
-
fs.readFileSync(options.usagesSlicesFile, "utf-8"),
|
|
738
|
-
);
|
|
993
|
+
usageSlice = readDosaiMethodsReport(options.usagesSlicesFile);
|
|
739
994
|
usagesSlicesFile = options.usagesSlicesFile;
|
|
995
|
+
} else if (usableSlicesFile(options.depsSlicesFile)) {
|
|
996
|
+
// createCsharpBom already ran dosai methods on this tree for --deep runs
|
|
997
|
+
// and wrote the deps slice. For dosai, that output IS the usages slice,
|
|
998
|
+
// so reuse it instead of invoking dosai a second time.
|
|
999
|
+
if (DEBUG_MODE) {
|
|
1000
|
+
console.log(
|
|
1001
|
+
`Reusing the dosai deps slices file "${options.depsSlicesFile}" for the usages analysis.`,
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
usageSlice = readDosaiMethodsReport(options.depsSlicesFile);
|
|
1005
|
+
usagesSlicesFile = options.depsSlicesFile;
|
|
740
1006
|
} else {
|
|
741
1007
|
retMap = await createSlice(language, dirPath, "usages", options);
|
|
742
1008
|
if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
|
|
743
|
-
usageSlice =
|
|
1009
|
+
usageSlice = readDosaiMethodsReport(retMap.slicesFile);
|
|
744
1010
|
usagesSlicesFile = retMap.slicesFile;
|
|
745
1011
|
}
|
|
746
1012
|
}
|
|
747
1013
|
if (usageSlice && Object.keys(usageSlice).length) {
|
|
748
1014
|
const dosaiEvidence = collectDosaiPurlEvidence(usageSlice, components);
|
|
1015
|
+
// Confidence/evidence facts ride along as properties so an unbuilt tree (Low
|
|
1016
|
+
// confidence, unresolved evidence) stays distinguishable from an only-imported
|
|
1017
|
+
// package in the emitted BOM.
|
|
1018
|
+
applyDosaiReachabilityEvidence(usageSlice, components);
|
|
749
1019
|
for (const [purl, locations] of Object.entries(
|
|
750
1020
|
dosaiEvidence.purlLocationMap,
|
|
751
1021
|
)) {
|
|
@@ -770,16 +1040,12 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
770
1040
|
safeExistsSync(options.dataFlowSlicesFile)
|
|
771
1041
|
) {
|
|
772
1042
|
dataFlowSlicesFile = options.dataFlowSlicesFile;
|
|
773
|
-
dataFlowSlice =
|
|
774
|
-
fs.readFileSync(options.dataFlowSlicesFile, "utf-8"),
|
|
775
|
-
);
|
|
1043
|
+
dataFlowSlice = readDosaiDataFlowReport(options.dataFlowSlicesFile);
|
|
776
1044
|
} else {
|
|
777
1045
|
retMap = await createSlice(language, dirPath, "data-flow", options);
|
|
778
1046
|
if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
|
|
779
1047
|
dataFlowSlicesFile = retMap.slicesFile;
|
|
780
|
-
dataFlowSlice =
|
|
781
|
-
fs.readFileSync(retMap.slicesFile, "utf-8"),
|
|
782
|
-
);
|
|
1048
|
+
dataFlowSlice = readDosaiDataFlowReport(retMap.slicesFile);
|
|
783
1049
|
}
|
|
784
1050
|
}
|
|
785
1051
|
if (dataFlowSlice && Object.keys(dataFlowSlice).length) {
|
|
@@ -826,7 +1092,7 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
826
1092
|
) {
|
|
827
1093
|
reachablesSlicesFile = options.reachablesSlicesFile;
|
|
828
1094
|
reachablesSlice = filterAtomSliceData(
|
|
829
|
-
|
|
1095
|
+
readReachablesSlices(options.reachablesSlicesFile),
|
|
830
1096
|
options,
|
|
831
1097
|
);
|
|
832
1098
|
} else {
|
|
@@ -834,7 +1100,7 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
834
1100
|
if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
|
|
835
1101
|
reachablesSlicesFile = retMap.slicesFile;
|
|
836
1102
|
reachablesSlice = filterAtomSliceData(
|
|
837
|
-
|
|
1103
|
+
readReachablesSlices(retMap.slicesFile),
|
|
838
1104
|
options,
|
|
839
1105
|
);
|
|
840
1106
|
}
|
|
@@ -850,7 +1116,7 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
850
1116
|
// Reuse existing usages slices
|
|
851
1117
|
if (options.usagesSlicesFile && usableSlicesFile(options.usagesSlicesFile)) {
|
|
852
1118
|
usageSlice = filterAtomSliceData(
|
|
853
|
-
|
|
1119
|
+
readSlicesFile(options.usagesSlicesFile, "usages"),
|
|
854
1120
|
options,
|
|
855
1121
|
);
|
|
856
1122
|
usagesSlicesFile = options.usagesSlicesFile;
|
|
@@ -859,7 +1125,7 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
859
1125
|
retMap = await createSlice(language, dirPath, "usages", options);
|
|
860
1126
|
if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
|
|
861
1127
|
usageSlice = filterAtomSliceData(
|
|
862
|
-
|
|
1128
|
+
readSlicesFile(retMap.slicesFile, "usages"),
|
|
863
1129
|
options,
|
|
864
1130
|
);
|
|
865
1131
|
usagesSlicesFile = retMap.slicesFile;
|
|
@@ -885,21 +1151,20 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
885
1151
|
components.length
|
|
886
1152
|
) {
|
|
887
1153
|
// Reuse existing semantics slices for swift and scala
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1154
|
+
const reusableSlice = loadReusableSemanticsSlice(
|
|
1155
|
+
language,
|
|
1156
|
+
options.semanticsSlicesFile,
|
|
1157
|
+
bomFile,
|
|
1158
|
+
dirPath,
|
|
1159
|
+
);
|
|
1160
|
+
if (reusableSlice) {
|
|
1161
|
+
semanticsSlice = reusableSlice;
|
|
895
1162
|
semanticsSlicesFile = options.semanticsSlicesFile;
|
|
896
1163
|
} else if (PROJECT_TYPE_ALIASES.swift.includes(language)) {
|
|
897
1164
|
// Generate our own slices for swift
|
|
898
1165
|
retMap = await createSlice(language, dirPath, "semantics", options);
|
|
899
1166
|
if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
|
|
900
|
-
semanticsSlice =
|
|
901
|
-
fs.readFileSync(retMap.slicesFile, "utf-8"),
|
|
902
|
-
);
|
|
1167
|
+
semanticsSlice = readSlicesFile(retMap.slicesFile, "semantics");
|
|
903
1168
|
semanticsSlicesFile = retMap.slicesFile;
|
|
904
1169
|
}
|
|
905
1170
|
}
|
|
@@ -936,7 +1201,7 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
936
1201
|
) {
|
|
937
1202
|
dataFlowSlicesFile = options.dataFlowSlicesFile;
|
|
938
1203
|
dataFlowSlice = filterAtomSliceData(
|
|
939
|
-
|
|
1204
|
+
readSlicesFile(options.dataFlowSlicesFile, "data-flow"),
|
|
940
1205
|
options,
|
|
941
1206
|
);
|
|
942
1207
|
} else if (!PROJECT_TYPE_ALIASES.scala.includes(language)) {
|
|
@@ -944,7 +1209,7 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
944
1209
|
if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
|
|
945
1210
|
dataFlowSlicesFile = retMap.slicesFile;
|
|
946
1211
|
dataFlowSlice = filterAtomSliceData(
|
|
947
|
-
|
|
1212
|
+
readSlicesFile(retMap.slicesFile, "data-flow"),
|
|
948
1213
|
options,
|
|
949
1214
|
);
|
|
950
1215
|
}
|
|
@@ -1303,6 +1568,14 @@ export async function parseSliceUsages(
|
|
|
1303
1568
|
/**
|
|
1304
1569
|
* Method to parse semantic slice data. Currently supported for swift and scala languages.
|
|
1305
1570
|
*
|
|
1571
|
+
* Swift slices created by cdxgen record, for every indexed source file, the
|
|
1572
|
+
* lines that reference each dependency module (`moduleReferences`); the module
|
|
1573
|
+
* of a reference is the declaring module recovered from the compiler-resolved
|
|
1574
|
+
* USR. Components are mapped to their package's modules through the
|
|
1575
|
+
* `packages` slice, so every occurrence is backed by a resolved reference.
|
|
1576
|
+
* Slices without `moduleReferences` (created by older cdxgen versions) fall
|
|
1577
|
+
* back to matching symbol names.
|
|
1578
|
+
*
|
|
1306
1579
|
* @param {String} language Project language.
|
|
1307
1580
|
* @param {Array} components Components from the input SBOM
|
|
1308
1581
|
* @param {Object} semanticsSlice Semantic slice data
|
|
@@ -1316,86 +1589,47 @@ export function parseSemanticSlices(language, components, semanticsSlice) {
|
|
|
1316
1589
|
if (!components) {
|
|
1317
1590
|
return undefined;
|
|
1318
1591
|
}
|
|
1319
|
-
const
|
|
1320
|
-
const
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
// - A granular and precise moduleInfos, which has the exact classes, protocols, enums etc belonging to each module
|
|
1325
|
-
// The objective then is to build the purlLocationMap, which is the list of locations where a given purl is used
|
|
1326
|
-
// For this, we have two attributes:
|
|
1327
|
-
// - A simpler fileStructures attribute that has the mapping between a swift file and the list of referenced types
|
|
1328
|
-
// - A granular fileIndexes attribute that contains information about the clang modules and line number information
|
|
1329
|
-
|
|
1330
|
-
// We first need to map out the component names to their purls
|
|
1331
|
-
// This is because the semantics slice use the module names everywhere
|
|
1592
|
+
const fileIndexes = semanticsSlice?.fileIndexes || {};
|
|
1593
|
+
const hasModuleReferences = Object.values(fileIndexes).some(
|
|
1594
|
+
(afileIndex) => afileIndex?.moduleReferences,
|
|
1595
|
+
);
|
|
1596
|
+
const purlLocationsSet = {};
|
|
1332
1597
|
for (const comp of components) {
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
if (semanticsSlice?.buildSymbols[comp.name]) {
|
|
1338
|
-
for (const asym of semanticsSlice.buildSymbols[comp.name]) {
|
|
1339
|
-
componentSymbolsMap[comp.name].add(asym);
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
const moduleInfo = semanticsSlice?.moduleInfos[comp.name] || {};
|
|
1343
|
-
if (moduleInfo.classes) {
|
|
1344
|
-
for (const asym of moduleInfo.classes) {
|
|
1345
|
-
componentSymbolsMap[comp.name].add(asym);
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
if (moduleInfo.protocols) {
|
|
1349
|
-
for (const asym of moduleInfo.protocols) {
|
|
1350
|
-
componentSymbolsMap[comp.name].add(asym);
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
if (moduleInfo.enums) {
|
|
1354
|
-
for (const asym of moduleInfo.enums) {
|
|
1355
|
-
componentSymbolsMap[comp.name].add(asym);
|
|
1356
|
-
}
|
|
1598
|
+
// Local packages and the root application have no purl, so there is
|
|
1599
|
+
// nothing the evidence could be attached to downstream
|
|
1600
|
+
if (!comp?.purl) {
|
|
1601
|
+
continue;
|
|
1357
1602
|
}
|
|
1358
|
-
|
|
1359
|
-
if (
|
|
1360
|
-
|
|
1361
|
-
for (const asym of moduleInfo.classMethods[aclassName]) {
|
|
1362
|
-
componentSymbolsMap[comp.name].add(asym);
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1603
|
+
const moduleNames = resolveSwiftComponentModules(comp, semanticsSlice);
|
|
1604
|
+
if (!moduleNames.size) {
|
|
1605
|
+
continue;
|
|
1365
1606
|
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1607
|
+
const locations = purlLocationsSet[comp.purl] || new Set();
|
|
1608
|
+
if (hasModuleReferences) {
|
|
1609
|
+
for (const [aswiftFile, afileIndex] of Object.entries(fileIndexes)) {
|
|
1610
|
+
for (const [amodule, lines] of Object.entries(
|
|
1611
|
+
afileIndex?.moduleReferences || {},
|
|
1612
|
+
)) {
|
|
1613
|
+
if (!moduleNames.has(amodule)) {
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
for (const aline of lines) {
|
|
1617
|
+
locations.add(`${aswiftFile}#${aline}`);
|
|
1618
|
+
}
|
|
1370
1619
|
}
|
|
1371
1620
|
}
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
const purlLocationsSet = {};
|
|
1381
|
-
// We now have a good set of data in componentSymbolsMap and allObfuscationsMap
|
|
1382
|
-
// We can iterate these symbols and check if they exist under fileIndexes.symbolLocations
|
|
1383
|
-
for (const compName of Object.keys(componentSymbolsMap)) {
|
|
1384
|
-
const compSymbols = Array.from(componentSymbolsMap[compName]);
|
|
1385
|
-
const searchHits = searchSymbolLocations(
|
|
1386
|
-
compSymbols,
|
|
1387
|
-
semanticsSlice?.fileIndexes,
|
|
1388
|
-
);
|
|
1389
|
-
// We have an occurrence hit. Let's populate the purlLocationMap
|
|
1390
|
-
if (searchHits?.length) {
|
|
1391
|
-
const locations =
|
|
1392
|
-
purlLocationsSet[componentNamePurlMap[compName]] || new Set();
|
|
1393
|
-
for (const ahit of searchHits) {
|
|
1621
|
+
} else {
|
|
1622
|
+
for (const ahit of searchSymbolLocations(
|
|
1623
|
+
collectLegacySwiftSymbols(moduleNames, semanticsSlice),
|
|
1624
|
+
fileIndexes,
|
|
1625
|
+
)) {
|
|
1394
1626
|
for (const aline of ahit.lineNumbers) {
|
|
1395
1627
|
locations.add(`${ahit.file}#${aline}`);
|
|
1396
1628
|
}
|
|
1397
1629
|
}
|
|
1398
|
-
|
|
1630
|
+
}
|
|
1631
|
+
if (locations.size) {
|
|
1632
|
+
purlLocationsSet[comp.purl] = locations;
|
|
1399
1633
|
}
|
|
1400
1634
|
}
|
|
1401
1635
|
const purlLocationMap = {};
|
|
@@ -1405,22 +1639,184 @@ export function parseSemanticSlices(language, components, semanticsSlice) {
|
|
|
1405
1639
|
return { purlLocationMap };
|
|
1406
1640
|
}
|
|
1407
1641
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1642
|
+
/**
|
|
1643
|
+
* Normalise a repository location or Swift purl into `host/owner/name`.
|
|
1644
|
+
*
|
|
1645
|
+
* @param {String} value Repository URL, scp-style git location, or swift purl
|
|
1646
|
+
* @returns {undefined|String} Lowercase `host/path` without scheme and `.git`
|
|
1647
|
+
*/
|
|
1648
|
+
function normalizeSwiftPackageLocation(value) {
|
|
1649
|
+
if (!value || typeof value !== "string") {
|
|
1411
1650
|
return undefined;
|
|
1412
1651
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1652
|
+
let text = value.trim();
|
|
1653
|
+
if (text.startsWith("pkg:swift/")) {
|
|
1654
|
+
text = text.slice("pkg:swift/".length).split("?")[0].split("#")[0];
|
|
1655
|
+
const at = text.lastIndexOf("@");
|
|
1656
|
+
if (at > 0) {
|
|
1657
|
+
text = text.slice(0, at);
|
|
1658
|
+
}
|
|
1659
|
+
try {
|
|
1660
|
+
text = decodeURIComponent(text);
|
|
1661
|
+
} catch (_e) {
|
|
1662
|
+
// Keep the raw value
|
|
1663
|
+
}
|
|
1664
|
+
} else {
|
|
1665
|
+
text = text.split("?")[0].split("#")[0];
|
|
1666
|
+
const scheme = text.indexOf("://");
|
|
1667
|
+
if (scheme >= 0) {
|
|
1668
|
+
text = text.slice(scheme + 3);
|
|
1669
|
+
// Drop userinfo such as git@
|
|
1670
|
+
const slash = text.indexOf("/");
|
|
1671
|
+
const at = text.indexOf("@");
|
|
1672
|
+
if (at >= 0 && (slash < 0 || at < slash)) {
|
|
1673
|
+
text = text.slice(at + 1);
|
|
1674
|
+
}
|
|
1675
|
+
} else if (text.includes("@") && text.includes(":")) {
|
|
1676
|
+
// scp-style git@github.com:owner/name.git
|
|
1677
|
+
text = text.slice(text.indexOf("@") + 1).replace(":", "/");
|
|
1678
|
+
} else {
|
|
1679
|
+
return undefined;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
text = text.replace(/\/+$/, "");
|
|
1683
|
+
if (text.endsWith(".git")) {
|
|
1684
|
+
text = text.slice(0, -4);
|
|
1685
|
+
}
|
|
1686
|
+
if (text.startsWith("www.")) {
|
|
1687
|
+
text = text.slice(4);
|
|
1688
|
+
}
|
|
1689
|
+
return text.toLowerCase() || undefined;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
/**
|
|
1693
|
+
* Resolve the Swift module names that belong to a SBOM component.
|
|
1694
|
+
*
|
|
1695
|
+
* A component is named after its package repository or identity while the
|
|
1696
|
+
* semantic slices are keyed by module names. With the `packages` slice, a
|
|
1697
|
+
* package cloned from a repository matches the component whose purl names
|
|
1698
|
+
* the same repository, so a fork with the same name never inherits another
|
|
1699
|
+
* package's evidence; registry and local packages, which have no repository
|
|
1700
|
+
* URL, match by identity or name. Older slices only carry the
|
|
1701
|
+
* `packageModules` aliases (identity, package name, repository name), which
|
|
1702
|
+
* are matched against the component name and the `cdx:swift:packageName`
|
|
1703
|
+
* property, so that `swift-argument-parser` resolves to its `ArgumentParser`
|
|
1704
|
+
* module.
|
|
1705
|
+
*
|
|
1706
|
+
* @param {Object} comp SBOM component
|
|
1707
|
+
* @param {Object} semanticsSlice Semantic slice data
|
|
1708
|
+
* @returns {Set<string>} Module names associated with the component
|
|
1709
|
+
*/
|
|
1710
|
+
function resolveSwiftComponentModules(comp, semanticsSlice) {
|
|
1711
|
+
const moduleNames = new Set();
|
|
1712
|
+
if (!comp?.name) {
|
|
1713
|
+
return moduleNames;
|
|
1714
|
+
}
|
|
1715
|
+
const aliases = new Set(
|
|
1716
|
+
[comp.name, getPropertyValue(comp, "cdx:swift:packageName")]
|
|
1717
|
+
.filter(Boolean)
|
|
1718
|
+
.map((alias) => String(alias).toLowerCase()),
|
|
1719
|
+
);
|
|
1720
|
+
if (Array.isArray(semanticsSlice?.packages)) {
|
|
1721
|
+
const componentLocation = normalizeSwiftPackageLocation(comp.purl);
|
|
1722
|
+
for (const apkg of semanticsSlice.packages) {
|
|
1723
|
+
const packageLocation = normalizeSwiftPackageLocation(apkg?.location);
|
|
1724
|
+
const matches =
|
|
1725
|
+
packageLocation && componentLocation
|
|
1726
|
+
? packageLocation === componentLocation
|
|
1727
|
+
: [apkg?.identity, apkg?.name].some(
|
|
1728
|
+
(alias) => alias && aliases.has(String(alias).toLowerCase()),
|
|
1729
|
+
);
|
|
1730
|
+
if (matches) {
|
|
1731
|
+
for (const amodule of apkg.modules || []) {
|
|
1732
|
+
moduleNames.add(amodule);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
return moduleNames;
|
|
1737
|
+
}
|
|
1738
|
+
const packageModules = semanticsSlice?.packageModules || {};
|
|
1739
|
+
for (const alias of new Set([comp.name, ...aliases])) {
|
|
1740
|
+
if (Array.isArray(packageModules[alias])) {
|
|
1741
|
+
for (const amodule of packageModules[alias]) {
|
|
1742
|
+
moduleNames.add(amodule);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
// Modules named after the package (Yams, SWXMLHash) still match
|
|
1747
|
+
if (!moduleNames.size) {
|
|
1748
|
+
moduleNames.add(comp.name);
|
|
1749
|
+
}
|
|
1750
|
+
return moduleNames;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
function getPropertyValue(comp, propertyName) {
|
|
1754
|
+
for (const aproperty of comp?.properties || []) {
|
|
1755
|
+
if (aproperty?.name === propertyName) {
|
|
1756
|
+
return aproperty.value;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
return undefined;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
/**
|
|
1763
|
+
* Collect the symbol names of a component's modules from slices that predate
|
|
1764
|
+
* `moduleReferences`, using the moduleInfos data and the coarser buildSymbols
|
|
1765
|
+
* derived from the build's output file maps.
|
|
1766
|
+
*
|
|
1767
|
+
* @param {Set<String>} moduleNames Module names of the component
|
|
1768
|
+
* @param {Object} semanticsSlice Semantic slice data
|
|
1769
|
+
* @returns {Object} `{ symbols, freeFunctions }` name sets
|
|
1770
|
+
*/
|
|
1771
|
+
function collectLegacySwiftSymbols(moduleNames, semanticsSlice) {
|
|
1772
|
+
const symbols = new Set();
|
|
1773
|
+
const freeFunctions = new Set();
|
|
1774
|
+
for (const moduleName of moduleNames) {
|
|
1775
|
+
for (const asym of semanticsSlice?.buildSymbols?.[moduleName] || []) {
|
|
1776
|
+
symbols.add(asym);
|
|
1777
|
+
}
|
|
1778
|
+
const moduleInfo = semanticsSlice?.moduleInfos?.[moduleName] || {};
|
|
1779
|
+
for (const asym of [
|
|
1780
|
+
...(moduleInfo.classes || []),
|
|
1781
|
+
...(moduleInfo.protocols || []),
|
|
1782
|
+
...(moduleInfo.enums || []),
|
|
1783
|
+
]) {
|
|
1784
|
+
symbols.add(asym);
|
|
1785
|
+
}
|
|
1786
|
+
for (const methods of [
|
|
1787
|
+
...Object.values(moduleInfo.classMethods || {}),
|
|
1788
|
+
...Object.values(moduleInfo.protocolMethods || {}),
|
|
1789
|
+
]) {
|
|
1790
|
+
for (const asym of methods || []) {
|
|
1791
|
+
symbols.add(asym);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
for (const asym of moduleInfo.functions || []) {
|
|
1795
|
+
freeFunctions.add(asym);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
return { symbols, freeFunctions };
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
function searchSymbolLocations(componentSymbols, fileIndexes) {
|
|
1802
|
+
const searchHits = [];
|
|
1803
|
+
const { symbols, freeFunctions } = componentSymbols;
|
|
1804
|
+
for (const aswiftFile of Object.keys(fileIndexes || {})) {
|
|
1805
|
+
const symbolLocations = fileIndexes[aswiftFile]?.symbolLocations;
|
|
1806
|
+
if (!symbolLocations) {
|
|
1415
1807
|
continue;
|
|
1416
1808
|
}
|
|
1417
|
-
for (const asym of Object.keys(
|
|
1418
|
-
|
|
1419
|
-
|
|
1809
|
+
for (const asym of Object.keys(symbolLocations)) {
|
|
1810
|
+
// The index records free functions by their full selector
|
|
1811
|
+
// (`dump(object:canonical:...)`) while module interfaces give the base
|
|
1812
|
+
// name. Only top-level functions are matched by base name: members such
|
|
1813
|
+
// as `encode(to:)` exist in nearly every module
|
|
1814
|
+
const baseName = asym.includes("(") ? asym.split("(")[0] : undefined;
|
|
1815
|
+
if (symbols.has(asym) || (baseName && freeFunctions.has(baseName))) {
|
|
1420
1816
|
searchHits.push({
|
|
1421
1817
|
file: aswiftFile,
|
|
1422
1818
|
symbol: asym,
|
|
1423
|
-
lineNumbers,
|
|
1819
|
+
lineNumbers: symbolLocations[asym],
|
|
1424
1820
|
});
|
|
1425
1821
|
}
|
|
1426
1822
|
}
|
|
@@ -1833,6 +2229,7 @@ export async function createEvinseFile(sliceArtefacts, options) {
|
|
|
1833
2229
|
cryptoComponents,
|
|
1834
2230
|
aiComponents,
|
|
1835
2231
|
cryptoGeneratePurls,
|
|
2232
|
+
workspaceComponent,
|
|
1836
2233
|
} = sliceArtefacts;
|
|
1837
2234
|
const bomFile = options.input;
|
|
1838
2235
|
const evinseOutFile = options.output;
|
|
@@ -1840,7 +2237,22 @@ export async function createEvinseFile(sliceArtefacts, options) {
|
|
|
1840
2237
|
const components = bomJson.components || [];
|
|
1841
2238
|
// Clear existing annotations
|
|
1842
2239
|
bomJson.annotations = [];
|
|
1843
|
-
|
|
2240
|
+
if (workspaceComponent && !bomJson.metadata?.component?.purl) {
|
|
2241
|
+
if (bomJson.metadata?.component) {
|
|
2242
|
+
// A purl-less project component: keep cdxgen's own identity fields
|
|
2243
|
+
// and supply the purl kosi's module discovery published, so the
|
|
2244
|
+
// evidence loop below can anchor workspace evidence to it.
|
|
2245
|
+
bomJson.metadata.component.purl = workspaceComponent.purl;
|
|
2246
|
+
bomJson.metadata.component["bom-ref"] ??= workspaceComponent.purl;
|
|
2247
|
+
} else {
|
|
2248
|
+
bomJson.metadata ??= {};
|
|
2249
|
+
bomJson.metadata.component = workspaceComponent;
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
mergeAnalyzerMetadataProperties(
|
|
2253
|
+
bomJson.metadata?.component,
|
|
2254
|
+
metadataProperties,
|
|
2255
|
+
);
|
|
1844
2256
|
let occEvidencePresent = false;
|
|
1845
2257
|
let csEvidencePresent = false;
|
|
1846
2258
|
let servicesPresent = false;
|
|
@@ -1899,8 +2311,15 @@ export async function createEvinseFile(sliceArtefacts, options) {
|
|
|
1899
2311
|
servicesPresent = true;
|
|
1900
2312
|
}
|
|
1901
2313
|
// Add the crypto components to the components list
|
|
2314
|
+
// Multi-type scans may have collected the same crypto assets during BOM
|
|
2315
|
+
// generation, so skip any bom-ref already present to keep the BOM valid.
|
|
1902
2316
|
if (cryptoComponents?.length) {
|
|
1903
|
-
|
|
2317
|
+
const existingRefs = new Set(
|
|
2318
|
+
bomJson.components.map((comp) => comp["bom-ref"]).filter(Boolean),
|
|
2319
|
+
);
|
|
2320
|
+
bomJson.components = bomJson.components.concat(
|
|
2321
|
+
cryptoComponents.filter((comp) => !existingRefs.has(comp["bom-ref"])),
|
|
2322
|
+
);
|
|
1904
2323
|
}
|
|
1905
2324
|
// Add the AI components (machine-learning-model, dataset) discovered by dosai
|
|
1906
2325
|
if (aiComponents?.length) {
|
|
@@ -1925,28 +2344,27 @@ export async function createEvinseFile(sliceArtefacts, options) {
|
|
|
1925
2344
|
bomJson.dependencies = newDependencies;
|
|
1926
2345
|
}
|
|
1927
2346
|
if (options.annotate) {
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
if (reachablesSlicesFile && safeExistsSync(reachablesSlicesFile)) {
|
|
2347
|
+
for (const slicesFile of [
|
|
2348
|
+
usagesSlicesFile,
|
|
2349
|
+
dataFlowSlicesFile,
|
|
2350
|
+
reachablesSlicesFile,
|
|
2351
|
+
]) {
|
|
2352
|
+
if (!slicesFile || !safeExistsSync(slicesFile)) {
|
|
2353
|
+
continue;
|
|
2354
|
+
}
|
|
2355
|
+
// The annotation embeds the slice as one string, which a file this
|
|
2356
|
+
// large cannot become.
|
|
2357
|
+
if (fs.statSync(slicesFile).size > MAX_JSON_TEXT_BYTES) {
|
|
2358
|
+
console.warn(
|
|
2359
|
+
`Not annotating the BOM with the slices file "${slicesFile}", which is too large to embed as text.`,
|
|
2360
|
+
);
|
|
2361
|
+
continue;
|
|
2362
|
+
}
|
|
1945
2363
|
bomJson.annotations.push({
|
|
1946
2364
|
subjects: [bomJson.serialNumber],
|
|
1947
2365
|
annotator: { component: bomJson.metadata.tools.components[0] },
|
|
1948
2366
|
timestamp: getTimestamp(),
|
|
1949
|
-
text: fs.readFileSync(
|
|
2367
|
+
text: fs.readFileSync(slicesFile, "utf8"),
|
|
1950
2368
|
});
|
|
1951
2369
|
}
|
|
1952
2370
|
}
|