@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/inventory/dosai.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
openSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
readSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
statSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { basename, delimiter, isAbsolute, join, resolve } from "node:path";
|
|
11
|
+
import process from "node:process";
|
|
3
12
|
|
|
4
13
|
import { DEBUG_MODE } from "../core/activity.js";
|
|
5
14
|
import { recordDegradation } from "../core/buildLedger.js";
|
|
@@ -9,8 +18,16 @@ import {
|
|
|
9
18
|
safeMkdtempSync,
|
|
10
19
|
safeRmSync,
|
|
11
20
|
safeSpawnSync,
|
|
21
|
+
safeWriteChunksSync,
|
|
12
22
|
safeWriteSync,
|
|
23
|
+
TIMEOUT_MS,
|
|
13
24
|
} from "../core/fs.js";
|
|
25
|
+
import { formatDuration, reapProcessTree } from "../core/processTree.js";
|
|
26
|
+
import {
|
|
27
|
+
indexJsonObject,
|
|
28
|
+
MAX_JSON_TEXT_BYTES,
|
|
29
|
+
readJsonRange,
|
|
30
|
+
} from "../parsers/largeJson.js";
|
|
14
31
|
import {
|
|
15
32
|
addDosaiSetValue,
|
|
16
33
|
buildDosaiPurlAliasMap,
|
|
@@ -181,11 +198,318 @@ function safeDosaiPatternPacks(value) {
|
|
|
181
198
|
.join(delimiter);
|
|
182
199
|
}
|
|
183
200
|
|
|
201
|
+
const MAX_DOSAI_EXCLUDES = 256;
|
|
202
|
+
const MAX_BRACE_RANGE = 64;
|
|
203
|
+
const GLOB_ESCAPABLE = "*?[]{}()!+@,";
|
|
204
|
+
// Characters a translated dosai glob may contain. dosai runs without a shell,
|
|
205
|
+
// but keeping shell metacharacters out means a pattern can never change
|
|
206
|
+
// meaning even if that ever changes. Anything else is reported as skipped.
|
|
207
|
+
const SAFE_DOSAI_GLOB = /^[\p{L}\p{N} _.+@#%~=/*?-]+$/u;
|
|
208
|
+
// Characters a dosai executable path may contain, including Windows drive
|
|
209
|
+
// letters and "Program Files (x86)". Shell metacharacters are not allowed.
|
|
210
|
+
const SAFE_DOSAI_EXECUTABLE = /^[\p{L}\p{N} _.,+@#%~=/\\:()-]+$/u;
|
|
211
|
+
const warnedDosaiMessages = new Set();
|
|
212
|
+
const dosaiWithoutExclude = new Set();
|
|
213
|
+
|
|
214
|
+
function warnDosaiOnce(message) {
|
|
215
|
+
if (!warnedDosaiMessages.has(message)) {
|
|
216
|
+
warnedDosaiMessages.add(message);
|
|
217
|
+
console.warn(message);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Find the brace group starting at `open` and return its closing index and the
|
|
223
|
+
* top-level comma positions, or undefined when the brace is never closed.
|
|
224
|
+
*/
|
|
225
|
+
function scanBraceGroup(pattern, open) {
|
|
226
|
+
let depth = 0;
|
|
227
|
+
const commas = [];
|
|
228
|
+
for (let i = open; i < pattern.length; i++) {
|
|
229
|
+
const char = pattern[i];
|
|
230
|
+
if (char === "{") {
|
|
231
|
+
depth++;
|
|
232
|
+
} else if (char === "}") {
|
|
233
|
+
depth--;
|
|
234
|
+
if (depth === 0) {
|
|
235
|
+
return { close: i, commas };
|
|
236
|
+
}
|
|
237
|
+
} else if (char === "," && depth === 1) {
|
|
238
|
+
commas.push(i);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function braceAlternatives(pattern, open, group) {
|
|
245
|
+
const body = pattern.slice(open + 1, group.close);
|
|
246
|
+
if (group.commas.length) {
|
|
247
|
+
const bounds = [open, ...group.commas, group.close];
|
|
248
|
+
const alternatives = [];
|
|
249
|
+
for (let i = 0; i < bounds.length - 1; i++) {
|
|
250
|
+
alternatives.push(pattern.slice(bounds[i] + 1, bounds[i + 1]));
|
|
251
|
+
}
|
|
252
|
+
return alternatives;
|
|
253
|
+
}
|
|
254
|
+
// minimatch also expands small integer ranges such as {1..3}.
|
|
255
|
+
const rangeParts = body.split("..");
|
|
256
|
+
if (
|
|
257
|
+
rangeParts.length === 2 &&
|
|
258
|
+
/^-?\d+$/.test(rangeParts[0]) &&
|
|
259
|
+
/^-?\d+$/.test(rangeParts[1])
|
|
260
|
+
) {
|
|
261
|
+
const from = Number.parseInt(rangeParts[0], 10);
|
|
262
|
+
const to = Number.parseInt(rangeParts[1], 10);
|
|
263
|
+
if (Math.abs(to - from) < MAX_BRACE_RANGE) {
|
|
264
|
+
const step = from <= to ? 1 : -1;
|
|
265
|
+
const alternatives = [];
|
|
266
|
+
for (let value = from; value !== to + step; value += step) {
|
|
267
|
+
alternatives.push(String(value));
|
|
268
|
+
}
|
|
269
|
+
return alternatives;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Expand brace groups the way minimatch does. A group without a comma or a
|
|
277
|
+
* numeric range, and a group written as "${...}", stays literal text.
|
|
278
|
+
*/
|
|
279
|
+
function expandGlobBraces(pattern, limit = MAX_DOSAI_EXCLUDES) {
|
|
280
|
+
let open = pattern.indexOf("{");
|
|
281
|
+
while (open !== -1) {
|
|
282
|
+
const group = scanBraceGroup(pattern, open);
|
|
283
|
+
if (!group) {
|
|
284
|
+
return [pattern];
|
|
285
|
+
}
|
|
286
|
+
const alternatives =
|
|
287
|
+
pattern[open - 1] === "$"
|
|
288
|
+
? undefined
|
|
289
|
+
: braceAlternatives(pattern, open, group);
|
|
290
|
+
if (alternatives) {
|
|
291
|
+
const head = pattern.slice(0, open);
|
|
292
|
+
const tail = pattern.slice(group.close + 1);
|
|
293
|
+
const expanded = [];
|
|
294
|
+
for (const alternative of alternatives) {
|
|
295
|
+
for (const entry of expandGlobBraces(
|
|
296
|
+
`${head}${alternative}${tail}`,
|
|
297
|
+
limit,
|
|
298
|
+
)) {
|
|
299
|
+
expanded.push(entry);
|
|
300
|
+
if (expanded.length >= limit) {
|
|
301
|
+
return expanded;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return expanded;
|
|
306
|
+
}
|
|
307
|
+
open = pattern.indexOf("{", group.close + 1);
|
|
308
|
+
}
|
|
309
|
+
return [pattern];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Normalise separators the way the atom exclude filter does: a backslash
|
|
314
|
+
* before a glob character is an escape, any other backslash is a Windows path
|
|
315
|
+
* separator. Returns undefined for escapes, which dosai cannot express.
|
|
316
|
+
*/
|
|
317
|
+
function normalizeDosaiGlobSeparators(pattern) {
|
|
318
|
+
let normalized = "";
|
|
319
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
320
|
+
const char = pattern[i];
|
|
321
|
+
if (char !== "\\") {
|
|
322
|
+
normalized += char;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (GLOB_ESCAPABLE.includes(pattern[i + 1] || "")) {
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
normalized += "/";
|
|
329
|
+
}
|
|
330
|
+
return normalized;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function hasUnsupportedGlobSyntax(glob) {
|
|
334
|
+
if (glob.includes("[") || glob.includes("]")) {
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
for (let i = 1; i < glob.length; i++) {
|
|
338
|
+
if (glob[i] === "(" && "@?+*!".includes(glob[i - 1])) {
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Rebase an absolute glob onto one of the scanned roots. Matching ignores case
|
|
347
|
+
* on macOS and Windows, as cdxgen's own glob does there.
|
|
348
|
+
*/
|
|
349
|
+
function rebaseAbsoluteGlob(glob, roots) {
|
|
350
|
+
const ignoreCase =
|
|
351
|
+
process.platform === "darwin" || process.platform === "win32";
|
|
352
|
+
const candidate = ignoreCase ? glob.toLowerCase() : glob;
|
|
353
|
+
for (const root of roots) {
|
|
354
|
+
let rootPath = root.replaceAll("\\", "/");
|
|
355
|
+
while (rootPath.endsWith("/")) {
|
|
356
|
+
rootPath = rootPath.slice(0, -1);
|
|
357
|
+
}
|
|
358
|
+
const rootPrefix = `${rootPath}/`;
|
|
359
|
+
const prefix = ignoreCase ? rootPrefix.toLowerCase() : rootPrefix;
|
|
360
|
+
if (candidate.startsWith(prefix)) {
|
|
361
|
+
const rest = glob.slice(prefix.length);
|
|
362
|
+
return rest ? rest : undefined;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return undefined;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Translate cdxgen --exclude globs into dosai --exclude globs.
|
|
370
|
+
*
|
|
371
|
+
* Dosai is an evidence analyzer, so the translation follows the exclude
|
|
372
|
+
* filter cdxgen already applies to atom evidence slices
|
|
373
|
+
* (`globPatternsToAtomIgnoreRegex`): a relative pattern matches at any depth,
|
|
374
|
+
* and an absolute pattern is anchored to the scanned directory. Dosai itself
|
|
375
|
+
* uses gitignore conventions, where a pattern with a slash is anchored, so
|
|
376
|
+
* relative patterns get a leading "**\/". Like the atom filter, and unlike
|
|
377
|
+
* cdxgen's file discovery, excluding a directory also excludes everything
|
|
378
|
+
* beneath it.
|
|
379
|
+
*
|
|
380
|
+
* Brace groups and numeric ranges are expanded, and comma separated lists are
|
|
381
|
+
* split, since dosai reads both as literal text. Character classes, extglobs,
|
|
382
|
+
* escapes, negation, and patterns that step outside the scanned directory
|
|
383
|
+
* have no dosai equivalent; they are reported as skipped rather than passed
|
|
384
|
+
* on as literals that match nothing. So is any pattern that still carries a
|
|
385
|
+
* character outside a small allowlist, such as a literal brace group or a
|
|
386
|
+
* shell metacharacter. Dosai matches case-sensitively on Linux.
|
|
387
|
+
*
|
|
388
|
+
* @param {string[]} excludes cdxgen exclude globs
|
|
389
|
+
* @param {string} srcPath Absolute directory dosai scans
|
|
390
|
+
* @returns {{patterns: string[], skipped: string[], truncated: number}} Translated patterns, dropped patterns, and how many were left out by the cap
|
|
391
|
+
*/
|
|
392
|
+
export function toDosaiExcludePatterns(excludes, srcPath) {
|
|
393
|
+
const patterns = new Set();
|
|
394
|
+
const skipped = [];
|
|
395
|
+
let truncated = 0;
|
|
396
|
+
if (!Array.isArray(excludes)) {
|
|
397
|
+
return { patterns: [], skipped, truncated };
|
|
398
|
+
}
|
|
399
|
+
const roots = [srcPath];
|
|
400
|
+
try {
|
|
401
|
+
const realRoot = realpathSync(srcPath);
|
|
402
|
+
if (realRoot !== srcPath) {
|
|
403
|
+
roots.push(realRoot);
|
|
404
|
+
}
|
|
405
|
+
} catch (_err) {
|
|
406
|
+
// The directory may not exist yet in tests; the given path still applies.
|
|
407
|
+
}
|
|
408
|
+
const candidates = [];
|
|
409
|
+
for (const pattern of excludes) {
|
|
410
|
+
if (typeof pattern !== "string" || /[\0\r\n]/.test(pattern)) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const entries =
|
|
414
|
+
pattern.includes(",") && !pattern.includes("{")
|
|
415
|
+
? pattern.split(",")
|
|
416
|
+
: [pattern];
|
|
417
|
+
for (const entry of entries) {
|
|
418
|
+
const trimmed = entry.trim();
|
|
419
|
+
if (trimmed) {
|
|
420
|
+
candidates.push(trimmed);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
for (const original of candidates) {
|
|
425
|
+
const normalized = original.startsWith("!")
|
|
426
|
+
? undefined
|
|
427
|
+
: normalizeDosaiGlobSeparators(original);
|
|
428
|
+
if (!normalized) {
|
|
429
|
+
skipped.push(original);
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
for (const expanded of expandGlobBraces(normalized)) {
|
|
433
|
+
let glob = expanded;
|
|
434
|
+
while (glob.includes("//")) {
|
|
435
|
+
glob = glob.replaceAll("//", "/");
|
|
436
|
+
}
|
|
437
|
+
if (hasUnsupportedGlobSyntax(glob)) {
|
|
438
|
+
skipped.push(expanded);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (isAbsolute(glob) || /^[A-Za-z]:\//.test(glob)) {
|
|
442
|
+
const rebased = rebaseAbsoluteGlob(glob, roots);
|
|
443
|
+
if (!rebased) {
|
|
444
|
+
skipped.push(expanded);
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
glob = `/${rebased}`;
|
|
448
|
+
} else {
|
|
449
|
+
while (glob.startsWith("./")) {
|
|
450
|
+
glob = glob.slice(2);
|
|
451
|
+
}
|
|
452
|
+
if (glob && !glob.startsWith("**/") && glob !== "**") {
|
|
453
|
+
glob = `**/${glob}`;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const segments = glob.split("/").filter(Boolean);
|
|
457
|
+
if (
|
|
458
|
+
!segments.length ||
|
|
459
|
+
segments.includes("..") ||
|
|
460
|
+
segments.every((segment) => segment === ".")
|
|
461
|
+
) {
|
|
462
|
+
skipped.push(expanded);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (!SAFE_DOSAI_GLOB.test(glob)) {
|
|
466
|
+
skipped.push(expanded);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (patterns.size >= MAX_DOSAI_EXCLUDES) {
|
|
470
|
+
if (!patterns.has(glob)) {
|
|
471
|
+
truncated++;
|
|
472
|
+
}
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
patterns.add(glob);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return { patterns: Array.from(patterns), skipped, truncated };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function dosaiExcludeArgs(excludes, srcPath) {
|
|
482
|
+
const { patterns, skipped, truncated } = toDosaiExcludePatterns(
|
|
483
|
+
excludes,
|
|
484
|
+
srcPath,
|
|
485
|
+
);
|
|
486
|
+
if (skipped.length) {
|
|
487
|
+
warnDosaiOnce(
|
|
488
|
+
`dosai cannot express these exclude patterns and will not apply them: ${skipped.join(", ")}`,
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
if (truncated) {
|
|
492
|
+
warnDosaiOnce(
|
|
493
|
+
`Only the first ${MAX_DOSAI_EXCLUDES} exclude patterns are passed to dosai; ${truncated} more were left out.`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
return patterns.flatMap((pattern) => ["--exclude", pattern]);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function dosaiRejectedExclude(result) {
|
|
500
|
+
const output = `${result?.stdout || ""}${result?.stderr || ""}`;
|
|
501
|
+
return output.includes("Unrecognized command or argument '--exclude'");
|
|
502
|
+
}
|
|
503
|
+
|
|
184
504
|
function safeDosaiExecutable(value) {
|
|
185
|
-
if (!value || typeof value !== "string"
|
|
505
|
+
if (!value || typeof value !== "string") {
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
const executable = value.trim();
|
|
509
|
+
if (!SAFE_DOSAI_EXECUTABLE.test(executable)) {
|
|
186
510
|
return undefined;
|
|
187
511
|
}
|
|
188
|
-
return
|
|
512
|
+
return executable;
|
|
189
513
|
}
|
|
190
514
|
|
|
191
515
|
/**
|
|
@@ -215,6 +539,207 @@ export function readDosaiJsonFile(jsonFile) {
|
|
|
215
539
|
}
|
|
216
540
|
}
|
|
217
541
|
|
|
542
|
+
// The sections of a dosai methods report that cdxgen reads. The rest (Methods,
|
|
543
|
+
// Properties, Fields, Reachability, DeadCode, ...) only reaches downstream
|
|
544
|
+
// tools through the persisted native report.
|
|
545
|
+
const DOSAI_METHODS_SECTIONS = [
|
|
546
|
+
"Metadata",
|
|
547
|
+
"Dependencies",
|
|
548
|
+
"MethodCalls",
|
|
549
|
+
"AssemblyInformation",
|
|
550
|
+
"CallGraph",
|
|
551
|
+
"PackageReachability",
|
|
552
|
+
"Services",
|
|
553
|
+
"ApiEndpoints",
|
|
554
|
+
"AiComponents",
|
|
555
|
+
];
|
|
556
|
+
|
|
557
|
+
// The file each report returned by readDosaiReport came from, and whether it
|
|
558
|
+
// was trimmed to what cdxgen reads. persistDosaiSemanticsReport copies the
|
|
559
|
+
// file rather than re-serialising a trimmed or oversized report.
|
|
560
|
+
const dosaiReportSources = new WeakMap();
|
|
561
|
+
|
|
562
|
+
function referencedIds(entries, field) {
|
|
563
|
+
const ids = new Set();
|
|
564
|
+
for (const entry of entries || []) {
|
|
565
|
+
for (const id of entry?.[field] || []) {
|
|
566
|
+
ids.add(id);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return ids;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function readDosaiReport(reportFile, options, readTrimmed) {
|
|
573
|
+
if (!reportFile || !safeExistsSync(reportFile)) {
|
|
574
|
+
return undefined;
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
const size = statSync(reportFile).size;
|
|
578
|
+
let report;
|
|
579
|
+
let trimmed = false;
|
|
580
|
+
if (size <= (options.maxTextBytes ?? MAX_JSON_TEXT_BYTES)) {
|
|
581
|
+
report = JSON.parse(readFileSync(reportFile, "utf-8"));
|
|
582
|
+
} else {
|
|
583
|
+
// dotnet/efcore produces a 1.9 GB methods report (issue 1033). Read it
|
|
584
|
+
// in bounded runs and keep only what cdxgen uses, which is a small
|
|
585
|
+
// fraction of the file.
|
|
586
|
+
if (DEBUG_MODE) {
|
|
587
|
+
console.log(
|
|
588
|
+
`The dosai report "${reportFile}" is ${size} bytes, more than one JavaScript string can hold, so only the parts cdxgen uses are read from it.`,
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
const index = indexJsonObject(reportFile, options);
|
|
592
|
+
report = {};
|
|
593
|
+
readTrimmed(report, (section, spec) => {
|
|
594
|
+
const range = index.get(section);
|
|
595
|
+
if (range) {
|
|
596
|
+
report[section] = readJsonRange(reportFile, range, spec, options);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
trimmed = true;
|
|
600
|
+
}
|
|
601
|
+
if (report && typeof report === "object") {
|
|
602
|
+
dosaiReportSources.set(report, { file: reportFile, trimmed });
|
|
603
|
+
}
|
|
604
|
+
return report;
|
|
605
|
+
} catch (err) {
|
|
606
|
+
if (DEBUG_MODE) {
|
|
607
|
+
console.log(
|
|
608
|
+
`Unable to read the dosai report "${reportFile}": ${err.message}`,
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
return undefined;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Read a dosai methods report of any size.
|
|
617
|
+
*
|
|
618
|
+
* A report that fits in one JavaScript string is parsed whole. A larger one is
|
|
619
|
+
* read in bounded runs, keeping the requested sections, the CallGraph nodes and
|
|
620
|
+
* edges that PackageReachability references, and the MethodCalls accepted by
|
|
621
|
+
* `keepMethodCall` (none when it is omitted). Nothing cdxgen reads is lost.
|
|
622
|
+
*
|
|
623
|
+
* @param {string} reportFile Path to the dosai methods JSON
|
|
624
|
+
* @param {Object} [options] Options
|
|
625
|
+
* @param {string[]} [options.sections] Sections to keep from a large report (default: every section cdxgen reads)
|
|
626
|
+
* @param {Function} [options.keepMethodCall] Predicate choosing the MethodCalls to keep from a large report
|
|
627
|
+
* @param {number} [options.maxTextBytes] Largest report parsed whole, for tests
|
|
628
|
+
* @returns {Object|undefined} Parsed report, or undefined when missing or invalid
|
|
629
|
+
*/
|
|
630
|
+
export function readDosaiMethodsReport(reportFile, options = {}) {
|
|
631
|
+
const sections = options.sections || DOSAI_METHODS_SECTIONS;
|
|
632
|
+
return readDosaiReport(reportFile, options, (report, read) => {
|
|
633
|
+
for (const section of sections) {
|
|
634
|
+
if (section !== "CallGraph" && section !== "MethodCalls") {
|
|
635
|
+
read(section);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (sections.includes("MethodCalls") && options.keepMethodCall) {
|
|
639
|
+
read("MethodCalls", { filter: options.keepMethodCall });
|
|
640
|
+
}
|
|
641
|
+
if (sections.includes("CallGraph")) {
|
|
642
|
+
// CallGraph precedes PackageReachability in the file, which is why the
|
|
643
|
+
// sections are read by range rather than in one pass.
|
|
644
|
+
const edgeIds = referencedIds(report.PackageReachability, "EdgeIds");
|
|
645
|
+
const nodeIds = referencedIds(report.PackageReachability, "NodeIds");
|
|
646
|
+
read("CallGraph", {
|
|
647
|
+
members: {
|
|
648
|
+
Edges: { filter: (edge) => edgeIds.has(edge?.Id) },
|
|
649
|
+
Nodes: { filter: (node) => nodeIds.has(node?.Id) },
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Read a dosai data-flow report of any size.
|
|
658
|
+
*
|
|
659
|
+
* A report that fits in one JavaScript string is parsed whole. A larger one
|
|
660
|
+
* keeps Metadata, Slices, PackageReachability, and the Nodes those reference.
|
|
661
|
+
*
|
|
662
|
+
* @param {string} reportFile Path to the dosai data-flow JSON
|
|
663
|
+
* @param {Object} [options] Options
|
|
664
|
+
* @param {number} [options.maxTextBytes] Largest report parsed whole, for tests
|
|
665
|
+
* @returns {Object|undefined} Parsed report, or undefined when missing or invalid
|
|
666
|
+
*/
|
|
667
|
+
export function readDosaiDataFlowReport(reportFile, options = {}) {
|
|
668
|
+
return readDosaiReport(reportFile, options, (report, read) => {
|
|
669
|
+
read("Metadata");
|
|
670
|
+
read("Slices");
|
|
671
|
+
read("PackageReachability");
|
|
672
|
+
const nodeIds = referencedIds(report.Slices, "NodeIds");
|
|
673
|
+
for (const id of referencedIds(report.PackageReachability, "NodeIds")) {
|
|
674
|
+
nodeIds.add(id);
|
|
675
|
+
}
|
|
676
|
+
read("Nodes", { filter: (node) => nodeIds.has(node?.Id) });
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Whether a dosai run ended because it ran out of time.
|
|
682
|
+
*
|
|
683
|
+
* dosai has no limit of its own: cdxgen's spawn timeout
|
|
684
|
+
* (CDXGEN_TIMEOUT_MS) stops the process, which reaches the caller only as an
|
|
685
|
+
* ETIMEDOUT error looking like any other failure (issue 4438).
|
|
686
|
+
*
|
|
687
|
+
* @param {Object} result spawnSync result
|
|
688
|
+
* @returns {boolean} true when the run was stopped by the spawn timeout
|
|
689
|
+
*/
|
|
690
|
+
export function dosaiRunTimedOut(result) {
|
|
691
|
+
return result?.error?.code === "ETIMEDOUT";
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* How a dosai run was stopped before it could finish, if it was.
|
|
696
|
+
*
|
|
697
|
+
* Besides the spawn timeout, a run is stopped when its console output outgrows
|
|
698
|
+
* CDXGEN_MAX_BUFFER (ENOBUFS) or a signal ends it, such as the kernel's
|
|
699
|
+
* out-of-memory killer. A stopped run can leave its slice half written.
|
|
700
|
+
*
|
|
701
|
+
* @param {Object} result spawnSync result
|
|
702
|
+
* @returns {"timeout"|"buffer"|"signal"|undefined} Why the run was stopped, or undefined when it ended by itself
|
|
703
|
+
*/
|
|
704
|
+
export function dosaiRunStopReason(result) {
|
|
705
|
+
if (dosaiRunTimedOut(result)) {
|
|
706
|
+
return "timeout";
|
|
707
|
+
}
|
|
708
|
+
if (result?.error?.code === "ENOBUFS") {
|
|
709
|
+
return "buffer";
|
|
710
|
+
}
|
|
711
|
+
if (result?.signal) {
|
|
712
|
+
return "signal";
|
|
713
|
+
}
|
|
714
|
+
return undefined;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// The output files of the dosai runs of this process that were stopped, so a
|
|
718
|
+
// caller does not follow the stop warning with advice for another failure.
|
|
719
|
+
const stoppedDosaiOutputs = new Set();
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Whether the dosai run that should have written a file was stopped (by the
|
|
723
|
+
* time limit, the output buffer limit, or a signal) and already reported so.
|
|
724
|
+
*
|
|
725
|
+
* @param {string} outputFile Output file passed to the dosai run
|
|
726
|
+
* @returns {boolean} true when the run was stopped
|
|
727
|
+
*/
|
|
728
|
+
export function dosaiRunWasStopped(outputFile) {
|
|
729
|
+
const outputPath = safeDosaiPath(outputFile);
|
|
730
|
+
return Boolean(outputPath && stoppedDosaiOutputs.has(outputPath));
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function dosaiStopMessage(reason, signal) {
|
|
734
|
+
if (reason === "timeout") {
|
|
735
|
+
return `WARN: dosai did not finish within ${formatDuration(TIMEOUT_MS)} and was stopped, so its slices are missing or incomplete. Allow more time with CDXGEN_TIMEOUT_MS (milliseconds, every external command).`;
|
|
736
|
+
}
|
|
737
|
+
if (reason === "buffer") {
|
|
738
|
+
return "WARN: dosai wrote more console output than CDXGEN_MAX_BUFFER allows and was stopped, so its slices are missing or incomplete. Raise CDXGEN_MAX_BUFFER (bytes).";
|
|
739
|
+
}
|
|
740
|
+
return `WARN: dosai was stopped by ${signal || "a signal"} before it finished, so its slices are missing or incomplete. A SIGKILL usually means the system ran out of memory.`;
|
|
741
|
+
}
|
|
742
|
+
|
|
218
743
|
/**
|
|
219
744
|
* Run a dosai subcommand ("methods", "dataflows", or "crypto") against a source
|
|
220
745
|
* tree and write its JSON output to the given file.
|
|
@@ -222,7 +747,7 @@ export function readDosaiJsonFile(jsonFile) {
|
|
|
222
747
|
* @param {string} command Dosai subcommand to execute
|
|
223
748
|
* @param {string} src Source directory to analyze
|
|
224
749
|
* @param {string} outputFile Path where the dosai JSON output is written
|
|
225
|
-
* @param {Object} [options] Options carrying dosaiCommand, dataFlowPatterns, or
|
|
750
|
+
* @param {Object} [options] Options carrying dosaiCommand, dataFlowPatterns, patternPacks, or exclude globs
|
|
226
751
|
* @returns {boolean} True when the command succeeded and produced the output file, false otherwise
|
|
227
752
|
*/
|
|
228
753
|
export function runDosaiCommand(command, src, outputFile, options = {}) {
|
|
@@ -254,13 +779,35 @@ export function runDosaiCommand(command, src, outputFile, options = {}) {
|
|
|
254
779
|
} else if (command === "crypto") {
|
|
255
780
|
args.push("--format", "dosai");
|
|
256
781
|
}
|
|
782
|
+
const excludeArgs = dosaiWithoutExclude.has(executable)
|
|
783
|
+
? []
|
|
784
|
+
: dosaiExcludeArgs(options.exclude, srcPath);
|
|
257
785
|
if (DEBUG_MODE) {
|
|
258
|
-
console.log("Executing", executable, args.join(" "));
|
|
786
|
+
console.log("Executing", executable, [...args, ...excludeArgs].join(" "));
|
|
259
787
|
}
|
|
260
|
-
|
|
788
|
+
stoppedDosaiOutputs.delete(outputPath);
|
|
789
|
+
let startedAt = Date.now();
|
|
790
|
+
let result = safeSpawnSync(executable, [...args, ...excludeArgs], {
|
|
261
791
|
cwd: srcPath,
|
|
262
792
|
shell: false,
|
|
263
793
|
});
|
|
794
|
+
// Dosai releases that predate --exclude reject it. Rerun without it so a
|
|
795
|
+
// user supplied exclude never turns into a missing slice.
|
|
796
|
+
if (
|
|
797
|
+
excludeArgs.length &&
|
|
798
|
+
result?.status !== 0 &&
|
|
799
|
+
dosaiRejectedExclude(result)
|
|
800
|
+
) {
|
|
801
|
+
dosaiWithoutExclude.add(executable);
|
|
802
|
+
warnDosaiOnce(
|
|
803
|
+
"This dosai build does not support --exclude, so it runs without the exclude patterns. Upgrade dosai or set DOSAI_CMD to a newer build to honour them.",
|
|
804
|
+
);
|
|
805
|
+
startedAt = Date.now();
|
|
806
|
+
result = safeSpawnSync(executable, args, {
|
|
807
|
+
cwd: srcPath,
|
|
808
|
+
shell: false,
|
|
809
|
+
});
|
|
810
|
+
}
|
|
264
811
|
if (dosaiSdkMessage(result)) {
|
|
265
812
|
recordDegradation("dotnet.sdk.missing", {
|
|
266
813
|
ecosystem: "csharp",
|
|
@@ -277,6 +824,22 @@ export function runDosaiCommand(command, src, outputFile, options = {}) {
|
|
|
277
824
|
"Alternatively, download the dosai self-contained binary (-full suffix) from https://github.com/owasp-dep-scan/dosai/releases and set DOSAI_CMD to its location.",
|
|
278
825
|
);
|
|
279
826
|
}
|
|
827
|
+
const stopReason = dosaiRunStopReason(result);
|
|
828
|
+
if (stopReason) {
|
|
829
|
+
// Visible without debug output: otherwise a stopped dosai reads like any
|
|
830
|
+
// other failure while the scan silently loses its .NET evidence.
|
|
831
|
+
console.warn(dosaiStopMessage(stopReason, result.signal));
|
|
832
|
+
// Windows kills only the dosai process itself, and the build hosts it
|
|
833
|
+
// started would keep running.
|
|
834
|
+
reapProcessTree(result.pid, startedAt, Date.now(), "dosai");
|
|
835
|
+
// A stopped dosai can leave its output half written, and the caller would
|
|
836
|
+
// read the truncated file back as a slice.
|
|
837
|
+
if (safeExistsSync(outputPath)) {
|
|
838
|
+
safeRmSync(outputPath, { force: true });
|
|
839
|
+
}
|
|
840
|
+
stoppedDosaiOutputs.add(outputPath);
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
280
843
|
if (result?.status !== 0 || result?.error || !safeExistsSync(outputPath)) {
|
|
281
844
|
if (DEBUG_MODE) {
|
|
282
845
|
if (result?.stderr || result?.stdout) {
|
|
@@ -369,6 +932,38 @@ function buildCombinedDosaiReport(methodsSlice, dataFlowSlice) {
|
|
|
369
932
|
};
|
|
370
933
|
}
|
|
371
934
|
|
|
935
|
+
function* fileChunks(filePath) {
|
|
936
|
+
const fd = openSync(filePath, "r");
|
|
937
|
+
try {
|
|
938
|
+
const buffer = Buffer.allocUnsafe(8 * 1024 * 1024);
|
|
939
|
+
for (;;) {
|
|
940
|
+
const count = readSync(fd, buffer, 0, buffer.length, null);
|
|
941
|
+
if (!count) {
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
yield buffer.subarray(0, count);
|
|
945
|
+
}
|
|
946
|
+
} finally {
|
|
947
|
+
closeSync(fd);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// The combined report as JSON text in pieces, with each part copied from its
|
|
952
|
+
// native file when there is one. Keys and their order match
|
|
953
|
+
// buildCombinedDosaiReport.
|
|
954
|
+
function* combinedDosaiReportChunks(combined, parts) {
|
|
955
|
+
yield `{"Metadata":${JSON.stringify(combined.Metadata)}`;
|
|
956
|
+
for (const [key, source] of parts) {
|
|
957
|
+
yield `,${JSON.stringify(key)}:`;
|
|
958
|
+
if (source) {
|
|
959
|
+
yield* fileChunks(source.file);
|
|
960
|
+
} else {
|
|
961
|
+
yield JSON.stringify(combined[key]);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
yield "}";
|
|
965
|
+
}
|
|
966
|
+
|
|
372
967
|
/**
|
|
373
968
|
* Persist the combined native dosai report to options.semanticsSlicesFile.
|
|
374
969
|
*
|
|
@@ -399,16 +994,42 @@ export function persistDosaiSemanticsReport(
|
|
|
399
994
|
return undefined;
|
|
400
995
|
}
|
|
401
996
|
const combined = buildCombinedDosaiReport(methodsSlice, dataFlowSlice);
|
|
997
|
+
const methodsSource = dosaiReportSources.get(methodsSlice);
|
|
998
|
+
const dataFlowSource = dosaiReportSources.get(dataFlowSlice);
|
|
402
999
|
try {
|
|
403
|
-
|
|
1000
|
+
let text;
|
|
1001
|
+
if (!methodsSource?.trimmed && !dataFlowSource?.trimmed) {
|
|
1002
|
+
try {
|
|
1003
|
+
text = JSON.stringify(combined);
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
// Too large for one string: write it in parts below instead.
|
|
1006
|
+
if (!(err instanceof RangeError)) {
|
|
1007
|
+
throw err;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
if (text !== undefined) {
|
|
1012
|
+
safeWriteSync(durablePath, text);
|
|
1013
|
+
} else {
|
|
1014
|
+
// A trimmed report lacks the sections cdxgen skipped, so each part is
|
|
1015
|
+
// copied from its native file when it has one, and serialised on its
|
|
1016
|
+
// own otherwise.
|
|
1017
|
+
safeWriteChunksSync(
|
|
1018
|
+
durablePath,
|
|
1019
|
+
combinedDosaiReportChunks(combined, [
|
|
1020
|
+
["methods", methodsSource],
|
|
1021
|
+
["dataflows", dataFlowSource],
|
|
1022
|
+
]),
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
404
1025
|
} catch (err) {
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
recordDegradation("dosai.semantics.
|
|
1026
|
+
// A part with no native file can still be too large to serialise, and the
|
|
1027
|
+
// write can fail. The SBOM evidence is already collected by this point, so
|
|
1028
|
+
// skip the durable copy instead of losing the whole analysis.
|
|
1029
|
+
recordDegradation("dosai.semantics.persist-failed", {
|
|
409
1030
|
ecosystem: "csharp",
|
|
410
1031
|
tool: "dosai",
|
|
411
|
-
impact: "
|
|
1032
|
+
impact: "none",
|
|
412
1033
|
command: `persistDosaiSemanticsReport ${durablePath}`,
|
|
413
1034
|
detail: `The combined dosai report could not be persisted: ${err.message}. The SBOM evidence is unaffected.`,
|
|
414
1035
|
});
|
|
@@ -421,21 +1042,63 @@ export function persistDosaiSemanticsReport(
|
|
|
421
1042
|
* Build a purl alias map for a list of components.
|
|
422
1043
|
*
|
|
423
1044
|
* @param {Object[]} [components] Component objects with purl fields
|
|
424
|
-
* @
|
|
1045
|
+
* @param {Object} [options] Options passed to {@link buildDosaiPurlAliasMap} (`srcPath`)
|
|
1046
|
+
* @returns {Map<string, string>} Map of component purls, carrying the version-free identity index
|
|
425
1047
|
*/
|
|
426
|
-
export function buildPurlAliasMap(components = []) {
|
|
427
|
-
return buildDosaiPurlAliasMap(components);
|
|
1048
|
+
export function buildPurlAliasMap(components = [], options = {}) {
|
|
1049
|
+
return buildDosaiPurlAliasMap(components, options);
|
|
428
1050
|
}
|
|
429
1051
|
|
|
430
1052
|
/**
|
|
431
|
-
* Resolve a
|
|
1053
|
+
* Resolve a dosai purl to the canonical component purl, by version, and for a
|
|
1054
|
+
* package the BOM holds in several versions by the record's location.
|
|
432
1055
|
*
|
|
433
1056
|
* @param {string} purl Purl from a dosai report
|
|
434
1057
|
* @param {Map<string, string>} purlAliasMap Alias map built by buildPurlAliasMap
|
|
435
|
-
* @
|
|
1058
|
+
* @param {string} [location] Source location of the dosai record
|
|
1059
|
+
* @returns {string|undefined} Canonical component purl, the input purl when the BOM has no such package,
|
|
1060
|
+
* or undefined when empty or ambiguous
|
|
1061
|
+
*/
|
|
1062
|
+
export function resolveComponentPurl(purl, purlAliasMap, location) {
|
|
1063
|
+
return resolveDosaiComponentPurl(purl, purlAliasMap, location);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* The component purls one PackageReachability fact belongs to: each of its
|
|
1068
|
+
* locations resolved in its own project (a versionless purl from unrestored
|
|
1069
|
+
* projects spans several), else the purl alone.
|
|
1070
|
+
*
|
|
1071
|
+
* @param {Object} reachability dosai PackageReachability entry
|
|
1072
|
+
* @param {Map<string, string>} purlAliasMap Alias map built by buildPurlAliasMap
|
|
1073
|
+
* @param {Object} [graph] Call graph lookups `{ edgesById, nodesById }`
|
|
1074
|
+
* @returns {string[]} Component purls
|
|
436
1075
|
*/
|
|
437
|
-
|
|
438
|
-
|
|
1076
|
+
function reachabilityComponentPurls(reachability, purlAliasMap, graph = {}) {
|
|
1077
|
+
const locations = [
|
|
1078
|
+
...(reachability.SourceLocations || []).map((location) =>
|
|
1079
|
+
dosaiSourceLocation(location),
|
|
1080
|
+
),
|
|
1081
|
+
...(reachability.EdgeIds || []).map((edgeId) =>
|
|
1082
|
+
dosaiSourceLocation(graph.edgesById?.get(edgeId)),
|
|
1083
|
+
),
|
|
1084
|
+
...(reachability.NodeIds || []).map((nodeId) =>
|
|
1085
|
+
dosaiSourceLocationFromNode(graph.nodesById?.get(nodeId)),
|
|
1086
|
+
),
|
|
1087
|
+
].filter(Boolean);
|
|
1088
|
+
const purls = new Set(
|
|
1089
|
+
locations
|
|
1090
|
+
.map((location) =>
|
|
1091
|
+
resolveComponentPurl(reachability.Purl, purlAliasMap, location),
|
|
1092
|
+
)
|
|
1093
|
+
.filter(Boolean),
|
|
1094
|
+
);
|
|
1095
|
+
if (!purls.size) {
|
|
1096
|
+
const purl = resolveComponentPurl(reachability.Purl, purlAliasMap);
|
|
1097
|
+
if (purl) {
|
|
1098
|
+
purls.add(purl);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return [...purls];
|
|
439
1102
|
}
|
|
440
1103
|
|
|
441
1104
|
/**
|
|
@@ -484,24 +1147,51 @@ export function addDosaiReachabilityProperties(component, reachability) {
|
|
|
484
1147
|
*
|
|
485
1148
|
* @param {Object} methodsSlice Parsed dosai methods slice JSON
|
|
486
1149
|
* @param {Object[]} [components] BOM components used to resolve purl aliases and mutate
|
|
1150
|
+
* @param {Object} [options] Options
|
|
1151
|
+
* @param {string} [options.srcPath] Directory dosai analyzed; a package the BOM holds in several
|
|
1152
|
+
* versions is matched by the project of the record's file
|
|
487
1153
|
* @returns {number} Number of distinct components enriched
|
|
488
1154
|
*/
|
|
489
|
-
export function applyDosaiReachabilityEvidence(
|
|
490
|
-
|
|
1155
|
+
export function applyDosaiReachabilityEvidence(
|
|
1156
|
+
methodsSlice,
|
|
1157
|
+
components = [],
|
|
1158
|
+
options = {},
|
|
1159
|
+
) {
|
|
1160
|
+
const purlAliasMap = buildPurlAliasMap(components, options);
|
|
491
1161
|
const componentsByPurl = new Map(
|
|
492
1162
|
components
|
|
493
1163
|
.filter((component) => component?.purl)
|
|
494
1164
|
.map((component) => [component.purl, component]),
|
|
495
1165
|
);
|
|
1166
|
+
const graph = {
|
|
1167
|
+
edgesById: new Map(
|
|
1168
|
+
(methodsSlice?.CallGraph?.Edges || []).map((edge) => [edge.Id, edge]),
|
|
1169
|
+
),
|
|
1170
|
+
nodesById: new Map(
|
|
1171
|
+
(methodsSlice?.CallGraph?.Nodes || []).map((node) => [node.Id, node]),
|
|
1172
|
+
),
|
|
1173
|
+
};
|
|
496
1174
|
const enriched = new Set();
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
1175
|
+
// The first fact applied to a component wins, so a fact that names the
|
|
1176
|
+
// component's exact purl goes before one that reached it by name.
|
|
1177
|
+
const facts = [...(methodsSlice?.PackageReachability || [])].sort(
|
|
1178
|
+
(a, b) =>
|
|
1179
|
+
Number(!componentsByPurl.has(a.Purl)) -
|
|
1180
|
+
Number(!componentsByPurl.has(b.Purl)),
|
|
1181
|
+
);
|
|
1182
|
+
for (const reachability of facts) {
|
|
1183
|
+
for (const purl of reachabilityComponentPurls(
|
|
1184
|
+
reachability,
|
|
1185
|
+
purlAliasMap,
|
|
1186
|
+
graph,
|
|
1187
|
+
)) {
|
|
1188
|
+
const component = componentsByPurl.get(purl);
|
|
1189
|
+
if (!component) {
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
addDosaiReachabilityProperties(component, reachability);
|
|
1193
|
+
enriched.add(component);
|
|
502
1194
|
}
|
|
503
|
-
addDosaiReachabilityProperties(component, reachability);
|
|
504
|
-
enriched.add(component);
|
|
505
1195
|
}
|
|
506
1196
|
return enriched.size;
|
|
507
1197
|
}
|
|
@@ -514,10 +1204,17 @@ export function applyDosaiReachabilityEvidence(methodsSlice, components = []) {
|
|
|
514
1204
|
*
|
|
515
1205
|
* @param {Object} methodsSlice Parsed dosai methods slice JSON
|
|
516
1206
|
* @param {Object[]} [components] BOM components used to resolve purl aliases
|
|
1207
|
+
* @param {Object} [options] Options
|
|
1208
|
+
* @param {string} [options.srcPath] Directory dosai analyzed; a package the BOM holds in several
|
|
1209
|
+
* versions is matched by the project of the record's file
|
|
517
1210
|
* @returns {Object} Object with purlLocationMap, purlModulesMap, and purlMethodsMap keyed by purl
|
|
518
1211
|
*/
|
|
519
|
-
export function collectDosaiPurlEvidence(
|
|
520
|
-
|
|
1212
|
+
export function collectDosaiPurlEvidence(
|
|
1213
|
+
methodsSlice,
|
|
1214
|
+
components = [],
|
|
1215
|
+
options = {},
|
|
1216
|
+
) {
|
|
1217
|
+
const purlAliasMap = buildPurlAliasMap(components, options);
|
|
521
1218
|
const purlLocationMap = {};
|
|
522
1219
|
const purlModulesMap = {};
|
|
523
1220
|
const purlMethodsMap = {};
|
|
@@ -529,7 +1226,11 @@ export function collectDosaiPurlEvidence(methodsSlice, components = []) {
|
|
|
529
1226
|
);
|
|
530
1227
|
|
|
531
1228
|
for (const dependency of methodsSlice?.Dependencies || []) {
|
|
532
|
-
const purl = resolveComponentPurl(
|
|
1229
|
+
const purl = resolveComponentPurl(
|
|
1230
|
+
dependency.Purl,
|
|
1231
|
+
purlAliasMap,
|
|
1232
|
+
dependency.Path,
|
|
1233
|
+
);
|
|
533
1234
|
if (!purl) {
|
|
534
1235
|
continue;
|
|
535
1236
|
}
|
|
@@ -542,42 +1243,61 @@ export function collectDosaiPurlEvidence(methodsSlice, components = []) {
|
|
|
542
1243
|
}
|
|
543
1244
|
|
|
544
1245
|
for (const reachability of methodsSlice?.PackageReachability || []) {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
1246
|
+
// Every location resolves in its own project: a versionless purl from
|
|
1247
|
+
// unrestored projects spans projects restored at different versions.
|
|
1248
|
+
const resolveAt = (location) =>
|
|
1249
|
+
resolveComponentPurl(reachability.Purl, purlAliasMap, location);
|
|
1250
|
+
const factPurls = new Set();
|
|
1251
|
+
// Modules and methods of call-graph items that name no source file
|
|
1252
|
+
// (frames inside the package), held for the fact's own version.
|
|
1253
|
+
const unplaced = [];
|
|
549
1254
|
let hasExplicitSourceLocations = false;
|
|
550
1255
|
for (const sourceLocation of reachability.SourceLocations || []) {
|
|
551
1256
|
const location = dosaiSourceLocation(sourceLocation);
|
|
1257
|
+
const purl = resolveAt(location);
|
|
552
1258
|
addDosaiSetValue(purlLocationMap, purl, location);
|
|
1259
|
+
if (purl) {
|
|
1260
|
+
factPurls.add(purl);
|
|
1261
|
+
}
|
|
553
1262
|
hasExplicitSourceLocations ||= Boolean(location);
|
|
554
1263
|
}
|
|
555
1264
|
for (const edgeId of reachability.EdgeIds || []) {
|
|
556
1265
|
const edge = edgesById.get(edgeId);
|
|
1266
|
+
const location = dosaiSourceLocation(edge);
|
|
1267
|
+
const purl = resolveAt(location ?? edge?.Path);
|
|
1268
|
+
const method = edge?.CalledMethodName || edge?.TargetName;
|
|
1269
|
+
if (!purl) {
|
|
1270
|
+
unplaced.push([purlMethodsMap, method]);
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
factPurls.add(purl);
|
|
557
1274
|
if (!hasExplicitSourceLocations) {
|
|
558
|
-
addDosaiSetValue(purlLocationMap, purl,
|
|
1275
|
+
addDosaiSetValue(purlLocationMap, purl, location);
|
|
559
1276
|
}
|
|
560
|
-
addDosaiSetValue(
|
|
561
|
-
purlMethodsMap,
|
|
562
|
-
purl,
|
|
563
|
-
edge?.CalledMethodName || edge?.TargetName,
|
|
564
|
-
);
|
|
1277
|
+
addDosaiSetValue(purlMethodsMap, purl, method);
|
|
565
1278
|
}
|
|
566
1279
|
for (const nodeId of reachability.NodeIds || []) {
|
|
567
1280
|
const node = nodesById.get(nodeId);
|
|
1281
|
+
const location = dosaiSourceLocationFromNode(node);
|
|
1282
|
+
const purl = resolveAt(location);
|
|
1283
|
+
const module = node?.ClassName || node?.Module;
|
|
1284
|
+
const method = node?.Name || node?.Identity?.MethodName;
|
|
1285
|
+
if (!purl) {
|
|
1286
|
+
unplaced.push([purlModulesMap, module], [purlMethodsMap, method]);
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
factPurls.add(purl);
|
|
568
1290
|
if (!hasExplicitSourceLocations) {
|
|
569
|
-
addDosaiSetValue(
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
1291
|
+
addDosaiSetValue(purlLocationMap, purl, location);
|
|
1292
|
+
}
|
|
1293
|
+
addDosaiSetValue(purlModulesMap, purl, module);
|
|
1294
|
+
addDosaiSetValue(purlMethodsMap, purl, method);
|
|
1295
|
+
}
|
|
1296
|
+
if (factPurls.size === 1) {
|
|
1297
|
+
const [purl] = factPurls;
|
|
1298
|
+
for (const [map, value] of unplaced) {
|
|
1299
|
+
addDosaiSetValue(map, purl, value);
|
|
574
1300
|
}
|
|
575
|
-
addDosaiSetValue(purlModulesMap, purl, node?.ClassName || node?.Module);
|
|
576
|
-
addDosaiSetValue(
|
|
577
|
-
purlMethodsMap,
|
|
578
|
-
purl,
|
|
579
|
-
node?.Name || node?.Identity?.MethodName,
|
|
580
|
-
);
|
|
581
1301
|
}
|
|
582
1302
|
}
|
|
583
1303
|
return { purlLocationMap, purlModulesMap, purlMethodsMap };
|
|
@@ -591,17 +1311,38 @@ export function collectDosaiPurlEvidence(methodsSlice, components = []) {
|
|
|
591
1311
|
*
|
|
592
1312
|
* @param {Object} dataFlowResult Parsed dosai data-flow slice JSON
|
|
593
1313
|
* @param {Object[]} [components] BOM components used to resolve purl aliases
|
|
1314
|
+
* @param {Object} [options] Options
|
|
1315
|
+
* @param {string} [options.srcPath] Directory dosai analyzed; a package the BOM holds in several
|
|
1316
|
+
* versions is matched by the project of the record's file
|
|
594
1317
|
* @returns {Object} Map of canonical purl to arrays of call-stack frame objects
|
|
595
1318
|
*/
|
|
596
|
-
export function collectDosaiDataFlowFrames(
|
|
597
|
-
|
|
1319
|
+
export function collectDosaiDataFlowFrames(
|
|
1320
|
+
dataFlowResult,
|
|
1321
|
+
components = [],
|
|
1322
|
+
options = {},
|
|
1323
|
+
) {
|
|
1324
|
+
const purlAliasMap = buildPurlAliasMap(components, options);
|
|
598
1325
|
const nodesById = new Map(
|
|
599
1326
|
(dataFlowResult?.Nodes || []).map((node) => [node.Id, node]),
|
|
600
1327
|
);
|
|
601
1328
|
const dataFlowFrames = {};
|
|
602
1329
|
const addFramesForPurl = (purl, frames) => {
|
|
603
|
-
|
|
604
|
-
|
|
1330
|
+
if (!frames.length) {
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
// The flow runs in the project of its first frame that settles one.
|
|
1334
|
+
let componentPurl;
|
|
1335
|
+
for (const frame of frames) {
|
|
1336
|
+
componentPurl = resolveComponentPurl(
|
|
1337
|
+
purl,
|
|
1338
|
+
purlAliasMap,
|
|
1339
|
+
frame.fullFilename,
|
|
1340
|
+
);
|
|
1341
|
+
if (componentPurl) {
|
|
1342
|
+
break;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
if (!componentPurl) {
|
|
605
1346
|
return;
|
|
606
1347
|
}
|
|
607
1348
|
dataFlowFrames[componentPurl] ??= [];
|