@cdxgen/cdxgen 13.2.0 → 13.4.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 +33 -14
- package/bin/cdxgen.js +310 -138
- package/bin/evinse.js +22 -0
- package/bin/repl.js +86 -0
- package/bin/sign.js +119 -15
- package/bin/tracebom.js +69 -5
- package/bin/validate.js +107 -19
- package/bin/verify.js +172 -63
- package/data/helpers/init.gradle +46 -21
- package/data/remediations.json +85 -0
- package/data/rules/dependency-sources.yaml +67 -0
- package/data/rules/scala-evidence.yaml +115 -0
- package/lib/audit/index.js +38 -6
- package/lib/cli/bomAssembly.js +105 -15
- package/lib/cli/cliOptions.js +93 -1
- package/lib/cli/index.js +156 -58
- package/lib/cli/jsBom.js +99 -53
- package/lib/cli/jvmBom.js +708 -147
- package/lib/cli/managedBom.js +415 -224
- package/lib/cli/nativeBom.js +264 -44
- package/lib/core/activity.js +12 -0
- package/lib/core/env.js +22 -15
- package/lib/core/fs.js +176 -7
- package/lib/core/httpClient.js +159 -24
- package/lib/core/metadataProperties.js +97 -0
- package/lib/core/pluginRun.js +184 -0
- package/lib/core/processTree.js +100 -0
- package/lib/core/projectConfig.js +31 -4
- package/lib/core/runState.js +39 -0
- package/lib/core/toml.js +72 -0
- package/lib/ecosystems/asarutils.js +4 -2
- package/lib/ecosystems/bunutils.js +274 -108
- package/lib/ecosystems/cargoCache.js +231 -8
- package/lib/ecosystems/caxa.js +434 -2
- package/lib/ecosystems/cmakeResolver.js +76 -21
- package/lib/ecosystems/cppBuildContext.js +631 -0
- package/lib/ecosystems/cppEvidence.js +399 -51
- package/lib/ecosystems/cppIncludeResolver.js +294 -0
- package/lib/ecosystems/denoutils.js +93 -66
- package/lib/ecosystems/ecosystems.js +1983 -793
- package/lib/ecosystems/golem.js +28 -12
- package/lib/ecosystems/gradleutils.js +356 -37
- package/lib/ecosystems/jsEvidence.js +104 -44
- package/lib/ecosystems/kosi.js +31 -7
- package/lib/ecosystems/npmutils.js +174 -9
- package/lib/ecosystems/parsers-dotnet.js +616 -159
- package/lib/ecosystems/parsers-elm.js +46 -28
- package/lib/ecosystems/parsers-gleam.js +3 -4
- package/lib/ecosystems/parsers-go.js +371 -70
- package/lib/ecosystems/parsers-js.js +263 -76
- package/lib/ecosystems/parsers-julia.js +2 -3
- package/lib/ecosystems/parsers-jvm.js +204 -12
- package/lib/ecosystems/parsers-misc.js +929 -216
- package/lib/ecosystems/parsers-python.js +167 -24
- package/lib/ecosystems/parsers-rust.js +28 -22
- package/lib/ecosystems/parsers-terraform.js +1573 -19
- package/lib/ecosystems/pythonutils.js +102 -6
- package/lib/ecosystems/remote/huggingface.js +3 -0
- package/lib/ecosystems/rubyutils.js +1492 -182
- package/lib/ecosystems/rusi.js +54 -10
- package/lib/ecosystems/sbtutils.js +243 -187
- package/lib/ecosystems/scalacliutils.js +342 -0
- package/lib/ecosystems/scalasem.js +2618 -0
- package/lib/ecosystems/terraformRegistry.js +288 -0
- package/lib/ecosystems/utils.js +2 -0
- package/lib/ecosystems/vendoredCode.js +156 -0
- package/lib/evinser/evinser.js +1008 -191
- package/lib/evinser/swiftsem.js +1914 -302
- package/lib/helpers/agenticToolutils.js +2 -2
- package/lib/helpers/bomSigner.js +1101 -222
- package/lib/inventory/aiCollector.js +4 -1
- package/lib/inventory/analyzer.js +110 -127
- package/lib/inventory/atomUtils.js +703 -43
- package/lib/inventory/auditCategories.js +7 -0
- package/lib/inventory/buildToolRateLimit.js +144 -0
- package/lib/inventory/cacheCommands.js +2 -1
- package/lib/inventory/cdxrs.js +103 -12
- package/lib/inventory/cmakeBuildDirs.js +254 -0
- package/lib/inventory/deps.js +501 -124
- package/lib/inventory/depsUtils.js +183 -11
- package/lib/inventory/display.js +4 -5
- package/lib/inventory/dosai.js +798 -57
- package/lib/inventory/dosaiParsers.js +310 -15
- package/lib/inventory/dynamic.js +90 -6
- package/lib/inventory/evidenceUtils.js +166 -113
- package/lib/inventory/fetchBatch.js +690 -55
- package/lib/inventory/fetchDiskCache.js +597 -0
- package/lib/inventory/forgeEnricher.js +14 -5
- package/lib/inventory/jvmLocalRepos.js +917 -0
- package/lib/inventory/mcpConfigParser.js +2 -3
- package/lib/inventory/parentSelection.js +146 -0
- package/lib/inventory/protobom.js +8 -1
- package/lib/inventory/purl.js +152 -3
- package/lib/inventory/rustFormulationParser.js +3 -4
- package/lib/inventory/sbomDocument.js +2 -70
- package/lib/inventory/scalaCoords.js +153 -0
- package/lib/inventory/source.js +45 -27
- package/lib/inventory/spdx.js +101 -0
- package/lib/inventory/traceRunner.js +295 -25
- package/lib/inventory/tracedCrypto.js +294 -0
- package/lib/managers/binary.js +115 -24
- package/lib/managers/docker.js +467 -46
- package/lib/managers/oci.js +63 -3
- package/lib/parsers/cmakeCache.js +69 -0
- package/lib/parsers/cmakeLists.js +466 -0
- package/lib/parsers/cmakePresets.js +400 -0
- package/lib/parsers/compileCommands.js +557 -0
- package/lib/parsers/largeJson.js +454 -0
- package/lib/parsers/npmrc.js +19 -1
- package/lib/parsers/strictJson.js +322 -0
- package/lib/parsers/terraformConfig.js +1073 -0
- package/lib/parsers/terraformSource.js +652 -0
- package/lib/server/server.js +23 -0
- package/lib/stages/postgen/introspection/markers.js +1 -1
- package/lib/stages/postgen/postgen.js +83 -16
- package/lib/stages/postgen/specVersionCompat.js +32 -0
- package/lib/validator/bomValidator.js +6 -16
- package/lib/validator/index.js +64 -11
- package/lib/validator/reporters/console.js +3 -0
- package/lib/validator/reporters/json.js +2 -0
- package/lib/validator/reporters/sarif.js +7 -0
- package/package.json +23 -23
- package/types/lib/audit/index.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.map +1 -1
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts +24 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts +7 -4
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts +3 -1
- 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 +9 -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/httpClient.d.ts +13 -8
- package/types/lib/core/httpClient.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/pluginRun.d.ts +97 -0
- package/types/lib/core/pluginRun.d.ts.map +1 -0
- package/types/lib/core/processTree.d.ts +36 -0
- package/types/lib/core/processTree.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/runState.d.ts +29 -0
- package/types/lib/core/runState.d.ts.map +1 -0
- package/types/lib/core/toml.d.ts +20 -0
- package/types/lib/core/toml.d.ts.map +1 -0
- package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
- 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 +12 -0
- package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
- 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 +28 -3
- package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
- package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
- package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
- package/types/lib/ecosystems/cppEvidence.d.ts +17 -1
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
- package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
- package/types/lib/ecosystems/denoutils.d.ts +9 -3
- package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts +62 -13
- 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 +70 -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 +13 -0
- package/types/lib/ecosystems/kosi.d.ts.map +1 -1
- package/types/lib/ecosystems/npmutils.d.ts +57 -0
- package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-dotnet.d.ts +118 -4
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-go.d.ts +54 -2
- package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts +16 -5
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts +46 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +70 -4
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts +53 -3
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
- package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +27 -2
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts +164 -3
- 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/ecosystems/sbtutils.d.ts +88 -2
- package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
- package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
- package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
- package/types/lib/ecosystems/scalasem.d.ts +311 -0
- package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
- package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
- package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
- package/types/lib/ecosystems/utils.d.ts +2 -2
- package/types/lib/ecosystems/utils.d.ts.map +1 -1
- package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
- package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
- package/types/lib/evinser/evinser.d.ts +132 -15
- 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/bomSigner.d.ts +182 -13
- package/types/lib/helpers/bomSigner.d.ts.map +1 -1
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/analyzer.d.ts.map +1 -1
- package/types/lib/inventory/atomUtils.d.ts +219 -5
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +3 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
- package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
- package/types/lib/inventory/cacheCommands.d.ts +2 -1
- package/types/lib/inventory/cacheCommands.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/cmakeBuildDirs.d.ts +58 -0
- package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
- package/types/lib/inventory/deps.d.ts +59 -1
- package/types/lib/inventory/deps.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 +121 -9
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dosaiParsers.d.ts +53 -7
- package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
- package/types/lib/inventory/dynamic.d.ts +10 -0
- package/types/lib/inventory/dynamic.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts +19 -4
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +83 -14
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
- package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
- package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
- package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
- package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/parentSelection.d.ts +34 -0
- package/types/lib/inventory/parentSelection.d.ts.map +1 -0
- package/types/lib/inventory/plugins.d.ts.map +1 -1
- package/types/lib/inventory/protobom.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/rustFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
- package/types/lib/inventory/scalaCoords.d.ts +80 -0
- package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/spdx.d.ts +9 -0
- package/types/lib/inventory/spdx.d.ts.map +1 -1
- package/types/lib/inventory/traceRunner.d.ts +59 -1
- package/types/lib/inventory/traceRunner.d.ts.map +1 -1
- package/types/lib/inventory/tracedCrypto.d.ts +42 -0
- package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
- 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 +17 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/parsers/cmakeCache.d.ts +16 -0
- package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
- package/types/lib/parsers/cmakeLists.d.ts +77 -0
- package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
- package/types/lib/parsers/cmakePresets.d.ts +78 -0
- package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
- package/types/lib/parsers/compileCommands.d.ts +115 -0
- package/types/lib/parsers/compileCommands.d.ts.map +1 -0
- 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/strictJson.d.ts +46 -0
- package/types/lib/parsers/strictJson.d.ts.map +1 -0
- package/types/lib/parsers/terraformConfig.d.ts +70 -0
- package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
- package/types/lib/parsers/terraformSource.d.ts +66 -0
- package/types/lib/parsers/terraformSource.d.ts.map +1 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/index.d.ts +18 -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/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
package/lib/evinser/evinser.js
CHANGED
|
@@ -15,6 +15,11 @@ import {
|
|
|
15
15
|
safeRmSync,
|
|
16
16
|
safeWriteSync,
|
|
17
17
|
} from "../core/fs.js";
|
|
18
|
+
import { hasCppSources } from "../ecosystems/cppEvidence.js";
|
|
19
|
+
import {
|
|
20
|
+
createIncludeAttributor,
|
|
21
|
+
parseCIncludeSlices,
|
|
22
|
+
} from "../ecosystems/cppIncludeResolver.js";
|
|
18
23
|
import {
|
|
19
24
|
analyzeGolemProject,
|
|
20
25
|
collectGolemEvidence,
|
|
@@ -40,14 +45,27 @@ import {
|
|
|
40
45
|
isRusiRustLanguage,
|
|
41
46
|
} from "../ecosystems/rusi.js";
|
|
42
47
|
import {
|
|
48
|
+
analyzeScalaProject,
|
|
49
|
+
collectScalasemApiEndpoints,
|
|
50
|
+
collectScalasemEvidence,
|
|
51
|
+
collectScalasemServices,
|
|
52
|
+
isScalasemLanguage,
|
|
53
|
+
} from "../ecosystems/scalasem.js";
|
|
54
|
+
import {
|
|
55
|
+
atomCompileCommandsArgs,
|
|
43
56
|
buildAtomCommandEnv,
|
|
44
57
|
executeAtom,
|
|
45
58
|
filterAtomSlicesByExcludePatterns,
|
|
59
|
+
readReachablesSlices,
|
|
60
|
+
readSlicesFile,
|
|
61
|
+
removeReachablesChunkFiles,
|
|
46
62
|
} from "../inventory/atomUtils.js";
|
|
63
|
+
import { isCycloneDxComponentTypeEnabled } from "../inventory/bomUtils.js";
|
|
47
64
|
import {
|
|
48
65
|
collectDosaiCryptoComponents,
|
|
49
66
|
findCryptoAlgos,
|
|
50
67
|
} from "../inventory/cbomutils.js";
|
|
68
|
+
import { cmakeBuildDirCandidates } from "../inventory/cmakeBuildDirs.js";
|
|
51
69
|
import { collectMvnDependencies } from "../inventory/deps.js";
|
|
52
70
|
import { mergeServices } from "../inventory/depsUtils.js";
|
|
53
71
|
import {
|
|
@@ -59,12 +77,16 @@ import {
|
|
|
59
77
|
collectDosaiServicesFromMethods,
|
|
60
78
|
createDosaiDataFlowSlice,
|
|
61
79
|
createDosaiMethodsSlice,
|
|
80
|
+
dosaiRunWasStopped,
|
|
62
81
|
isDosaiDotnetLanguage,
|
|
63
82
|
normalizeDosaiServiceMap,
|
|
64
83
|
persistDosaiSemanticsReport,
|
|
84
|
+
readDosaiDataFlowReport,
|
|
85
|
+
readDosaiMethodsReport,
|
|
65
86
|
} from "../inventory/dosai.js";
|
|
66
87
|
import { parseOccurrenceEvidenceLocation } from "../inventory/evidenceUtils.js";
|
|
67
88
|
import { executeOsQuery } from "../managers/binary.js";
|
|
89
|
+
import { MAX_JSON_TEXT_BYTES, readJsonFile } from "../parsers/largeJson.js";
|
|
68
90
|
import { postProcess } from "../stages/postgen/postgen.js";
|
|
69
91
|
import { createOrLoad } from "./db.js";
|
|
70
92
|
import { findPurlLocations } from "./scalasem.js";
|
|
@@ -96,10 +118,175 @@ function appendComponentProperties(component, properties = []) {
|
|
|
96
118
|
}
|
|
97
119
|
}
|
|
98
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Merges one analyzer run's metadata properties into the BOM's metadata
|
|
123
|
+
* component. Analyzer metadata describes a single run, so when the input BOM
|
|
124
|
+
* already carries facts from an earlier run (cdxgen --profile research, then
|
|
125
|
+
* evinse), everything under the analyzer's own `cdx:<analyzer>:` prefix is
|
|
126
|
+
* replaced. Replacing by exact name would keep facts the new run no longer
|
|
127
|
+
* emits, such as a cdx:rusi:requestedBackend left from a fallback run.
|
|
128
|
+
*
|
|
129
|
+
* @param {Object} component Metadata component to update in place
|
|
130
|
+
* @param {Object[]} metadataProperties Properties from the analyzer run
|
|
131
|
+
*/
|
|
132
|
+
export function mergeAnalyzerMetadataProperties(
|
|
133
|
+
component,
|
|
134
|
+
metadataProperties = [],
|
|
135
|
+
) {
|
|
136
|
+
if (!component || !metadataProperties.length) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (component.properties?.length) {
|
|
140
|
+
const analyzerPrefixes = [
|
|
141
|
+
...new Set(
|
|
142
|
+
metadataProperties
|
|
143
|
+
.map((p) => /^cdx:[^:]+:/.exec(p.name)?.[0])
|
|
144
|
+
.filter(Boolean),
|
|
145
|
+
),
|
|
146
|
+
];
|
|
147
|
+
component.properties = component.properties.filter(
|
|
148
|
+
(p) => !analyzerPrefixes.some((prefix) => p.name?.startsWith(prefix)),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
appendComponentProperties(component, metadataProperties);
|
|
152
|
+
}
|
|
153
|
+
|
|
99
154
|
function filterAtomSliceData(sliceData, options = {}) {
|
|
100
155
|
return filterAtomSlicesByExcludePatterns(sliceData, options.exclude);
|
|
101
156
|
}
|
|
102
157
|
|
|
158
|
+
/**
|
|
159
|
+
* The project type whose evidence a package URL type belongs to. Generic
|
|
160
|
+
* purls are left out because many collectors use that type; they count as C
|
|
161
|
+
* only when the tree holds C or C++ sources.
|
|
162
|
+
*/
|
|
163
|
+
const EVIDENCE_LANGUAGE_BY_PURL_TYPE = {
|
|
164
|
+
cargo: "rust",
|
|
165
|
+
cocoapods: "swift",
|
|
166
|
+
composer: "php",
|
|
167
|
+
conan: "c",
|
|
168
|
+
gem: "ruby",
|
|
169
|
+
golang: "go",
|
|
170
|
+
maven: "java",
|
|
171
|
+
npm: "js",
|
|
172
|
+
nuget: "csharp",
|
|
173
|
+
pypi: "python",
|
|
174
|
+
swift: "swift",
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The evidence language of a `cdxgen --evidence` run that names no project
|
|
179
|
+
* type.
|
|
180
|
+
*
|
|
181
|
+
* A directory with a Scala build takes the scalasem path. Otherwise the
|
|
182
|
+
* generated BOM decides: Java when it lists Maven packages, as before, and
|
|
183
|
+
* else the language most of its packages belong to. A BOM with no package in
|
|
184
|
+
* a language the evidence step analyses gives no language, so no analyzer
|
|
185
|
+
* runs over sources it cannot read.
|
|
186
|
+
*
|
|
187
|
+
* @param {string} dirPath Project directory
|
|
188
|
+
* @param {Object} [bomJson] The BOM generated for the directory
|
|
189
|
+
* @param {Object} [options] CLI options, for the exclude patterns
|
|
190
|
+
* @returns {string|undefined} Project type for the evidence phase
|
|
191
|
+
*/
|
|
192
|
+
export function defaultEvidenceLanguage(dirPath, bomJson, options = {}) {
|
|
193
|
+
const scalaBuildFiles = [
|
|
194
|
+
"build.sbt",
|
|
195
|
+
"build.sc",
|
|
196
|
+
"build.mill",
|
|
197
|
+
"project.scala",
|
|
198
|
+
];
|
|
199
|
+
if (
|
|
200
|
+
scalaBuildFiles.some((file) => safeExistsSync(join(dirPath || ".", file)))
|
|
201
|
+
) {
|
|
202
|
+
return "scala";
|
|
203
|
+
}
|
|
204
|
+
const counts = new Map();
|
|
205
|
+
let cppSources;
|
|
206
|
+
for (const component of bomJson?.components || []) {
|
|
207
|
+
const purl = `${component?.purl || ""}`;
|
|
208
|
+
if (!purl.startsWith("pkg:") || !purl.includes("/")) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const purlType = purl.slice(4, purl.indexOf("/")).toLowerCase();
|
|
212
|
+
let language = EVIDENCE_LANGUAGE_BY_PURL_TYPE[purlType];
|
|
213
|
+
if (!language && purlType === "generic") {
|
|
214
|
+
cppSources ??= hasCppSources(dirPath || ".", options);
|
|
215
|
+
language = cppSources ? "c" : undefined;
|
|
216
|
+
}
|
|
217
|
+
if (language) {
|
|
218
|
+
counts.set(language, (counts.get(language) || 0) + 1);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (counts.has("java")) {
|
|
222
|
+
return "java";
|
|
223
|
+
}
|
|
224
|
+
let chosen;
|
|
225
|
+
for (const [language, count] of counts) {
|
|
226
|
+
if (!chosen || count > counts.get(chosen)) {
|
|
227
|
+
chosen = language;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return chosen;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Build the options for the evinse phase (prepareDB, analyzeProject, and
|
|
235
|
+
* createEvinseFile) from the cdxgen CLI options.
|
|
236
|
+
*
|
|
237
|
+
* This object used to be assembled inline in bin/cdxgen.js, which dropped
|
|
238
|
+
* fields the analyzers read - most notably `exclude`, so every dosai and atom
|
|
239
|
+
* run triggered by `--evidence` ignored the user's `--exclude` patterns. Keep
|
|
240
|
+
* the wiring in one place so the forwarded fields stay testable.
|
|
241
|
+
*
|
|
242
|
+
* @param {Object} options CLI options
|
|
243
|
+
* @param {Object} args Parsed CLI arguments; `args._[0]` is the source directory
|
|
244
|
+
* @param {string} input Path of the BOM file the evinse phase enriches
|
|
245
|
+
* @returns {Object} Options for prepareDB, analyzeProject, and createEvinseFile
|
|
246
|
+
*/
|
|
247
|
+
export function buildEvinseOptions(options, args, input) {
|
|
248
|
+
const sourceDir = args._?.[0] || ".";
|
|
249
|
+
return {
|
|
250
|
+
_: args._,
|
|
251
|
+
input,
|
|
252
|
+
output: options.evinseOutput,
|
|
253
|
+
language: options.projectType,
|
|
254
|
+
skipMavenCollector: false,
|
|
255
|
+
force: false,
|
|
256
|
+
withReachables: options.deep,
|
|
257
|
+
usagesSlicesFile: options.usagesSlicesFile,
|
|
258
|
+
dataFlowSlicesFile: options.dataFlowSlicesFile,
|
|
259
|
+
reachablesSlicesFile: options.reachablesSlicesFile,
|
|
260
|
+
semanticsSlicesFile: options.semanticsSlicesFile,
|
|
261
|
+
openapiSpecFile: options.openapiSpecFile,
|
|
262
|
+
componentType: options.componentType,
|
|
263
|
+
includeCrypto:
|
|
264
|
+
options.includeCrypto &&
|
|
265
|
+
isCycloneDxComponentTypeEnabled("cryptographic-asset", options),
|
|
266
|
+
specVersion: options.specVersion,
|
|
267
|
+
profile: options.profile,
|
|
268
|
+
jsonPretty: options.jsonPretty,
|
|
269
|
+
exclude: options.exclude,
|
|
270
|
+
// Forwarded so a failed rusi/golem run can claim the --fail-on-error exit
|
|
271
|
+
// instead of degrading the reachability evidence in silence.
|
|
272
|
+
failOnError: options.failOnError,
|
|
273
|
+
// The scala analyzer reads these: a command override, the test-source
|
|
274
|
+
// switch and the install policy that decides whether builds may run.
|
|
275
|
+
scalasemCommand: options.scalasemCommand,
|
|
276
|
+
scalasemIncludeTests: options.scalasemIncludeTests,
|
|
277
|
+
scalasem: options.scalasem,
|
|
278
|
+
installDeps: options.installDeps,
|
|
279
|
+
requiredOnly: options.requiredOnly,
|
|
280
|
+
// --deep publishes the namespaces the Scala classpath join reads.
|
|
281
|
+
scalasemNamespaces: options.deep,
|
|
282
|
+
// Absolute path of the deps slice createCsharpBom writes for --deep runs,
|
|
283
|
+
// so analyzeProject can reuse it instead of rerunning dosai methods.
|
|
284
|
+
depsSlicesFile: options.depsSlicesFile
|
|
285
|
+
? resolve(sourceDir, options.depsSlicesFile)
|
|
286
|
+
: undefined,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
103
290
|
/**
|
|
104
291
|
* Function to create the db for the libraries referred in the sbom.
|
|
105
292
|
*
|
|
@@ -153,7 +340,7 @@ export async function prepareDB(options) {
|
|
|
153
340
|
await catalogMavenDeps(dirPath, purlsJars, Namespaces, options);
|
|
154
341
|
}
|
|
155
342
|
if (gradleFiles?.length) {
|
|
156
|
-
await catalogGradleDeps(dirPath, purlsJars, Namespaces);
|
|
343
|
+
await catalogGradleDeps(dirPath, purlsJars, Namespaces, options);
|
|
157
344
|
}
|
|
158
345
|
}
|
|
159
346
|
for (const purl of Object.keys(purlsToSlice)) {
|
|
@@ -162,11 +349,42 @@ export async function prepareDB(options) {
|
|
|
162
349
|
return { sequelize, Namespaces, Usages, DataFlows };
|
|
163
350
|
}
|
|
164
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Read the jar namespace map cdxgen writes next to the BOM it generates
|
|
354
|
+
* (`<bom>.map`). The map for the BOM passed as `--input` is preferred; a
|
|
355
|
+
* `bom.json.map` in the project directory is the fallback.
|
|
356
|
+
*
|
|
357
|
+
* @param {string} dirPath Project directory scanned by evinse
|
|
358
|
+
* @param {Object} [options] CLI options, for `input`
|
|
359
|
+
* @returns {Object|undefined} The mapping, when a non-empty one exists.
|
|
360
|
+
*/
|
|
361
|
+
export function readNamespaceMap(dirPath, options = {}) {
|
|
362
|
+
const candidates = [
|
|
363
|
+
options?.input ? `${options.input}.map` : undefined,
|
|
364
|
+
join(dirPath, "bom.json.map"),
|
|
365
|
+
];
|
|
366
|
+
for (const mapFile of candidates) {
|
|
367
|
+
if (!mapFile || !safeExistsSync(mapFile)) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
const mapData = JSON.parse(fs.readFileSync(mapFile, "utf-8"));
|
|
372
|
+
if (mapData && Object.keys(mapData).length) {
|
|
373
|
+
return mapData;
|
|
374
|
+
}
|
|
375
|
+
} catch (_err) {
|
|
376
|
+
// An unreadable map is the same as no map.
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
|
|
165
382
|
/**
|
|
166
383
|
* Collect Maven jar namespace mappings for the purls found in the SBOM.
|
|
167
384
|
*
|
|
168
|
-
* If a
|
|
169
|
-
* otherwise the maven command is invoked to collect the jar
|
|
385
|
+
* If cdxgen left a namespace map next to the BOM (see {@link readNamespaceMap})
|
|
386
|
+
* it is reused; otherwise the maven command is invoked to collect the jar
|
|
387
|
+
* dependencies. Each
|
|
170
388
|
* resolved purl is recorded in the `purlsJars` map and persisted in the
|
|
171
389
|
* `Namespaces` model.
|
|
172
390
|
*
|
|
@@ -182,19 +400,7 @@ export async function catalogMavenDeps(
|
|
|
182
400
|
Namespaces,
|
|
183
401
|
options = {},
|
|
184
402
|
) {
|
|
185
|
-
let jarNSMapping;
|
|
186
|
-
if (safeExistsSync(join(dirPath, "bom.json.map"))) {
|
|
187
|
-
try {
|
|
188
|
-
const mapData = JSON.parse(
|
|
189
|
-
fs.readFileSync(join(dirPath, "bom.json.map"), "utf-8"),
|
|
190
|
-
);
|
|
191
|
-
if (mapData && Object.keys(mapData).length) {
|
|
192
|
-
jarNSMapping = mapData;
|
|
193
|
-
}
|
|
194
|
-
} catch (_err) {
|
|
195
|
-
// ignore
|
|
196
|
-
}
|
|
197
|
-
}
|
|
403
|
+
let jarNSMapping = readNamespaceMap(dirPath, options);
|
|
198
404
|
if (!jarNSMapping) {
|
|
199
405
|
console.log("About to collect jar dependencies for the path", dirPath);
|
|
200
406
|
const mavenCmd = getMavenCommand(dirPath, dirPath);
|
|
@@ -238,18 +444,28 @@ export async function catalogMavenDeps(
|
|
|
238
444
|
* @param {Object} Namespaces Sequelize-like Namespaces model used to persist pom and namespaces
|
|
239
445
|
* @returns {Promise<void>}
|
|
240
446
|
*/
|
|
241
|
-
export async function catalogGradleDeps(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
447
|
+
export async function catalogGradleDeps(
|
|
448
|
+
dirPath,
|
|
449
|
+
purlsJars,
|
|
450
|
+
Namespaces,
|
|
451
|
+
options = {},
|
|
452
|
+
) {
|
|
453
|
+
// The map cdxgen wrote next to the BOM already holds the namespaces; the
|
|
454
|
+
// Gradle cache is walked only without it.
|
|
455
|
+
let jarNSMapping = readNamespaceMap(dirPath, options);
|
|
456
|
+
if (!jarNSMapping) {
|
|
457
|
+
console.log(
|
|
458
|
+
"About to collect jar dependencies from the gradle cache. This would take a while ...",
|
|
459
|
+
);
|
|
460
|
+
const gradleCmd = getGradleCommand(dirPath, dirPath);
|
|
461
|
+
// collect all jars including from the cache if data-flow mode is enabled
|
|
462
|
+
jarNSMapping = await collectGradleDependencies(
|
|
463
|
+
gradleCmd,
|
|
464
|
+
dirPath,
|
|
465
|
+
false,
|
|
466
|
+
true,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
253
469
|
if (jarNSMapping) {
|
|
254
470
|
for (const purl of Object.keys(jarNSMapping)) {
|
|
255
471
|
purlsJars[purl] = jarNSMapping[purl].jarFile;
|
|
@@ -429,7 +645,9 @@ export async function createSlice(
|
|
|
429
645
|
resolve(slicesFile),
|
|
430
646
|
options,
|
|
431
647
|
);
|
|
432
|
-
|
|
648
|
+
// A stopped run has already said why; it says nothing about whether the
|
|
649
|
+
// project is supported (issue 4438).
|
|
650
|
+
if (!sliceResult && !dosaiRunWasStopped(resolve(slicesFile))) {
|
|
433
651
|
console.warn(
|
|
434
652
|
`Unable to generate ${sliceType} slice using dosai. Check if this is a supported .NET project.`,
|
|
435
653
|
);
|
|
@@ -444,6 +662,11 @@ export async function createSlice(
|
|
|
444
662
|
`Creating ${sliceType} slice for ${resolve(filePath)}. Please wait ...`,
|
|
445
663
|
);
|
|
446
664
|
const atomFile = join(sliceOutputDir, `${language}-app.atom`);
|
|
665
|
+
if (sliceType === "reachables") {
|
|
666
|
+
// atom writes reachables in numbered chunks next to the slices file; one
|
|
667
|
+
// left by an earlier run must not be read back as part of this one.
|
|
668
|
+
removeReachablesChunkFiles(slicesFile);
|
|
669
|
+
}
|
|
447
670
|
let args = [sliceType];
|
|
448
671
|
// Support for crypto slices aka CBOM
|
|
449
672
|
if (sliceType === "reachables" && options.includeCrypto) {
|
|
@@ -461,15 +684,9 @@ export async function createSlice(
|
|
|
461
684
|
"Use an official cdxgen container image to improve the precision of endpoints detection (for SaaSBOM).",
|
|
462
685
|
);
|
|
463
686
|
}
|
|
464
|
-
if (
|
|
687
|
+
if (language === "ruby") {
|
|
465
688
|
args.push("--remove-atom");
|
|
466
689
|
}
|
|
467
|
-
if (["scala"].includes(language)) {
|
|
468
|
-
semanticsSlicesFile = join(
|
|
469
|
-
sliceOutputDir,
|
|
470
|
-
basename(options.semanticsSlicesFile || "semantics.slices.json"),
|
|
471
|
-
);
|
|
472
|
-
}
|
|
473
690
|
}
|
|
474
691
|
args = args.concat([
|
|
475
692
|
"-l",
|
|
@@ -488,6 +705,7 @@ export async function createSlice(
|
|
|
488
705
|
args.push("--slice-depth");
|
|
489
706
|
args.push(readEnvironmentVariable("ATOM_SLICE_DEPTH"));
|
|
490
707
|
}
|
|
708
|
+
args.push(...atomCompileCommandsArgs(filePath, language, options));
|
|
491
709
|
args.push(resolve(filePath));
|
|
492
710
|
const atomExcludeEnv = buildAtomCommandEnv(options, language);
|
|
493
711
|
// Execute atom
|
|
@@ -540,15 +758,6 @@ export async function createSlice(
|
|
|
540
758
|
`openapi spec file "${join(filePath, openapiSpecFile)}" was not generated successfully. Check if atom-tools pypi package is installed and available in PATH.`,
|
|
541
759
|
);
|
|
542
760
|
}
|
|
543
|
-
if (
|
|
544
|
-
["scala"].includes(language) &&
|
|
545
|
-
sliceType === "usages" &&
|
|
546
|
-
!safeExistsSync(semanticsSlicesFile)
|
|
547
|
-
) {
|
|
548
|
-
console.log(
|
|
549
|
-
`Semantics slices file "${semanticsSlicesFile}" was not generated successfully. Try running atom cli in Java mode.`,
|
|
550
|
-
);
|
|
551
|
-
}
|
|
552
761
|
return {
|
|
553
762
|
tempDir: sliceOutputDir,
|
|
554
763
|
tempDirOwned,
|
|
@@ -598,8 +807,9 @@ export function purlToLanguage(purl, filePath) {
|
|
|
598
807
|
* Seed purl-location and import maps from SBOM component evidence.
|
|
599
808
|
*
|
|
600
809
|
* Walks the supplied components reading `internal:ImportedModules` (or
|
|
601
|
-
* `internal:Namespaces` for php/ruby
|
|
602
|
-
*
|
|
810
|
+
* `internal:Namespaces` for php/ruby, plus the `internal:ImportedSymbols` the
|
|
811
|
+
* C/C++ collector records) and `evidence.occurrences` properties to construct
|
|
812
|
+
* lookup maps used during slice analysis.
|
|
603
813
|
*
|
|
604
814
|
* @param {Object[]} components CycloneDX components from the input SBOM
|
|
605
815
|
* @param {string} language Application language used to select the import property name
|
|
@@ -624,6 +834,18 @@ export function initFromSbom(components, language) {
|
|
|
624
834
|
.forEach((v) => {
|
|
625
835
|
purlImportsMap[comp.purl] = (v.value || "").split(",");
|
|
626
836
|
});
|
|
837
|
+
// A C/C++ component records the symbols the project uses from it, which
|
|
838
|
+
// are what C usages are matched against. The list is pipe-separated:
|
|
839
|
+
// C++ symbols may contain commas.
|
|
840
|
+
if (["c", "cpp", "c++"].includes(language)) {
|
|
841
|
+
(comp.properties || [])
|
|
842
|
+
.filter((v) => v.name === "internal:ImportedSymbols")
|
|
843
|
+
.forEach((v) => {
|
|
844
|
+
purlImportsMap[comp.purl] = (
|
|
845
|
+
purlImportsMap[comp.purl] || []
|
|
846
|
+
).concat((v.value || "").split("|").filter(Boolean));
|
|
847
|
+
});
|
|
848
|
+
}
|
|
627
849
|
}
|
|
628
850
|
if (comp.evidence.occurrences) {
|
|
629
851
|
// Keep the `location#line` shape so a re-run over a cdxgen BOM does not
|
|
@@ -654,6 +876,273 @@ function usableSlicesFile(slicesFile) {
|
|
|
654
876
|
return fileSizeInBytes > 1024;
|
|
655
877
|
}
|
|
656
878
|
|
|
879
|
+
/**
|
|
880
|
+
* Load a semantics slices file for reuse when it belongs to this project.
|
|
881
|
+
*
|
|
882
|
+
* `--semantics-slices-file` defaults to `semantics.slices.json` in the working
|
|
883
|
+
* directory, which is shared by every project scanned from there and also
|
|
884
|
+
* receives the reports of other analyzers (dosai, rusi, golem). A file is
|
|
885
|
+
* only reused when its shape matches the language. Swift slices must also
|
|
886
|
+
* describe the same project directory and must not predate the input SBOM:
|
|
887
|
+
* `cdxgen --evidence` writes a fresh SBOM before evinse runs, so an older
|
|
888
|
+
* slice was made from an earlier state of the sources and is regenerated.
|
|
889
|
+
* Scala reports are handled by the scalasem analyzer, which applies its own
|
|
890
|
+
* rules.
|
|
891
|
+
*
|
|
892
|
+
* @param {string} language Project language
|
|
893
|
+
* @param {string} slicesFile Candidate semantics slices file
|
|
894
|
+
* @param {string} bomFile Input SBOM
|
|
895
|
+
* @param {string} dirPath Project directory
|
|
896
|
+
* @returns {undefined|Object} Parsed slice when it can be reused
|
|
897
|
+
*/
|
|
898
|
+
export function loadReusableSemanticsSlice(
|
|
899
|
+
language,
|
|
900
|
+
slicesFile,
|
|
901
|
+
bomFile,
|
|
902
|
+
dirPath,
|
|
903
|
+
) {
|
|
904
|
+
if (!usableSlicesFile(slicesFile)) {
|
|
905
|
+
return undefined;
|
|
906
|
+
}
|
|
907
|
+
let parsed;
|
|
908
|
+
try {
|
|
909
|
+
parsed = readJsonFile(slicesFile);
|
|
910
|
+
} catch (_e) {
|
|
911
|
+
return undefined;
|
|
912
|
+
}
|
|
913
|
+
if (!parsed || typeof parsed !== "object") {
|
|
914
|
+
return undefined;
|
|
915
|
+
}
|
|
916
|
+
if (!PROJECT_TYPE_ALIASES.swift.includes(language)) {
|
|
917
|
+
return undefined;
|
|
918
|
+
}
|
|
919
|
+
if (!(parsed.fileIndexes || parsed.buildSymbols || parsed.moduleInfos)) {
|
|
920
|
+
return undefined;
|
|
921
|
+
}
|
|
922
|
+
if (parsed.projectPath && resolve(parsed.projectPath) !== resolve(dirPath)) {
|
|
923
|
+
return undefined;
|
|
924
|
+
}
|
|
925
|
+
try {
|
|
926
|
+
if (fs.statSync(slicesFile).mtimeMs < fs.statSync(bomFile).mtimeMs) {
|
|
927
|
+
return undefined;
|
|
928
|
+
}
|
|
929
|
+
} catch (_e) {
|
|
930
|
+
// Without timestamps the shape and project checks decide
|
|
931
|
+
}
|
|
932
|
+
return parsed;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* The Scala evidence path: scalasem only, never atom. The branch returns
|
|
937
|
+
* early for every entry point (evinse, --evidence, cbom, saasbom) and every
|
|
938
|
+
* Scala project type, so no usages, reachables, data-flow or atom-run
|
|
939
|
+
* scalasem happens here. Slices the user passes are still read: a version 1
|
|
940
|
+
* semantics slice keeps its usedTypes join, usages and reachables slices
|
|
941
|
+
* keep their own evidence, and an OpenAPI spec keeps its services.
|
|
942
|
+
*
|
|
943
|
+
* @param {string} language Project language, normalized to `scala`.
|
|
944
|
+
* @param {string} dirPath Project directory.
|
|
945
|
+
* @param {Object} options Command line options.
|
|
946
|
+
* @param {Object} bomJson Parsed input SBOM.
|
|
947
|
+
* @param {Object[]} components Components of the input SBOM.
|
|
948
|
+
* @param {Object} purlLocationMap Occurrences seeded from the input SBOM.
|
|
949
|
+
* @param {Object} purlImportsMap Imports seeded from the input SBOM.
|
|
950
|
+
* @param {Object} dbObjMap The evinse database objects.
|
|
951
|
+
* @returns {Promise<Object>} Slice artefacts for `createEvinseFile`.
|
|
952
|
+
*/
|
|
953
|
+
async function analyzeScalaEvidenceProject(
|
|
954
|
+
language,
|
|
955
|
+
dirPath,
|
|
956
|
+
options,
|
|
957
|
+
bomJson,
|
|
958
|
+
components,
|
|
959
|
+
purlLocationMap,
|
|
960
|
+
purlImportsMap,
|
|
961
|
+
dbObjMap,
|
|
962
|
+
) {
|
|
963
|
+
let servicesMap = {};
|
|
964
|
+
const dataFlowFrames = {};
|
|
965
|
+
let componentPropertiesMap = {};
|
|
966
|
+
let metadataProperties = [];
|
|
967
|
+
let cryptoComponents = [];
|
|
968
|
+
const cryptoGeneratePurls = {};
|
|
969
|
+
const mergeLocations = (locationMap) => {
|
|
970
|
+
for (const [purl, locations] of Object.entries(locationMap || {})) {
|
|
971
|
+
purlLocationMap[purl] ??= new Set();
|
|
972
|
+
for (const location of locations) {
|
|
973
|
+
purlLocationMap[purl].add(location);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
const mergeFrames = (framesMap) => {
|
|
978
|
+
for (const [purl, stacks] of Object.entries(framesMap || {})) {
|
|
979
|
+
dataFlowFrames[purl] = [...(dataFlowFrames[purl] || []), ...stacks];
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
const mergeProvides = (providesMap) => {
|
|
983
|
+
for (const [purl, refs] of Object.entries(providesMap || {})) {
|
|
984
|
+
cryptoGeneratePurls[purl] ??= new Set();
|
|
985
|
+
for (const ref of refs || []) {
|
|
986
|
+
cryptoGeneratePurls[purl].add(ref);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
const analysis = analyzeScalaProject(dirPath, options);
|
|
991
|
+
if (analysis?.report) {
|
|
992
|
+
const evidence = await collectScalasemEvidence(
|
|
993
|
+
analysis.report,
|
|
994
|
+
[bomJson.metadata?.component, ...components].filter(Boolean),
|
|
995
|
+
{ ...options, scalaNamespaceMap: readNamespaceMap(dirPath, options) },
|
|
996
|
+
);
|
|
997
|
+
mergeLocations(evidence.purlLocationMap);
|
|
998
|
+
mergeFrames(evidence.dataFlowFrames);
|
|
999
|
+
mergeProvides(evidence.cryptoGeneratePurls);
|
|
1000
|
+
componentPropertiesMap = evidence.componentPropertiesMap;
|
|
1001
|
+
cryptoComponents = evidence.cryptoComponents;
|
|
1002
|
+
metadataProperties = evidence.metadataProperties;
|
|
1003
|
+
servicesMap = collectScalasemServices(analysis.report, servicesMap);
|
|
1004
|
+
servicesMap = collectScalasemApiEndpoints(analysis.report, servicesMap);
|
|
1005
|
+
}
|
|
1006
|
+
if (analysis?.metadataProperties?.length) {
|
|
1007
|
+
for (const property of analysis.metadataProperties) {
|
|
1008
|
+
if (
|
|
1009
|
+
!metadataProperties.some(
|
|
1010
|
+
(known) =>
|
|
1011
|
+
known.name === property.name && known.value === property.value,
|
|
1012
|
+
)
|
|
1013
|
+
) {
|
|
1014
|
+
metadataProperties.push(property);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
// A version 1 semantics slice still goes through the usedTypes join, merged
|
|
1019
|
+
// with the report evidence rather than replacing it.
|
|
1020
|
+
if (analysis?.v1Slice) {
|
|
1021
|
+
mergeLocations(
|
|
1022
|
+
findPurlLocations(components, analysis.v1Slice).purlLocationMap,
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
// Usages and reachables slices the user made, with atom or otherwise, are
|
|
1026
|
+
// read as they are for any other language; none is ever generated here.
|
|
1027
|
+
let usagesSlicesFile;
|
|
1028
|
+
let reachablesSlicesFile;
|
|
1029
|
+
if (
|
|
1030
|
+
(options.withReachables || options.includeCrypto) &&
|
|
1031
|
+
usableSlicesFile(options.reachablesSlicesFile)
|
|
1032
|
+
) {
|
|
1033
|
+
reachablesSlicesFile = options.reachablesSlicesFile;
|
|
1034
|
+
const reachables = collectReachableFrames(
|
|
1035
|
+
language,
|
|
1036
|
+
filterAtomSliceData(readReachablesSlices(reachablesSlicesFile), options),
|
|
1037
|
+
);
|
|
1038
|
+
mergeFrames(reachables.dataFlowFrames);
|
|
1039
|
+
mergeProvides(reachables.cryptoGeneratePurls);
|
|
1040
|
+
const knownRefs = new Set(
|
|
1041
|
+
cryptoComponents.map((asset) => asset["bom-ref"]),
|
|
1042
|
+
);
|
|
1043
|
+
cryptoComponents = cryptoComponents.concat(
|
|
1044
|
+
(reachables.cryptoComponents || []).filter(
|
|
1045
|
+
(asset) => !knownRefs.has(asset["bom-ref"]),
|
|
1046
|
+
),
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
let userDefinedTypesMap = {};
|
|
1050
|
+
if (usableSlicesFile(options.usagesSlicesFile)) {
|
|
1051
|
+
usagesSlicesFile = options.usagesSlicesFile;
|
|
1052
|
+
const usageSlice = filterAtomSliceData(
|
|
1053
|
+
readSlicesFile(usagesSlicesFile, "usages"),
|
|
1054
|
+
options,
|
|
1055
|
+
);
|
|
1056
|
+
if (usageSlice && Object.keys(usageSlice).length) {
|
|
1057
|
+
const retMap = await parseObjectSlices(
|
|
1058
|
+
language,
|
|
1059
|
+
usageSlice,
|
|
1060
|
+
dbObjMap,
|
|
1061
|
+
servicesMap,
|
|
1062
|
+
purlLocationMap,
|
|
1063
|
+
purlImportsMap,
|
|
1064
|
+
);
|
|
1065
|
+
servicesMap = retMap.servicesMap || servicesMap;
|
|
1066
|
+
userDefinedTypesMap = retMap.userDefinedTypesMap || {};
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
// An OpenAPI spec the user has (atom-tools' conversion of this report, or
|
|
1070
|
+
// the project's own) adds its routes to the report's: a route both name is
|
|
1071
|
+
// one service.
|
|
1072
|
+
const openapiSpecFile = options.openapiSpecFile
|
|
1073
|
+
? resolve(dirPath, options.openapiSpecFile)
|
|
1074
|
+
: undefined;
|
|
1075
|
+
if (openapiSpecFile && safeExistsSync(openapiSpecFile)) {
|
|
1076
|
+
const openapiServices = {};
|
|
1077
|
+
detectServicesFromOpenAPI(language, openapiSpecFile, openapiServices);
|
|
1078
|
+
mergeServiceDefinitions(servicesMap, openapiServices);
|
|
1079
|
+
}
|
|
1080
|
+
// A BOM without a project component still needs somewhere for the run's
|
|
1081
|
+
// metadata properties to live; a nameless entry would explain nothing.
|
|
1082
|
+
let workspaceComponent;
|
|
1083
|
+
if (!bomJson.metadata?.component) {
|
|
1084
|
+
workspaceComponent = {
|
|
1085
|
+
type: "application",
|
|
1086
|
+
name: basename(resolve(dirPath)),
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
return {
|
|
1090
|
+
purlLocationMap,
|
|
1091
|
+
servicesMap,
|
|
1092
|
+
dataFlowFrames,
|
|
1093
|
+
componentPropertiesMap,
|
|
1094
|
+
metadataProperties,
|
|
1095
|
+
cryptoComponents,
|
|
1096
|
+
cryptoGeneratePurls,
|
|
1097
|
+
usagesSlicesFile,
|
|
1098
|
+
reachablesSlicesFile,
|
|
1099
|
+
semanticsSlicesFile: analysis?.reportFile,
|
|
1100
|
+
userDefinedTypesMap,
|
|
1101
|
+
workspaceComponent,
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* Merge service definitions into a services map: endpoints join, and a
|
|
1107
|
+
* property another source already carries is not added again, whatever the
|
|
1108
|
+
* case of an HTTP method.
|
|
1109
|
+
*
|
|
1110
|
+
* @param {Object} target Services map that receives the definitions.
|
|
1111
|
+
* @param {Object} source Services map to merge in.
|
|
1112
|
+
* @returns {Object} The target map.
|
|
1113
|
+
*/
|
|
1114
|
+
export function mergeServiceDefinitions(target, source) {
|
|
1115
|
+
const sameProperty = (left, right) =>
|
|
1116
|
+
left.name === right.name &&
|
|
1117
|
+
(left.name === "cdx:service:httpMethod"
|
|
1118
|
+
? String(left.value).toUpperCase() === String(right.value).toUpperCase()
|
|
1119
|
+
: left.value === right.value);
|
|
1120
|
+
for (const [name, definition] of Object.entries(source || {})) {
|
|
1121
|
+
const existing = target[name];
|
|
1122
|
+
if (!existing) {
|
|
1123
|
+
target[name] = definition;
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
existing.endpoints ??= new Set();
|
|
1127
|
+
for (const endpoint of definition.endpoints || []) {
|
|
1128
|
+
existing.endpoints.add(endpoint);
|
|
1129
|
+
}
|
|
1130
|
+
existing.properties ??= [];
|
|
1131
|
+
for (const property of definition.properties || []) {
|
|
1132
|
+
if (!existing.properties.some((known) => sameProperty(known, property))) {
|
|
1133
|
+
existing.properties.push(property);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (definition.authenticated) {
|
|
1137
|
+
existing.authenticated = true;
|
|
1138
|
+
}
|
|
1139
|
+
if (definition.xTrustBoundary) {
|
|
1140
|
+
existing.xTrustBoundary = true;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return target;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
657
1146
|
/**
|
|
658
1147
|
* Function to analyze the project
|
|
659
1148
|
*
|
|
@@ -663,7 +1152,12 @@ function usableSlicesFile(slicesFile) {
|
|
|
663
1152
|
export async function analyzeProject(dbObjMap, options) {
|
|
664
1153
|
const dirPath = options._[0] || ".";
|
|
665
1154
|
const languages = options.language;
|
|
666
|
-
|
|
1155
|
+
let language = Array.isArray(languages) ? languages[0] : languages;
|
|
1156
|
+
// cdxgen --evidence forwards the raw project type, so -t sbt, -t mill and
|
|
1157
|
+
// -t scala3 must be normalized once to behave like -l scala everywhere.
|
|
1158
|
+
if (PROJECT_TYPE_ALIASES.scala.includes(language)) {
|
|
1159
|
+
language = "scala";
|
|
1160
|
+
}
|
|
667
1161
|
let usageSlice;
|
|
668
1162
|
let dataFlowSlice;
|
|
669
1163
|
let reachablesSlice;
|
|
@@ -817,6 +1311,19 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
817
1311
|
};
|
|
818
1312
|
}
|
|
819
1313
|
|
|
1314
|
+
if (isScalasemLanguage(language)) {
|
|
1315
|
+
return analyzeScalaEvidenceProject(
|
|
1316
|
+
language,
|
|
1317
|
+
dirPath,
|
|
1318
|
+
options,
|
|
1319
|
+
bomJson,
|
|
1320
|
+
components,
|
|
1321
|
+
purlLocationMap,
|
|
1322
|
+
purlImportsMap,
|
|
1323
|
+
dbObjMap,
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
820
1327
|
if (isDosaiDotnetLanguage(language)) {
|
|
821
1328
|
if (options.profile === "research") {
|
|
822
1329
|
options.withDataFlow = true;
|
|
@@ -826,23 +1333,38 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
826
1333
|
options.usagesSlicesFile &&
|
|
827
1334
|
usableSlicesFile(options.usagesSlicesFile)
|
|
828
1335
|
) {
|
|
829
|
-
usageSlice =
|
|
830
|
-
fs.readFileSync(options.usagesSlicesFile, "utf-8"),
|
|
831
|
-
);
|
|
1336
|
+
usageSlice = readDosaiMethodsReport(options.usagesSlicesFile);
|
|
832
1337
|
usagesSlicesFile = options.usagesSlicesFile;
|
|
1338
|
+
} else if (usableSlicesFile(options.depsSlicesFile)) {
|
|
1339
|
+
// createCsharpBom already ran dosai methods on this tree for --deep runs
|
|
1340
|
+
// and wrote the deps slice. For dosai, that output IS the usages slice,
|
|
1341
|
+
// so reuse it instead of invoking dosai a second time.
|
|
1342
|
+
if (DEBUG_MODE) {
|
|
1343
|
+
console.log(
|
|
1344
|
+
`Reusing the dosai deps slices file "${options.depsSlicesFile}" for the usages analysis.`,
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
usageSlice = readDosaiMethodsReport(options.depsSlicesFile);
|
|
1348
|
+
usagesSlicesFile = options.depsSlicesFile;
|
|
833
1349
|
} else {
|
|
834
1350
|
retMap = await createSlice(language, dirPath, "usages", options);
|
|
835
1351
|
if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
|
|
836
|
-
usageSlice =
|
|
1352
|
+
usageSlice = readDosaiMethodsReport(retMap.slicesFile);
|
|
837
1353
|
usagesSlicesFile = retMap.slicesFile;
|
|
838
1354
|
}
|
|
839
1355
|
}
|
|
840
1356
|
if (usageSlice && Object.keys(usageSlice).length) {
|
|
841
|
-
|
|
1357
|
+
// dosai locations are relative to the analyzed directory; a package the
|
|
1358
|
+
// BOM holds in several versions is matched by the project of the file.
|
|
1359
|
+
const dosaiEvidence = collectDosaiPurlEvidence(usageSlice, components, {
|
|
1360
|
+
srcPath: resolve(dirPath),
|
|
1361
|
+
});
|
|
842
1362
|
// Confidence/evidence facts ride along as properties so an unbuilt tree (Low
|
|
843
1363
|
// confidence, unresolved evidence) stays distinguishable from an only-imported
|
|
844
1364
|
// package in the emitted BOM.
|
|
845
|
-
applyDosaiReachabilityEvidence(usageSlice, components
|
|
1365
|
+
applyDosaiReachabilityEvidence(usageSlice, components, {
|
|
1366
|
+
srcPath: resolve(dirPath),
|
|
1367
|
+
});
|
|
846
1368
|
for (const [purl, locations] of Object.entries(
|
|
847
1369
|
dosaiEvidence.purlLocationMap,
|
|
848
1370
|
)) {
|
|
@@ -867,20 +1389,18 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
867
1389
|
safeExistsSync(options.dataFlowSlicesFile)
|
|
868
1390
|
) {
|
|
869
1391
|
dataFlowSlicesFile = options.dataFlowSlicesFile;
|
|
870
|
-
dataFlowSlice =
|
|
871
|
-
fs.readFileSync(options.dataFlowSlicesFile, "utf-8"),
|
|
872
|
-
);
|
|
1392
|
+
dataFlowSlice = readDosaiDataFlowReport(options.dataFlowSlicesFile);
|
|
873
1393
|
} else {
|
|
874
1394
|
retMap = await createSlice(language, dirPath, "data-flow", options);
|
|
875
1395
|
if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
|
|
876
1396
|
dataFlowSlicesFile = retMap.slicesFile;
|
|
877
|
-
dataFlowSlice =
|
|
878
|
-
fs.readFileSync(retMap.slicesFile, "utf-8"),
|
|
879
|
-
);
|
|
1397
|
+
dataFlowSlice = readDosaiDataFlowReport(retMap.slicesFile);
|
|
880
1398
|
}
|
|
881
1399
|
}
|
|
882
1400
|
if (dataFlowSlice && Object.keys(dataFlowSlice).length) {
|
|
883
|
-
dataFlowFrames = collectDosaiDataFlowFrames(dataFlowSlice, components
|
|
1401
|
+
dataFlowFrames = collectDosaiDataFlowFrames(dataFlowSlice, components, {
|
|
1402
|
+
srcPath: resolve(dirPath),
|
|
1403
|
+
});
|
|
884
1404
|
}
|
|
885
1405
|
}
|
|
886
1406
|
if (options.includeCrypto) {
|
|
@@ -923,7 +1443,7 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
923
1443
|
) {
|
|
924
1444
|
reachablesSlicesFile = options.reachablesSlicesFile;
|
|
925
1445
|
reachablesSlice = filterAtomSliceData(
|
|
926
|
-
|
|
1446
|
+
readReachablesSlices(options.reachablesSlicesFile),
|
|
927
1447
|
options,
|
|
928
1448
|
);
|
|
929
1449
|
} else {
|
|
@@ -931,7 +1451,7 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
931
1451
|
if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
|
|
932
1452
|
reachablesSlicesFile = retMap.slicesFile;
|
|
933
1453
|
reachablesSlice = filterAtomSliceData(
|
|
934
|
-
|
|
1454
|
+
readReachablesSlices(retMap.slicesFile),
|
|
935
1455
|
options,
|
|
936
1456
|
);
|
|
937
1457
|
}
|
|
@@ -947,7 +1467,7 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
947
1467
|
// Reuse existing usages slices
|
|
948
1468
|
if (options.usagesSlicesFile && usableSlicesFile(options.usagesSlicesFile)) {
|
|
949
1469
|
usageSlice = filterAtomSliceData(
|
|
950
|
-
|
|
1470
|
+
readSlicesFile(options.usagesSlicesFile, "usages"),
|
|
951
1471
|
options,
|
|
952
1472
|
);
|
|
953
1473
|
usagesSlicesFile = options.usagesSlicesFile;
|
|
@@ -956,7 +1476,7 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
956
1476
|
retMap = await createSlice(language, dirPath, "usages", options);
|
|
957
1477
|
if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
|
|
958
1478
|
usageSlice = filterAtomSliceData(
|
|
959
|
-
|
|
1479
|
+
readSlicesFile(retMap.slicesFile, "usages"),
|
|
960
1480
|
options,
|
|
961
1481
|
);
|
|
962
1482
|
usagesSlicesFile = retMap.slicesFile;
|
|
@@ -976,31 +1496,48 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
976
1496
|
}
|
|
977
1497
|
}
|
|
978
1498
|
// Support for semantics slicing
|
|
979
|
-
if (
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
semanticsSlice = JSON.parse(
|
|
990
|
-
fs.readFileSync(options.semanticsSlicesFile, "utf-8"),
|
|
991
|
-
);
|
|
1499
|
+
if (PROJECT_TYPE_ALIASES.swift.includes(language) && components.length) {
|
|
1500
|
+
// Reuse existing semantics slices for swift
|
|
1501
|
+
const reusableSlice = loadReusableSemanticsSlice(
|
|
1502
|
+
language,
|
|
1503
|
+
options.semanticsSlicesFile,
|
|
1504
|
+
bomFile,
|
|
1505
|
+
dirPath,
|
|
1506
|
+
);
|
|
1507
|
+
if (reusableSlice) {
|
|
1508
|
+
semanticsSlice = reusableSlice;
|
|
992
1509
|
semanticsSlicesFile = options.semanticsSlicesFile;
|
|
993
|
-
} else
|
|
1510
|
+
} else {
|
|
994
1511
|
// Generate our own slices for swift
|
|
995
1512
|
retMap = await createSlice(language, dirPath, "semantics", options);
|
|
996
1513
|
if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
|
|
997
|
-
semanticsSlice =
|
|
998
|
-
fs.readFileSync(retMap.slicesFile, "utf-8"),
|
|
999
|
-
);
|
|
1514
|
+
semanticsSlice = readSlicesFile(retMap.slicesFile, "semantics");
|
|
1000
1515
|
semanticsSlicesFile = retMap.slicesFile;
|
|
1001
1516
|
}
|
|
1002
1517
|
}
|
|
1003
1518
|
}
|
|
1519
|
+
let cIncludeLocations = new Map();
|
|
1520
|
+
// A C/C++ include names the file it resolved to and the functions the
|
|
1521
|
+
// including file calls from it (atom 4 and later): those functions are the
|
|
1522
|
+
// usages of the component that provides the file
|
|
1523
|
+
if (
|
|
1524
|
+
["c", "cpp", "c++"].includes(language) &&
|
|
1525
|
+
usageSlice &&
|
|
1526
|
+
Object.keys(usageSlice).length
|
|
1527
|
+
) {
|
|
1528
|
+
const attributed = attributeCIncludes(
|
|
1529
|
+
usageSlice,
|
|
1530
|
+
components,
|
|
1531
|
+
dirPath,
|
|
1532
|
+
options,
|
|
1533
|
+
);
|
|
1534
|
+
for (const [purl, names] of attributed.symbols) {
|
|
1535
|
+
purlImportsMap[purl] = Array.from(
|
|
1536
|
+
new Set([...(purlImportsMap[purl] || []), ...names]),
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
cIncludeLocations = attributed.locations;
|
|
1540
|
+
}
|
|
1004
1541
|
// Parse usage slices
|
|
1005
1542
|
if (usageSlice && Object.keys(usageSlice).length) {
|
|
1006
1543
|
const retMap = await parseObjectSlices(
|
|
@@ -1016,7 +1553,14 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
1016
1553
|
servicesMap = retMap.servicesMap;
|
|
1017
1554
|
userDefinedTypesMap = retMap.userDefinedTypesMap;
|
|
1018
1555
|
}
|
|
1019
|
-
//
|
|
1556
|
+
// A component whose calls the slices do not show (code inside the project,
|
|
1557
|
+
// such as a vendored library) is still used where its header is included
|
|
1558
|
+
for (const [purl, includeLocations] of cIncludeLocations) {
|
|
1559
|
+
if (!purlLocationMap[purl]?.size) {
|
|
1560
|
+
purlLocationMap[purl] = new Set(includeLocations);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
// Parse the semantics slices for swift
|
|
1020
1564
|
if (
|
|
1021
1565
|
semanticsSlice &&
|
|
1022
1566
|
Object.keys(semanticsSlice).length &&
|
|
@@ -1024,7 +1568,25 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
1024
1568
|
) {
|
|
1025
1569
|
// Identify the purl locations
|
|
1026
1570
|
const retMap = parseSemanticSlices(language, components, semanticsSlice);
|
|
1027
|
-
|
|
1571
|
+
// The usages step may already have locations for a purl; the semantics
|
|
1572
|
+
// pass adds its own rather than replacing them.
|
|
1573
|
+
for (const [apurl, locations] of Object.entries(
|
|
1574
|
+
retMap.purlLocationMap || {},
|
|
1575
|
+
)) {
|
|
1576
|
+
purlLocationMap[apurl] ??= new Set();
|
|
1577
|
+
for (const alocation of locations) {
|
|
1578
|
+
purlLocationMap[apurl].add(alocation);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
// A user-supplied OpenAPI spec must yield services even when the usages
|
|
1583
|
+
// slice is empty.
|
|
1584
|
+
if (
|
|
1585
|
+
openapiSpecFile &&
|
|
1586
|
+
safeExistsSync(openapiSpecFile) &&
|
|
1587
|
+
!Object.keys(servicesMap).length
|
|
1588
|
+
) {
|
|
1589
|
+
detectServicesFromOpenAPI(language, openapiSpecFile, servicesMap);
|
|
1028
1590
|
}
|
|
1029
1591
|
if (options.withDataFlow) {
|
|
1030
1592
|
if (
|
|
@@ -1033,7 +1595,7 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
1033
1595
|
) {
|
|
1034
1596
|
dataFlowSlicesFile = options.dataFlowSlicesFile;
|
|
1035
1597
|
dataFlowSlice = filterAtomSliceData(
|
|
1036
|
-
|
|
1598
|
+
readSlicesFile(options.dataFlowSlicesFile, "data-flow"),
|
|
1037
1599
|
options,
|
|
1038
1600
|
);
|
|
1039
1601
|
} else if (!PROJECT_TYPE_ALIASES.scala.includes(language)) {
|
|
@@ -1041,7 +1603,7 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
1041
1603
|
if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
|
|
1042
1604
|
dataFlowSlicesFile = retMap.slicesFile;
|
|
1043
1605
|
dataFlowSlice = filterAtomSliceData(
|
|
1044
|
-
|
|
1606
|
+
readSlicesFile(retMap.slicesFile, "data-flow"),
|
|
1045
1607
|
options,
|
|
1046
1608
|
);
|
|
1047
1609
|
}
|
|
@@ -1093,6 +1655,125 @@ export async function analyzeProject(dbObjMap, options) {
|
|
|
1093
1655
|
* @param {string} [openapiSpecFile=undefined] Optional path to an OpenAPI spec used for service detection
|
|
1094
1656
|
* @returns {Promise<Object>} Map with `purlLocationMap`, `servicesMap`, and `userDefinedTypesMap`
|
|
1095
1657
|
*/
|
|
1658
|
+
/**
|
|
1659
|
+
* Attribute the include slices of a C/C++ usages report to the BOM
|
|
1660
|
+
* components that provide the files they resolved to: each header's imported
|
|
1661
|
+
* symbols (the functions the including file calls that it declares), and the
|
|
1662
|
+
* include directive's location. Reports from atom releases that do not name
|
|
1663
|
+
* the resolved file attribute nothing.
|
|
1664
|
+
*
|
|
1665
|
+
* @param {Object} usageSlice Parsed usages report
|
|
1666
|
+
* @param {Object[]} components Components of the input BOM
|
|
1667
|
+
* @param {string} dirPath Source directory
|
|
1668
|
+
* @param {Object} options CLI options
|
|
1669
|
+
* @returns {{symbols: Map<string, Set<string>>, locations: Map<string, Set<string>>}}
|
|
1670
|
+
* Imported symbols and include locations (`file#line`) by purl
|
|
1671
|
+
*/
|
|
1672
|
+
export function attributeCIncludes(
|
|
1673
|
+
usageSlice,
|
|
1674
|
+
components,
|
|
1675
|
+
dirPath,
|
|
1676
|
+
options = {},
|
|
1677
|
+
) {
|
|
1678
|
+
const symbols = new Map();
|
|
1679
|
+
const locations = new Map();
|
|
1680
|
+
const includes = [...parseCIncludeSlices(usageSlice)].filter(
|
|
1681
|
+
([, e]) => e.paths.size && e.symbols.size,
|
|
1682
|
+
);
|
|
1683
|
+
if (!includes.length || !components?.length) {
|
|
1684
|
+
return { symbols, locations };
|
|
1685
|
+
}
|
|
1686
|
+
const attribute = createIncludeAttributor({
|
|
1687
|
+
src: dirPath,
|
|
1688
|
+
components,
|
|
1689
|
+
buildDirs: cmakeBuildDirCandidates(dirPath, options),
|
|
1690
|
+
});
|
|
1691
|
+
const withPurl = components.filter((c) => c?.purl);
|
|
1692
|
+
const byName = (name, purlType) =>
|
|
1693
|
+
withPurl.find(
|
|
1694
|
+
(c) =>
|
|
1695
|
+
c.name?.toLowerCase() === `${name}`.toLowerCase() &&
|
|
1696
|
+
(!purlType || c.purl.startsWith(`pkg:${purlType}/`)),
|
|
1697
|
+
);
|
|
1698
|
+
const purlByHeader = new Map();
|
|
1699
|
+
for (const [header, entry] of includes) {
|
|
1700
|
+
const target = attribute(entry.paths);
|
|
1701
|
+
let component;
|
|
1702
|
+
if (target?.kind === "component") {
|
|
1703
|
+
component = target.component;
|
|
1704
|
+
} else if (target?.kind === "os") {
|
|
1705
|
+
component =
|
|
1706
|
+
withPurl.find((c) => c.purl === target.pkgInfo.purl) ||
|
|
1707
|
+
byName(target.pkgInfo.name);
|
|
1708
|
+
} else if (target?.kind === "vcpkg") {
|
|
1709
|
+
component = byName(target.port);
|
|
1710
|
+
} else if (target?.kind === "conan") {
|
|
1711
|
+
component = byName(target.name, "conan") || byName(target.name);
|
|
1712
|
+
}
|
|
1713
|
+
if (!component?.purl) {
|
|
1714
|
+
continue;
|
|
1715
|
+
}
|
|
1716
|
+
purlByHeader.set(header, component.purl);
|
|
1717
|
+
const known = symbols.get(component.purl) || new Set();
|
|
1718
|
+
for (const symbol of entry.symbols) {
|
|
1719
|
+
known.add(symbol);
|
|
1720
|
+
}
|
|
1721
|
+
symbols.set(component.purl, known);
|
|
1722
|
+
}
|
|
1723
|
+
// the directives that include an attributed header and use it
|
|
1724
|
+
for (const slice of usageSlice?.objectSlices || []) {
|
|
1725
|
+
const purl = purlByHeader.get(slice?.fullName);
|
|
1726
|
+
if (
|
|
1727
|
+
!purl ||
|
|
1728
|
+
!`${slice.code || ""}`.startsWith("#include") ||
|
|
1729
|
+
!slice.fileName ||
|
|
1730
|
+
!slice.importedSymbols?.length
|
|
1731
|
+
) {
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1734
|
+
const known = locations.get(purl) || new Set();
|
|
1735
|
+
known.add(
|
|
1736
|
+
slice.lineNumber
|
|
1737
|
+
? `${slice.fileName}#${slice.lineNumber}`
|
|
1738
|
+
: slice.fileName,
|
|
1739
|
+
);
|
|
1740
|
+
locations.set(purl, known);
|
|
1741
|
+
}
|
|
1742
|
+
return { symbols, locations };
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
/**
|
|
1746
|
+
* Seed the C/C++ imports from the include slices of a usages report (see
|
|
1747
|
+
* `attributeCIncludes`).
|
|
1748
|
+
*
|
|
1749
|
+
* @param {Object} usageSlice Parsed usages report
|
|
1750
|
+
* @param {Object[]} components Components of the input BOM
|
|
1751
|
+
* @param {string} dirPath Source directory
|
|
1752
|
+
* @param {Object} purlImportsMap Imports by purl, extended in place
|
|
1753
|
+
* @param {Object} options CLI options
|
|
1754
|
+
* @returns {Object} purlImportsMap
|
|
1755
|
+
*/
|
|
1756
|
+
export function seedCImportsFromIncludes(
|
|
1757
|
+
usageSlice,
|
|
1758
|
+
components,
|
|
1759
|
+
dirPath,
|
|
1760
|
+
purlImportsMap = {},
|
|
1761
|
+
options = {},
|
|
1762
|
+
) {
|
|
1763
|
+
const { symbols } = attributeCIncludes(
|
|
1764
|
+
usageSlice,
|
|
1765
|
+
components,
|
|
1766
|
+
dirPath,
|
|
1767
|
+
options,
|
|
1768
|
+
);
|
|
1769
|
+
for (const [purl, names] of symbols) {
|
|
1770
|
+
purlImportsMap[purl] = Array.from(
|
|
1771
|
+
new Set([...(purlImportsMap[purl] || []), ...names]),
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1774
|
+
return purlImportsMap;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1096
1777
|
export async function parseObjectSlices(
|
|
1097
1778
|
language,
|
|
1098
1779
|
usageSlice,
|
|
@@ -1400,99 +2081,64 @@ export async function parseSliceUsages(
|
|
|
1400
2081
|
/**
|
|
1401
2082
|
* Method to parse semantic slice data. Currently supported for swift and scala languages.
|
|
1402
2083
|
*
|
|
2084
|
+
* Swift slices created by cdxgen record, for every indexed source file, the
|
|
2085
|
+
* lines that reference each dependency module (`moduleReferences`); the module
|
|
2086
|
+
* of a reference is the declaring module recovered from the compiler-resolved
|
|
2087
|
+
* USR. Components are mapped to their package's modules through the
|
|
2088
|
+
* `packages` slice, so every occurrence is backed by a resolved reference.
|
|
2089
|
+
* Slices without `moduleReferences` (created by older cdxgen versions) fall
|
|
2090
|
+
* back to matching symbol names.
|
|
2091
|
+
*
|
|
1403
2092
|
* @param {String} language Project language.
|
|
1404
2093
|
* @param {Array} components Components from the input SBOM
|
|
1405
2094
|
* @param {Object} semanticsSlice Semantic slice data
|
|
1406
2095
|
* @returns {Object} Parsed metadata
|
|
1407
2096
|
*/
|
|
1408
|
-
export function parseSemanticSlices(
|
|
1409
|
-
// For scala, use the dedicated scalasem module.
|
|
1410
|
-
if (language === "scala") {
|
|
1411
|
-
return findPurlLocations(components, semanticsSlice);
|
|
1412
|
-
}
|
|
2097
|
+
export function parseSemanticSlices(_language, components, semanticsSlice) {
|
|
1413
2098
|
if (!components) {
|
|
1414
2099
|
return undefined;
|
|
1415
2100
|
}
|
|
1416
|
-
const
|
|
1417
|
-
const
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
// - A granular and precise moduleInfos, which has the exact classes, protocols, enums etc belonging to each module
|
|
1422
|
-
// The objective then is to build the purlLocationMap, which is the list of locations where a given purl is used
|
|
1423
|
-
// For this, we have two attributes:
|
|
1424
|
-
// - A simpler fileStructures attribute that has the mapping between a swift file and the list of referenced types
|
|
1425
|
-
// - A granular fileIndexes attribute that contains information about the clang modules and line number information
|
|
1426
|
-
|
|
1427
|
-
// We first need to map out the component names to their purls
|
|
1428
|
-
// This is because the semantics slice use the module names everywhere
|
|
2101
|
+
const fileIndexes = semanticsSlice?.fileIndexes || {};
|
|
2102
|
+
const hasModuleReferences = Object.values(fileIndexes).some(
|
|
2103
|
+
(afileIndex) => afileIndex?.moduleReferences,
|
|
2104
|
+
);
|
|
2105
|
+
const purlLocationsSet = {};
|
|
1429
2106
|
for (const comp of components) {
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
if (semanticsSlice?.buildSymbols[comp.name]) {
|
|
1435
|
-
for (const asym of semanticsSlice.buildSymbols[comp.name]) {
|
|
1436
|
-
componentSymbolsMap[comp.name].add(asym);
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
const moduleInfo = semanticsSlice?.moduleInfos[comp.name] || {};
|
|
1440
|
-
if (moduleInfo.classes) {
|
|
1441
|
-
for (const asym of moduleInfo.classes) {
|
|
1442
|
-
componentSymbolsMap[comp.name].add(asym);
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
if (moduleInfo.protocols) {
|
|
1446
|
-
for (const asym of moduleInfo.protocols) {
|
|
1447
|
-
componentSymbolsMap[comp.name].add(asym);
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
if (moduleInfo.enums) {
|
|
1451
|
-
for (const asym of moduleInfo.enums) {
|
|
1452
|
-
componentSymbolsMap[comp.name].add(asym);
|
|
1453
|
-
}
|
|
2107
|
+
// Local packages and the root application have no purl, so there is
|
|
2108
|
+
// nothing the evidence could be attached to downstream
|
|
2109
|
+
if (!comp?.purl) {
|
|
2110
|
+
continue;
|
|
1454
2111
|
}
|
|
1455
|
-
|
|
1456
|
-
if (
|
|
1457
|
-
|
|
1458
|
-
for (const asym of moduleInfo.classMethods[aclassName]) {
|
|
1459
|
-
componentSymbolsMap[comp.name].add(asym);
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
2112
|
+
const moduleNames = resolveSwiftComponentModules(comp, semanticsSlice);
|
|
2113
|
+
if (!moduleNames.size) {
|
|
2114
|
+
continue;
|
|
1462
2115
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
2116
|
+
const locations = purlLocationsSet[comp.purl] || new Set();
|
|
2117
|
+
if (hasModuleReferences) {
|
|
2118
|
+
for (const [aswiftFile, afileIndex] of Object.entries(fileIndexes)) {
|
|
2119
|
+
for (const [amodule, lines] of Object.entries(
|
|
2120
|
+
afileIndex?.moduleReferences || {},
|
|
2121
|
+
)) {
|
|
2122
|
+
if (!moduleNames.has(amodule)) {
|
|
2123
|
+
continue;
|
|
2124
|
+
}
|
|
2125
|
+
for (const aline of lines) {
|
|
2126
|
+
locations.add(`${aswiftFile}#${aline}`);
|
|
2127
|
+
}
|
|
1467
2128
|
}
|
|
1468
2129
|
}
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
|
-
const purlLocationsSet = {};
|
|
1478
|
-
// We now have a good set of data in componentSymbolsMap and allObfuscationsMap
|
|
1479
|
-
// We can iterate these symbols and check if they exist under fileIndexes.symbolLocations
|
|
1480
|
-
for (const compName of Object.keys(componentSymbolsMap)) {
|
|
1481
|
-
const compSymbols = Array.from(componentSymbolsMap[compName]);
|
|
1482
|
-
const searchHits = searchSymbolLocations(
|
|
1483
|
-
compSymbols,
|
|
1484
|
-
semanticsSlice?.fileIndexes,
|
|
1485
|
-
);
|
|
1486
|
-
// We have an occurrence hit. Let's populate the purlLocationMap
|
|
1487
|
-
if (searchHits?.length) {
|
|
1488
|
-
const locations =
|
|
1489
|
-
purlLocationsSet[componentNamePurlMap[compName]] || new Set();
|
|
1490
|
-
for (const ahit of searchHits) {
|
|
2130
|
+
} else {
|
|
2131
|
+
for (const ahit of searchSymbolLocations(
|
|
2132
|
+
collectLegacySwiftSymbols(moduleNames, semanticsSlice),
|
|
2133
|
+
fileIndexes,
|
|
2134
|
+
)) {
|
|
1491
2135
|
for (const aline of ahit.lineNumbers) {
|
|
1492
2136
|
locations.add(`${ahit.file}#${aline}`);
|
|
1493
2137
|
}
|
|
1494
2138
|
}
|
|
1495
|
-
|
|
2139
|
+
}
|
|
2140
|
+
if (locations.size) {
|
|
2141
|
+
purlLocationsSet[comp.purl] = locations;
|
|
1496
2142
|
}
|
|
1497
2143
|
}
|
|
1498
2144
|
const purlLocationMap = {};
|
|
@@ -1502,22 +2148,184 @@ export function parseSemanticSlices(language, components, semanticsSlice) {
|
|
|
1502
2148
|
return { purlLocationMap };
|
|
1503
2149
|
}
|
|
1504
2150
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
2151
|
+
/**
|
|
2152
|
+
* Normalise a repository location or Swift purl into `host/owner/name`.
|
|
2153
|
+
*
|
|
2154
|
+
* @param {String} value Repository URL, scp-style git location, or swift purl
|
|
2155
|
+
* @returns {undefined|String} Lowercase `host/path` without scheme and `.git`
|
|
2156
|
+
*/
|
|
2157
|
+
function normalizeSwiftPackageLocation(value) {
|
|
2158
|
+
if (!value || typeof value !== "string") {
|
|
1508
2159
|
return undefined;
|
|
1509
2160
|
}
|
|
1510
|
-
|
|
1511
|
-
|
|
2161
|
+
let text = value.trim();
|
|
2162
|
+
if (text.startsWith("pkg:swift/")) {
|
|
2163
|
+
text = text.slice("pkg:swift/".length).split("?")[0].split("#")[0];
|
|
2164
|
+
const at = text.lastIndexOf("@");
|
|
2165
|
+
if (at > 0) {
|
|
2166
|
+
text = text.slice(0, at);
|
|
2167
|
+
}
|
|
2168
|
+
try {
|
|
2169
|
+
text = decodeURIComponent(text);
|
|
2170
|
+
} catch (_e) {
|
|
2171
|
+
// Keep the raw value
|
|
2172
|
+
}
|
|
2173
|
+
} else {
|
|
2174
|
+
text = text.split("?")[0].split("#")[0];
|
|
2175
|
+
const scheme = text.indexOf("://");
|
|
2176
|
+
if (scheme >= 0) {
|
|
2177
|
+
text = text.slice(scheme + 3);
|
|
2178
|
+
// Drop userinfo such as git@
|
|
2179
|
+
const slash = text.indexOf("/");
|
|
2180
|
+
const at = text.indexOf("@");
|
|
2181
|
+
if (at >= 0 && (slash < 0 || at < slash)) {
|
|
2182
|
+
text = text.slice(at + 1);
|
|
2183
|
+
}
|
|
2184
|
+
} else if (text.includes("@") && text.includes(":")) {
|
|
2185
|
+
// scp-style git@github.com:owner/name.git
|
|
2186
|
+
text = text.slice(text.indexOf("@") + 1).replace(":", "/");
|
|
2187
|
+
} else {
|
|
2188
|
+
return undefined;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
text = text.replace(/\/+$/, "");
|
|
2192
|
+
if (text.endsWith(".git")) {
|
|
2193
|
+
text = text.slice(0, -4);
|
|
2194
|
+
}
|
|
2195
|
+
if (text.startsWith("www.")) {
|
|
2196
|
+
text = text.slice(4);
|
|
2197
|
+
}
|
|
2198
|
+
return text.toLowerCase() || undefined;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
/**
|
|
2202
|
+
* Resolve the Swift module names that belong to a SBOM component.
|
|
2203
|
+
*
|
|
2204
|
+
* A component is named after its package repository or identity while the
|
|
2205
|
+
* semantic slices are keyed by module names. With the `packages` slice, a
|
|
2206
|
+
* package cloned from a repository matches the component whose purl names
|
|
2207
|
+
* the same repository, so a fork with the same name never inherits another
|
|
2208
|
+
* package's evidence; registry and local packages, which have no repository
|
|
2209
|
+
* URL, match by identity or name. Older slices only carry the
|
|
2210
|
+
* `packageModules` aliases (identity, package name, repository name), which
|
|
2211
|
+
* are matched against the component name and the `cdx:swift:packageName`
|
|
2212
|
+
* property, so that `swift-argument-parser` resolves to its `ArgumentParser`
|
|
2213
|
+
* module.
|
|
2214
|
+
*
|
|
2215
|
+
* @param {Object} comp SBOM component
|
|
2216
|
+
* @param {Object} semanticsSlice Semantic slice data
|
|
2217
|
+
* @returns {Set<string>} Module names associated with the component
|
|
2218
|
+
*/
|
|
2219
|
+
function resolveSwiftComponentModules(comp, semanticsSlice) {
|
|
2220
|
+
const moduleNames = new Set();
|
|
2221
|
+
if (!comp?.name) {
|
|
2222
|
+
return moduleNames;
|
|
2223
|
+
}
|
|
2224
|
+
const aliases = new Set(
|
|
2225
|
+
[comp.name, getPropertyValue(comp, "cdx:swift:packageName")]
|
|
2226
|
+
.filter(Boolean)
|
|
2227
|
+
.map((alias) => String(alias).toLowerCase()),
|
|
2228
|
+
);
|
|
2229
|
+
if (Array.isArray(semanticsSlice?.packages)) {
|
|
2230
|
+
const componentLocation = normalizeSwiftPackageLocation(comp.purl);
|
|
2231
|
+
for (const apkg of semanticsSlice.packages) {
|
|
2232
|
+
const packageLocation = normalizeSwiftPackageLocation(apkg?.location);
|
|
2233
|
+
const matches =
|
|
2234
|
+
packageLocation && componentLocation
|
|
2235
|
+
? packageLocation === componentLocation
|
|
2236
|
+
: [apkg?.identity, apkg?.name].some(
|
|
2237
|
+
(alias) => alias && aliases.has(String(alias).toLowerCase()),
|
|
2238
|
+
);
|
|
2239
|
+
if (matches) {
|
|
2240
|
+
for (const amodule of apkg.modules || []) {
|
|
2241
|
+
moduleNames.add(amodule);
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
return moduleNames;
|
|
2246
|
+
}
|
|
2247
|
+
const packageModules = semanticsSlice?.packageModules || {};
|
|
2248
|
+
for (const alias of new Set([comp.name, ...aliases])) {
|
|
2249
|
+
if (Array.isArray(packageModules[alias])) {
|
|
2250
|
+
for (const amodule of packageModules[alias]) {
|
|
2251
|
+
moduleNames.add(amodule);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
// Modules named after the package (Yams, SWXMLHash) still match
|
|
2256
|
+
if (!moduleNames.size) {
|
|
2257
|
+
moduleNames.add(comp.name);
|
|
2258
|
+
}
|
|
2259
|
+
return moduleNames;
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
function getPropertyValue(comp, propertyName) {
|
|
2263
|
+
for (const aproperty of comp?.properties || []) {
|
|
2264
|
+
if (aproperty?.name === propertyName) {
|
|
2265
|
+
return aproperty.value;
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
return undefined;
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
/**
|
|
2272
|
+
* Collect the symbol names of a component's modules from slices that predate
|
|
2273
|
+
* `moduleReferences`, using the moduleInfos data and the coarser buildSymbols
|
|
2274
|
+
* derived from the build's output file maps.
|
|
2275
|
+
*
|
|
2276
|
+
* @param {Set<String>} moduleNames Module names of the component
|
|
2277
|
+
* @param {Object} semanticsSlice Semantic slice data
|
|
2278
|
+
* @returns {Object} `{ symbols, freeFunctions }` name sets
|
|
2279
|
+
*/
|
|
2280
|
+
function collectLegacySwiftSymbols(moduleNames, semanticsSlice) {
|
|
2281
|
+
const symbols = new Set();
|
|
2282
|
+
const freeFunctions = new Set();
|
|
2283
|
+
for (const moduleName of moduleNames) {
|
|
2284
|
+
for (const asym of semanticsSlice?.buildSymbols?.[moduleName] || []) {
|
|
2285
|
+
symbols.add(asym);
|
|
2286
|
+
}
|
|
2287
|
+
const moduleInfo = semanticsSlice?.moduleInfos?.[moduleName] || {};
|
|
2288
|
+
for (const asym of [
|
|
2289
|
+
...(moduleInfo.classes || []),
|
|
2290
|
+
...(moduleInfo.protocols || []),
|
|
2291
|
+
...(moduleInfo.enums || []),
|
|
2292
|
+
]) {
|
|
2293
|
+
symbols.add(asym);
|
|
2294
|
+
}
|
|
2295
|
+
for (const methods of [
|
|
2296
|
+
...Object.values(moduleInfo.classMethods || {}),
|
|
2297
|
+
...Object.values(moduleInfo.protocolMethods || {}),
|
|
2298
|
+
]) {
|
|
2299
|
+
for (const asym of methods || []) {
|
|
2300
|
+
symbols.add(asym);
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
for (const asym of moduleInfo.functions || []) {
|
|
2304
|
+
freeFunctions.add(asym);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
return { symbols, freeFunctions };
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
function searchSymbolLocations(componentSymbols, fileIndexes) {
|
|
2311
|
+
const searchHits = [];
|
|
2312
|
+
const { symbols, freeFunctions } = componentSymbols;
|
|
2313
|
+
for (const aswiftFile of Object.keys(fileIndexes || {})) {
|
|
2314
|
+
const symbolLocations = fileIndexes[aswiftFile]?.symbolLocations;
|
|
2315
|
+
if (!symbolLocations) {
|
|
1512
2316
|
continue;
|
|
1513
2317
|
}
|
|
1514
|
-
for (const asym of Object.keys(
|
|
1515
|
-
|
|
1516
|
-
|
|
2318
|
+
for (const asym of Object.keys(symbolLocations)) {
|
|
2319
|
+
// The index records free functions by their full selector
|
|
2320
|
+
// (`dump(object:canonical:...)`) while module interfaces give the base
|
|
2321
|
+
// name. Only top-level functions are matched by base name: members such
|
|
2322
|
+
// as `encode(to:)` exist in nearly every module
|
|
2323
|
+
const baseName = asym.includes("(") ? asym.split("(")[0] : undefined;
|
|
2324
|
+
if (symbols.has(asym) || (baseName && freeFunctions.has(baseName))) {
|
|
1517
2325
|
searchHits.push({
|
|
1518
2326
|
file: aswiftFile,
|
|
1519
2327
|
symbol: asym,
|
|
1520
|
-
lineNumbers,
|
|
2328
|
+
lineNumbers: symbolLocations[asym],
|
|
1521
2329
|
});
|
|
1522
2330
|
}
|
|
1523
2331
|
}
|
|
@@ -1950,7 +2758,10 @@ export async function createEvinseFile(sliceArtefacts, options) {
|
|
|
1950
2758
|
bomJson.metadata.component = workspaceComponent;
|
|
1951
2759
|
}
|
|
1952
2760
|
}
|
|
1953
|
-
|
|
2761
|
+
mergeAnalyzerMetadataProperties(
|
|
2762
|
+
bomJson.metadata?.component,
|
|
2763
|
+
metadataProperties,
|
|
2764
|
+
);
|
|
1954
2765
|
let occEvidencePresent = false;
|
|
1955
2766
|
let csEvidencePresent = false;
|
|
1956
2767
|
let servicesPresent = false;
|
|
@@ -2009,8 +2820,15 @@ export async function createEvinseFile(sliceArtefacts, options) {
|
|
|
2009
2820
|
servicesPresent = true;
|
|
2010
2821
|
}
|
|
2011
2822
|
// Add the crypto components to the components list
|
|
2823
|
+
// Multi-type scans may have collected the same crypto assets during BOM
|
|
2824
|
+
// generation, so skip any bom-ref already present to keep the BOM valid.
|
|
2012
2825
|
if (cryptoComponents?.length) {
|
|
2013
|
-
|
|
2826
|
+
const existingRefs = new Set(
|
|
2827
|
+
bomJson.components.map((comp) => comp["bom-ref"]).filter(Boolean),
|
|
2828
|
+
);
|
|
2829
|
+
bomJson.components = bomJson.components.concat(
|
|
2830
|
+
cryptoComponents.filter((comp) => !existingRefs.has(comp["bom-ref"])),
|
|
2831
|
+
);
|
|
2014
2832
|
}
|
|
2015
2833
|
// Add the AI components (machine-learning-model, dataset) discovered by dosai
|
|
2016
2834
|
if (aiComponents?.length) {
|
|
@@ -2035,28 +2853,27 @@ export async function createEvinseFile(sliceArtefacts, options) {
|
|
|
2035
2853
|
bomJson.dependencies = newDependencies;
|
|
2036
2854
|
}
|
|
2037
2855
|
if (options.annotate) {
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
if (reachablesSlicesFile && safeExistsSync(reachablesSlicesFile)) {
|
|
2856
|
+
for (const slicesFile of [
|
|
2857
|
+
usagesSlicesFile,
|
|
2858
|
+
dataFlowSlicesFile,
|
|
2859
|
+
reachablesSlicesFile,
|
|
2860
|
+
]) {
|
|
2861
|
+
if (!slicesFile || !safeExistsSync(slicesFile)) {
|
|
2862
|
+
continue;
|
|
2863
|
+
}
|
|
2864
|
+
// The annotation embeds the slice as one string, which a file this
|
|
2865
|
+
// large cannot become.
|
|
2866
|
+
if (fs.statSync(slicesFile).size > MAX_JSON_TEXT_BYTES) {
|
|
2867
|
+
console.warn(
|
|
2868
|
+
`Not annotating the BOM with the slices file "${slicesFile}", which is too large to embed as text.`,
|
|
2869
|
+
);
|
|
2870
|
+
continue;
|
|
2871
|
+
}
|
|
2055
2872
|
bomJson.annotations.push({
|
|
2056
2873
|
subjects: [bomJson.serialNumber],
|
|
2057
2874
|
annotator: { component: bomJson.metadata.tools.components[0] },
|
|
2058
2875
|
timestamp: getTimestamp(),
|
|
2059
|
-
text: fs.readFileSync(
|
|
2876
|
+
text: fs.readFileSync(slicesFile, "utf8"),
|
|
2060
2877
|
});
|
|
2061
2878
|
}
|
|
2062
2879
|
}
|