@cdxgen/cdxgen 13.1.0 → 13.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -15
- package/bin/cdxgen.js +243 -122
- package/bin/evinse.js +7 -0
- package/bin/sign.js +88 -14
- package/bin/tracebom.js +64 -4
- package/bin/validate.js +29 -7
- package/bin/verify.js +79 -39
- package/data/crypto-oid.json +88 -12
- package/data/remediations.json +47 -0
- package/data/rules/ai-bom.yaml +78 -0
- package/data/rules/cargo-rust.yaml +132 -0
- package/data/rules/mcp-servers.yaml +122 -0
- package/lib/audit/targets.js +6 -1
- package/lib/cli/bomAssembly.js +117 -15
- package/lib/cli/cliOptions.js +93 -1
- package/lib/cli/index.js +235 -5
- package/lib/cli/jsBom.js +108 -26
- package/lib/cli/jvmBom.js +68 -12
- package/lib/cli/managedBom.js +264 -41
- package/lib/cli/nativeBom.js +923 -15
- package/lib/core/activity.js +120 -2
- package/lib/core/env.js +44 -7
- package/lib/core/fs.js +174 -6
- package/lib/core/metadataProperties.js +97 -0
- package/lib/core/projectConfig.js +31 -4
- package/lib/core/userAgent.js +151 -0
- package/lib/ecosystems/bunutils.js +269 -101
- package/lib/ecosystems/cargoCache.js +364 -0
- package/lib/ecosystems/caxa.js +447 -7
- package/lib/ecosystems/cmakeResolver.js +66 -0
- package/lib/ecosystems/cppEvidence.js +104 -33
- package/lib/ecosystems/ecosystems.js +260 -99
- package/lib/ecosystems/golem.js +11 -1
- package/lib/ecosystems/gradleutils.js +185 -9
- package/lib/ecosystems/jsEvidence.js +19 -20
- package/lib/ecosystems/kosi.js +1263 -0
- package/lib/ecosystems/npmutils.js +110 -9
- package/lib/ecosystems/parsers-crystal.js +179 -0
- package/lib/ecosystems/parsers-dotnet.js +120 -49
- package/lib/ecosystems/parsers-elm.js +816 -0
- package/lib/ecosystems/parsers-gleam.js +1 -1
- package/lib/ecosystems/parsers-go.js +47 -9
- package/lib/ecosystems/parsers-js.js +171 -50
- package/lib/ecosystems/parsers-julia.js +268 -0
- package/lib/ecosystems/parsers-lua.js +218 -0
- package/lib/ecosystems/parsers-misc.js +919 -161
- package/lib/ecosystems/parsers-nim.js +221 -0
- package/lib/ecosystems/parsers-opam.js +180 -0
- package/lib/ecosystems/parsers-perl.js +277 -0
- package/lib/ecosystems/parsers-python.js +27 -15
- package/lib/ecosystems/parsers-r.js +148 -0
- package/lib/ecosystems/parsers-rebar.js +411 -0
- package/lib/ecosystems/parsers-rust.js +954 -33
- package/lib/ecosystems/parsers-spack.js +204 -0
- package/lib/ecosystems/parsers-stack.js +264 -0
- package/lib/ecosystems/parsers-terraform.js +194 -0
- package/lib/ecosystems/rubyutils.js +1426 -166
- package/lib/ecosystems/rusi.js +158 -7
- package/lib/evinser/evinser.js +547 -129
- package/lib/evinser/swiftsem.js +1914 -302
- package/lib/helpers/agenticToolutils.js +925 -0
- package/lib/helpers/bomSigner.js +652 -221
- package/lib/inventory/agentFormulationParser.js +57 -4
- package/lib/inventory/aiCollector.js +504 -10
- package/lib/inventory/aiInventory.js +12 -0
- package/lib/inventory/analyzer.js +488 -30
- package/lib/inventory/astWorker.js +6 -0
- package/lib/inventory/atomUtils.js +568 -31
- package/lib/inventory/auditCategories.js +8 -0
- package/lib/inventory/cbomutils.js +30 -0
- package/lib/inventory/cdxrs.js +110 -14
- package/lib/inventory/communityAiConfigParser.js +215 -2
- package/lib/inventory/depsUtils.js +183 -11
- package/lib/inventory/display.js +4 -5
- package/lib/inventory/dosai.js +627 -13
- package/lib/inventory/dynamic.js +39 -1
- package/lib/inventory/envcontext.js +9 -6
- package/lib/inventory/evidenceUtils.js +26 -5
- package/lib/inventory/fetchBatch.js +190 -19
- package/lib/inventory/fetchRate.js +74 -11
- package/lib/inventory/forgeEnricher.js +3 -2
- package/lib/inventory/jvmToolEnv.js +6 -5
- package/lib/inventory/mcpConfigParser.js +166 -2
- package/lib/inventory/mcpDiscovery.js +11 -0
- package/lib/inventory/mcpServerManifest.js +389 -0
- package/lib/inventory/mcpbBundle.js +419 -0
- package/lib/inventory/plugins.js +3 -0
- package/lib/inventory/protobom.js +62 -89
- package/lib/inventory/protobomLoader.js +4 -0
- package/lib/inventory/purl.js +152 -3
- package/lib/inventory/rustFormulationParser.js +1 -1
- package/lib/inventory/sbomDocument.js +2 -70
- package/lib/inventory/source.js +47 -23
- package/lib/inventory/stylePackageRefs.js +41 -0
- package/lib/inventory/svelteUtils.js +524 -0
- package/lib/inventory/traceRunner.js +210 -21
- package/lib/managers/binary.js +115 -24
- package/lib/managers/docker.js +440 -20
- package/lib/managers/oci.js +39 -1
- package/lib/parsers/cmakeLists.js +452 -0
- package/lib/parsers/huggingfaceUtils.js +15 -7
- package/lib/parsers/largeJson.js +454 -0
- package/lib/parsers/npmrc.js +19 -1
- package/lib/parsers/onnx.js +294 -0
- package/lib/parsers/safetensors.js +114 -0
- package/lib/stages/postgen/introspection/markers.js +29 -6
- package/lib/stages/postgen/postgen.js +237 -16
- package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
- package/lib/validator/bomValidator.js +6 -16
- package/lib/validator/index.js +48 -10
- package/lib/validator/reporters/console.js +3 -0
- package/lib/validator/reporters/json.js +1 -0
- package/lib/validator/reporters/sarif.js +7 -0
- package/package.json +27 -27
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts +2 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +33 -1
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +2 -2
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts +23 -0
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts +33 -0
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts +88 -8
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/activity.d.ts.map +1 -1
- package/types/lib/core/env.d.ts +21 -0
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +55 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/metadataProperties.d.ts +39 -0
- package/types/lib/core/metadataProperties.d.ts.map +1 -0
- package/types/lib/core/projectConfig.d.ts +6 -2
- package/types/lib/core/projectConfig.d.ts.map +1 -1
- package/types/lib/core/userAgent.d.ts +56 -0
- package/types/lib/core/userAgent.d.ts.map +1 -0
- package/types/lib/ecosystems/bunutils.d.ts +12 -5
- package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
- package/types/lib/ecosystems/cargoCache.d.ts +75 -0
- package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
- package/types/lib/ecosystems/caxa.d.ts +14 -0
- package/types/lib/ecosystems/caxa.d.ts.map +1 -1
- package/types/lib/ecosystems/cmakeResolver.d.ts +23 -0
- package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts +2 -2
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts +15 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +40 -4
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
- package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts +86 -0
- package/types/lib/ecosystems/kosi.d.ts.map +1 -0
- package/types/lib/ecosystems/npmutils.d.ts +42 -0
- package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
- package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
- package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-go.d.ts +10 -0
- package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts +10 -4
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
- package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
- package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-misc.d.ts +90 -3
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
- package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
- package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
- package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-r.d.ts +35 -0
- package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
- package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
- package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
- package/types/lib/ecosystems/pythonutils.d.ts +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts +143 -2
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts +13 -0
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +81 -2
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/swiftsem.d.ts +280 -15
- package/types/lib/evinser/swiftsem.d.ts.map +1 -1
- package/types/lib/helpers/agenticToolutils.d.ts +87 -0
- package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
- package/types/lib/helpers/bomSigner.d.ts +106 -13
- package/types/lib/helpers/bomSigner.d.ts.map +1 -1
- package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/aiInventory.d.ts.map +1 -1
- package/types/lib/inventory/analyzer.d.ts.map +1 -1
- package/types/lib/inventory/atomUtils.d.ts +145 -3
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +4 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/cbomutils.d.ts.map +1 -1
- package/types/lib/inventory/cdxrs.d.ts +8 -3
- package/types/lib/inventory/cdxrs.d.ts.map +1 -1
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/depsUtils.d.ts +15 -0
- package/types/lib/inventory/depsUtils.d.ts.map +1 -1
- package/types/lib/inventory/display.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +87 -1
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dynamic.d.ts.map +1 -1
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +34 -0
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/fetchRate.d.ts +9 -6
- package/types/lib/inventory/fetchRate.d.ts.map +1 -1
- package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
- package/types/lib/inventory/jvmToolEnv.d.ts +5 -4
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
- package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
- package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
- package/types/lib/inventory/mcpbBundle.d.ts +19 -0
- package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
- package/types/lib/inventory/plugins.d.ts.map +1 -1
- package/types/lib/inventory/protobom.d.ts +18 -13
- package/types/lib/inventory/protobom.d.ts.map +1 -1
- package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
- package/types/lib/inventory/purl.d.ts +71 -2
- package/types/lib/inventory/purl.d.ts.map +1 -1
- package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
- package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
- package/types/lib/inventory/svelteUtils.d.ts +124 -0
- package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
- package/types/lib/inventory/traceRunner.d.ts +47 -1
- package/types/lib/inventory/traceRunner.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +30 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +29 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts +14 -0
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/parsers/cmakeLists.d.ts +76 -0
- package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
- package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
- package/types/lib/parsers/largeJson.d.ts +52 -0
- package/types/lib/parsers/largeJson.d.ts.map +1 -0
- package/types/lib/parsers/npmrc.d.ts +5 -0
- package/types/lib/parsers/npmrc.d.ts.map +1 -1
- package/types/lib/parsers/onnx.d.ts +28 -0
- package/types/lib/parsers/onnx.d.ts.map +1 -0
- package/types/lib/parsers/safetensors.d.ts +22 -0
- package/types/lib/parsers/safetensors.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts +3 -2
- package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts +33 -0
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/index.d.ts +10 -1
- package/types/lib/validator/index.d.ts.map +1 -1
- package/types/lib/validator/reporters/console.d.ts.map +1 -1
- package/types/lib/validator/reporters/json.d.ts.map +1 -1
- package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
package/lib/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,14 @@ import {
|
|
|
9
18
|
safeMkdtempSync,
|
|
10
19
|
safeRmSync,
|
|
11
20
|
safeSpawnSync,
|
|
21
|
+
safeWriteChunksSync,
|
|
12
22
|
safeWriteSync,
|
|
13
23
|
} from "../core/fs.js";
|
|
24
|
+
import {
|
|
25
|
+
indexJsonObject,
|
|
26
|
+
MAX_JSON_TEXT_BYTES,
|
|
27
|
+
readJsonRange,
|
|
28
|
+
} from "../parsers/largeJson.js";
|
|
14
29
|
import {
|
|
15
30
|
addDosaiSetValue,
|
|
16
31
|
buildDosaiPurlAliasMap,
|
|
@@ -26,6 +41,15 @@ const DOTNET_LANGUAGES = new Set([
|
|
|
26
41
|
"cs",
|
|
27
42
|
"dotnet",
|
|
28
43
|
"dotnet-framework",
|
|
44
|
+
// Versioned CLI types (`-t dotnet11`): the project-type aliases map them to
|
|
45
|
+
// csharp for dispatch, but gates such as shouldCollectDosaiCrypto inspect the
|
|
46
|
+
// raw options.projectType, so the versioned spellings must resolve here too.
|
|
47
|
+
"dotnet6",
|
|
48
|
+
"dotnet7",
|
|
49
|
+
"dotnet8",
|
|
50
|
+
"dotnet9",
|
|
51
|
+
"dotnet10",
|
|
52
|
+
"dotnet11",
|
|
29
53
|
"f#",
|
|
30
54
|
"fsharp",
|
|
31
55
|
"fs",
|
|
@@ -172,11 +196,318 @@ function safeDosaiPatternPacks(value) {
|
|
|
172
196
|
.join(delimiter);
|
|
173
197
|
}
|
|
174
198
|
|
|
199
|
+
const MAX_DOSAI_EXCLUDES = 256;
|
|
200
|
+
const MAX_BRACE_RANGE = 64;
|
|
201
|
+
const GLOB_ESCAPABLE = "*?[]{}()!+@,";
|
|
202
|
+
// Characters a translated dosai glob may contain. dosai runs without a shell,
|
|
203
|
+
// but keeping shell metacharacters out means a pattern can never change
|
|
204
|
+
// meaning even if that ever changes. Anything else is reported as skipped.
|
|
205
|
+
const SAFE_DOSAI_GLOB = /^[\p{L}\p{N} _.+@#%~=/*?-]+$/u;
|
|
206
|
+
// Characters a dosai executable path may contain, including Windows drive
|
|
207
|
+
// letters and "Program Files (x86)". Shell metacharacters are not allowed.
|
|
208
|
+
const SAFE_DOSAI_EXECUTABLE = /^[\p{L}\p{N} _.,+@#%~=/\\:()-]+$/u;
|
|
209
|
+
const warnedDosaiMessages = new Set();
|
|
210
|
+
const dosaiWithoutExclude = new Set();
|
|
211
|
+
|
|
212
|
+
function warnDosaiOnce(message) {
|
|
213
|
+
if (!warnedDosaiMessages.has(message)) {
|
|
214
|
+
warnedDosaiMessages.add(message);
|
|
215
|
+
console.warn(message);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Find the brace group starting at `open` and return its closing index and the
|
|
221
|
+
* top-level comma positions, or undefined when the brace is never closed.
|
|
222
|
+
*/
|
|
223
|
+
function scanBraceGroup(pattern, open) {
|
|
224
|
+
let depth = 0;
|
|
225
|
+
const commas = [];
|
|
226
|
+
for (let i = open; i < pattern.length; i++) {
|
|
227
|
+
const char = pattern[i];
|
|
228
|
+
if (char === "{") {
|
|
229
|
+
depth++;
|
|
230
|
+
} else if (char === "}") {
|
|
231
|
+
depth--;
|
|
232
|
+
if (depth === 0) {
|
|
233
|
+
return { close: i, commas };
|
|
234
|
+
}
|
|
235
|
+
} else if (char === "," && depth === 1) {
|
|
236
|
+
commas.push(i);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function braceAlternatives(pattern, open, group) {
|
|
243
|
+
const body = pattern.slice(open + 1, group.close);
|
|
244
|
+
if (group.commas.length) {
|
|
245
|
+
const bounds = [open, ...group.commas, group.close];
|
|
246
|
+
const alternatives = [];
|
|
247
|
+
for (let i = 0; i < bounds.length - 1; i++) {
|
|
248
|
+
alternatives.push(pattern.slice(bounds[i] + 1, bounds[i + 1]));
|
|
249
|
+
}
|
|
250
|
+
return alternatives;
|
|
251
|
+
}
|
|
252
|
+
// minimatch also expands small integer ranges such as {1..3}.
|
|
253
|
+
const rangeParts = body.split("..");
|
|
254
|
+
if (
|
|
255
|
+
rangeParts.length === 2 &&
|
|
256
|
+
/^-?\d+$/.test(rangeParts[0]) &&
|
|
257
|
+
/^-?\d+$/.test(rangeParts[1])
|
|
258
|
+
) {
|
|
259
|
+
const from = Number.parseInt(rangeParts[0], 10);
|
|
260
|
+
const to = Number.parseInt(rangeParts[1], 10);
|
|
261
|
+
if (Math.abs(to - from) < MAX_BRACE_RANGE) {
|
|
262
|
+
const step = from <= to ? 1 : -1;
|
|
263
|
+
const alternatives = [];
|
|
264
|
+
for (let value = from; value !== to + step; value += step) {
|
|
265
|
+
alternatives.push(String(value));
|
|
266
|
+
}
|
|
267
|
+
return alternatives;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Expand brace groups the way minimatch does. A group without a comma or a
|
|
275
|
+
* numeric range, and a group written as "${...}", stays literal text.
|
|
276
|
+
*/
|
|
277
|
+
function expandGlobBraces(pattern, limit = MAX_DOSAI_EXCLUDES) {
|
|
278
|
+
let open = pattern.indexOf("{");
|
|
279
|
+
while (open !== -1) {
|
|
280
|
+
const group = scanBraceGroup(pattern, open);
|
|
281
|
+
if (!group) {
|
|
282
|
+
return [pattern];
|
|
283
|
+
}
|
|
284
|
+
const alternatives =
|
|
285
|
+
pattern[open - 1] === "$"
|
|
286
|
+
? undefined
|
|
287
|
+
: braceAlternatives(pattern, open, group);
|
|
288
|
+
if (alternatives) {
|
|
289
|
+
const head = pattern.slice(0, open);
|
|
290
|
+
const tail = pattern.slice(group.close + 1);
|
|
291
|
+
const expanded = [];
|
|
292
|
+
for (const alternative of alternatives) {
|
|
293
|
+
for (const entry of expandGlobBraces(
|
|
294
|
+
`${head}${alternative}${tail}`,
|
|
295
|
+
limit,
|
|
296
|
+
)) {
|
|
297
|
+
expanded.push(entry);
|
|
298
|
+
if (expanded.length >= limit) {
|
|
299
|
+
return expanded;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return expanded;
|
|
304
|
+
}
|
|
305
|
+
open = pattern.indexOf("{", group.close + 1);
|
|
306
|
+
}
|
|
307
|
+
return [pattern];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Normalise separators the way the atom exclude filter does: a backslash
|
|
312
|
+
* before a glob character is an escape, any other backslash is a Windows path
|
|
313
|
+
* separator. Returns undefined for escapes, which dosai cannot express.
|
|
314
|
+
*/
|
|
315
|
+
function normalizeDosaiGlobSeparators(pattern) {
|
|
316
|
+
let normalized = "";
|
|
317
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
318
|
+
const char = pattern[i];
|
|
319
|
+
if (char !== "\\") {
|
|
320
|
+
normalized += char;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (GLOB_ESCAPABLE.includes(pattern[i + 1] || "")) {
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
normalized += "/";
|
|
327
|
+
}
|
|
328
|
+
return normalized;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function hasUnsupportedGlobSyntax(glob) {
|
|
332
|
+
if (glob.includes("[") || glob.includes("]")) {
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
for (let i = 1; i < glob.length; i++) {
|
|
336
|
+
if (glob[i] === "(" && "@?+*!".includes(glob[i - 1])) {
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Rebase an absolute glob onto one of the scanned roots. Matching ignores case
|
|
345
|
+
* on macOS and Windows, as cdxgen's own glob does there.
|
|
346
|
+
*/
|
|
347
|
+
function rebaseAbsoluteGlob(glob, roots) {
|
|
348
|
+
const ignoreCase =
|
|
349
|
+
process.platform === "darwin" || process.platform === "win32";
|
|
350
|
+
const candidate = ignoreCase ? glob.toLowerCase() : glob;
|
|
351
|
+
for (const root of roots) {
|
|
352
|
+
let rootPath = root.replaceAll("\\", "/");
|
|
353
|
+
while (rootPath.endsWith("/")) {
|
|
354
|
+
rootPath = rootPath.slice(0, -1);
|
|
355
|
+
}
|
|
356
|
+
const rootPrefix = `${rootPath}/`;
|
|
357
|
+
const prefix = ignoreCase ? rootPrefix.toLowerCase() : rootPrefix;
|
|
358
|
+
if (candidate.startsWith(prefix)) {
|
|
359
|
+
const rest = glob.slice(prefix.length);
|
|
360
|
+
return rest ? rest : undefined;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Translate cdxgen --exclude globs into dosai --exclude globs.
|
|
368
|
+
*
|
|
369
|
+
* Dosai is an evidence analyzer, so the translation follows the exclude
|
|
370
|
+
* filter cdxgen already applies to atom evidence slices
|
|
371
|
+
* (`globPatternsToAtomIgnoreRegex`): a relative pattern matches at any depth,
|
|
372
|
+
* and an absolute pattern is anchored to the scanned directory. Dosai itself
|
|
373
|
+
* uses gitignore conventions, where a pattern with a slash is anchored, so
|
|
374
|
+
* relative patterns get a leading "**\/". Like the atom filter, and unlike
|
|
375
|
+
* cdxgen's file discovery, excluding a directory also excludes everything
|
|
376
|
+
* beneath it.
|
|
377
|
+
*
|
|
378
|
+
* Brace groups and numeric ranges are expanded, and comma separated lists are
|
|
379
|
+
* split, since dosai reads both as literal text. Character classes, extglobs,
|
|
380
|
+
* escapes, negation, and patterns that step outside the scanned directory
|
|
381
|
+
* have no dosai equivalent; they are reported as skipped rather than passed
|
|
382
|
+
* on as literals that match nothing. So is any pattern that still carries a
|
|
383
|
+
* character outside a small allowlist, such as a literal brace group or a
|
|
384
|
+
* shell metacharacter. Dosai matches case-sensitively on Linux.
|
|
385
|
+
*
|
|
386
|
+
* @param {string[]} excludes cdxgen exclude globs
|
|
387
|
+
* @param {string} srcPath Absolute directory dosai scans
|
|
388
|
+
* @returns {{patterns: string[], skipped: string[], truncated: number}} Translated patterns, dropped patterns, and how many were left out by the cap
|
|
389
|
+
*/
|
|
390
|
+
export function toDosaiExcludePatterns(excludes, srcPath) {
|
|
391
|
+
const patterns = new Set();
|
|
392
|
+
const skipped = [];
|
|
393
|
+
let truncated = 0;
|
|
394
|
+
if (!Array.isArray(excludes)) {
|
|
395
|
+
return { patterns: [], skipped, truncated };
|
|
396
|
+
}
|
|
397
|
+
const roots = [srcPath];
|
|
398
|
+
try {
|
|
399
|
+
const realRoot = realpathSync(srcPath);
|
|
400
|
+
if (realRoot !== srcPath) {
|
|
401
|
+
roots.push(realRoot);
|
|
402
|
+
}
|
|
403
|
+
} catch (_err) {
|
|
404
|
+
// The directory may not exist yet in tests; the given path still applies.
|
|
405
|
+
}
|
|
406
|
+
const candidates = [];
|
|
407
|
+
for (const pattern of excludes) {
|
|
408
|
+
if (typeof pattern !== "string" || /[\0\r\n]/.test(pattern)) {
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const entries =
|
|
412
|
+
pattern.includes(",") && !pattern.includes("{")
|
|
413
|
+
? pattern.split(",")
|
|
414
|
+
: [pattern];
|
|
415
|
+
for (const entry of entries) {
|
|
416
|
+
const trimmed = entry.trim();
|
|
417
|
+
if (trimmed) {
|
|
418
|
+
candidates.push(trimmed);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
for (const original of candidates) {
|
|
423
|
+
const normalized = original.startsWith("!")
|
|
424
|
+
? undefined
|
|
425
|
+
: normalizeDosaiGlobSeparators(original);
|
|
426
|
+
if (!normalized) {
|
|
427
|
+
skipped.push(original);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
for (const expanded of expandGlobBraces(normalized)) {
|
|
431
|
+
let glob = expanded;
|
|
432
|
+
while (glob.includes("//")) {
|
|
433
|
+
glob = glob.replaceAll("//", "/");
|
|
434
|
+
}
|
|
435
|
+
if (hasUnsupportedGlobSyntax(glob)) {
|
|
436
|
+
skipped.push(expanded);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (isAbsolute(glob) || /^[A-Za-z]:\//.test(glob)) {
|
|
440
|
+
const rebased = rebaseAbsoluteGlob(glob, roots);
|
|
441
|
+
if (!rebased) {
|
|
442
|
+
skipped.push(expanded);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
glob = `/${rebased}`;
|
|
446
|
+
} else {
|
|
447
|
+
while (glob.startsWith("./")) {
|
|
448
|
+
glob = glob.slice(2);
|
|
449
|
+
}
|
|
450
|
+
if (glob && !glob.startsWith("**/") && glob !== "**") {
|
|
451
|
+
glob = `**/${glob}`;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const segments = glob.split("/").filter(Boolean);
|
|
455
|
+
if (
|
|
456
|
+
!segments.length ||
|
|
457
|
+
segments.includes("..") ||
|
|
458
|
+
segments.every((segment) => segment === ".")
|
|
459
|
+
) {
|
|
460
|
+
skipped.push(expanded);
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (!SAFE_DOSAI_GLOB.test(glob)) {
|
|
464
|
+
skipped.push(expanded);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (patterns.size >= MAX_DOSAI_EXCLUDES) {
|
|
468
|
+
if (!patterns.has(glob)) {
|
|
469
|
+
truncated++;
|
|
470
|
+
}
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
patterns.add(glob);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return { patterns: Array.from(patterns), skipped, truncated };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function dosaiExcludeArgs(excludes, srcPath) {
|
|
480
|
+
const { patterns, skipped, truncated } = toDosaiExcludePatterns(
|
|
481
|
+
excludes,
|
|
482
|
+
srcPath,
|
|
483
|
+
);
|
|
484
|
+
if (skipped.length) {
|
|
485
|
+
warnDosaiOnce(
|
|
486
|
+
`dosai cannot express these exclude patterns and will not apply them: ${skipped.join(", ")}`,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
if (truncated) {
|
|
490
|
+
warnDosaiOnce(
|
|
491
|
+
`Only the first ${MAX_DOSAI_EXCLUDES} exclude patterns are passed to dosai; ${truncated} more were left out.`,
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
return patterns.flatMap((pattern) => ["--exclude", pattern]);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function dosaiRejectedExclude(result) {
|
|
498
|
+
const output = `${result?.stdout || ""}${result?.stderr || ""}`;
|
|
499
|
+
return output.includes("Unrecognized command or argument '--exclude'");
|
|
500
|
+
}
|
|
501
|
+
|
|
175
502
|
function safeDosaiExecutable(value) {
|
|
176
|
-
if (!value || typeof value !== "string"
|
|
503
|
+
if (!value || typeof value !== "string") {
|
|
504
|
+
return undefined;
|
|
505
|
+
}
|
|
506
|
+
const executable = value.trim();
|
|
507
|
+
if (!SAFE_DOSAI_EXECUTABLE.test(executable)) {
|
|
177
508
|
return undefined;
|
|
178
509
|
}
|
|
179
|
-
return
|
|
510
|
+
return executable;
|
|
180
511
|
}
|
|
181
512
|
|
|
182
513
|
/**
|
|
@@ -206,6 +537,144 @@ export function readDosaiJsonFile(jsonFile) {
|
|
|
206
537
|
}
|
|
207
538
|
}
|
|
208
539
|
|
|
540
|
+
// The sections of a dosai methods report that cdxgen reads. The rest (Methods,
|
|
541
|
+
// Properties, Fields, Reachability, DeadCode, ...) only reaches downstream
|
|
542
|
+
// tools through the persisted native report.
|
|
543
|
+
const DOSAI_METHODS_SECTIONS = [
|
|
544
|
+
"Metadata",
|
|
545
|
+
"Dependencies",
|
|
546
|
+
"MethodCalls",
|
|
547
|
+
"AssemblyInformation",
|
|
548
|
+
"CallGraph",
|
|
549
|
+
"PackageReachability",
|
|
550
|
+
"Services",
|
|
551
|
+
"ApiEndpoints",
|
|
552
|
+
"AiComponents",
|
|
553
|
+
];
|
|
554
|
+
|
|
555
|
+
// The file each report returned by readDosaiReport came from, and whether it
|
|
556
|
+
// was trimmed to what cdxgen reads. persistDosaiSemanticsReport copies the
|
|
557
|
+
// file rather than re-serialising a trimmed or oversized report.
|
|
558
|
+
const dosaiReportSources = new WeakMap();
|
|
559
|
+
|
|
560
|
+
function referencedIds(entries, field) {
|
|
561
|
+
const ids = new Set();
|
|
562
|
+
for (const entry of entries || []) {
|
|
563
|
+
for (const id of entry?.[field] || []) {
|
|
564
|
+
ids.add(id);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return ids;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function readDosaiReport(reportFile, options, readTrimmed) {
|
|
571
|
+
if (!reportFile || !safeExistsSync(reportFile)) {
|
|
572
|
+
return undefined;
|
|
573
|
+
}
|
|
574
|
+
try {
|
|
575
|
+
const size = statSync(reportFile).size;
|
|
576
|
+
let report;
|
|
577
|
+
let trimmed = false;
|
|
578
|
+
if (size <= (options.maxTextBytes ?? MAX_JSON_TEXT_BYTES)) {
|
|
579
|
+
report = JSON.parse(readFileSync(reportFile, "utf-8"));
|
|
580
|
+
} else {
|
|
581
|
+
// dotnet/efcore produces a 1.9 GB methods report (issue 1033). Read it
|
|
582
|
+
// in bounded runs and keep only what cdxgen uses, which is a small
|
|
583
|
+
// fraction of the file.
|
|
584
|
+
if (DEBUG_MODE) {
|
|
585
|
+
console.log(
|
|
586
|
+
`The dosai report "${reportFile}" is ${size} bytes, more than one JavaScript string can hold, so only the parts cdxgen uses are read from it.`,
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
const index = indexJsonObject(reportFile, options);
|
|
590
|
+
report = {};
|
|
591
|
+
readTrimmed(report, (section, spec) => {
|
|
592
|
+
const range = index.get(section);
|
|
593
|
+
if (range) {
|
|
594
|
+
report[section] = readJsonRange(reportFile, range, spec, options);
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
trimmed = true;
|
|
598
|
+
}
|
|
599
|
+
if (report && typeof report === "object") {
|
|
600
|
+
dosaiReportSources.set(report, { file: reportFile, trimmed });
|
|
601
|
+
}
|
|
602
|
+
return report;
|
|
603
|
+
} catch (err) {
|
|
604
|
+
if (DEBUG_MODE) {
|
|
605
|
+
console.log(
|
|
606
|
+
`Unable to read the dosai report "${reportFile}": ${err.message}`,
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
return undefined;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Read a dosai methods report of any size.
|
|
615
|
+
*
|
|
616
|
+
* A report that fits in one JavaScript string is parsed whole. A larger one is
|
|
617
|
+
* read in bounded runs, keeping the requested sections, the CallGraph nodes and
|
|
618
|
+
* edges that PackageReachability references, and the MethodCalls accepted by
|
|
619
|
+
* `keepMethodCall` (none when it is omitted). Nothing cdxgen reads is lost.
|
|
620
|
+
*
|
|
621
|
+
* @param {string} reportFile Path to the dosai methods JSON
|
|
622
|
+
* @param {Object} [options] Options
|
|
623
|
+
* @param {string[]} [options.sections] Sections to keep from a large report (default: every section cdxgen reads)
|
|
624
|
+
* @param {Function} [options.keepMethodCall] Predicate choosing the MethodCalls to keep from a large report
|
|
625
|
+
* @param {number} [options.maxTextBytes] Largest report parsed whole, for tests
|
|
626
|
+
* @returns {Object|undefined} Parsed report, or undefined when missing or invalid
|
|
627
|
+
*/
|
|
628
|
+
export function readDosaiMethodsReport(reportFile, options = {}) {
|
|
629
|
+
const sections = options.sections || DOSAI_METHODS_SECTIONS;
|
|
630
|
+
return readDosaiReport(reportFile, options, (report, read) => {
|
|
631
|
+
for (const section of sections) {
|
|
632
|
+
if (section !== "CallGraph" && section !== "MethodCalls") {
|
|
633
|
+
read(section);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (sections.includes("MethodCalls") && options.keepMethodCall) {
|
|
637
|
+
read("MethodCalls", { filter: options.keepMethodCall });
|
|
638
|
+
}
|
|
639
|
+
if (sections.includes("CallGraph")) {
|
|
640
|
+
// CallGraph precedes PackageReachability in the file, which is why the
|
|
641
|
+
// sections are read by range rather than in one pass.
|
|
642
|
+
const edgeIds = referencedIds(report.PackageReachability, "EdgeIds");
|
|
643
|
+
const nodeIds = referencedIds(report.PackageReachability, "NodeIds");
|
|
644
|
+
read("CallGraph", {
|
|
645
|
+
members: {
|
|
646
|
+
Edges: { filter: (edge) => edgeIds.has(edge?.Id) },
|
|
647
|
+
Nodes: { filter: (node) => nodeIds.has(node?.Id) },
|
|
648
|
+
},
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Read a dosai data-flow report of any size.
|
|
656
|
+
*
|
|
657
|
+
* A report that fits in one JavaScript string is parsed whole. A larger one
|
|
658
|
+
* keeps Metadata, Slices, PackageReachability, and the Nodes those reference.
|
|
659
|
+
*
|
|
660
|
+
* @param {string} reportFile Path to the dosai data-flow JSON
|
|
661
|
+
* @param {Object} [options] Options
|
|
662
|
+
* @param {number} [options.maxTextBytes] Largest report parsed whole, for tests
|
|
663
|
+
* @returns {Object|undefined} Parsed report, or undefined when missing or invalid
|
|
664
|
+
*/
|
|
665
|
+
export function readDosaiDataFlowReport(reportFile, options = {}) {
|
|
666
|
+
return readDosaiReport(reportFile, options, (report, read) => {
|
|
667
|
+
read("Metadata");
|
|
668
|
+
read("Slices");
|
|
669
|
+
read("PackageReachability");
|
|
670
|
+
const nodeIds = referencedIds(report.Slices, "NodeIds");
|
|
671
|
+
for (const id of referencedIds(report.PackageReachability, "NodeIds")) {
|
|
672
|
+
nodeIds.add(id);
|
|
673
|
+
}
|
|
674
|
+
read("Nodes", { filter: (node) => nodeIds.has(node?.Id) });
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
209
678
|
/**
|
|
210
679
|
* Run a dosai subcommand ("methods", "dataflows", or "crypto") against a source
|
|
211
680
|
* tree and write its JSON output to the given file.
|
|
@@ -213,7 +682,7 @@ export function readDosaiJsonFile(jsonFile) {
|
|
|
213
682
|
* @param {string} command Dosai subcommand to execute
|
|
214
683
|
* @param {string} src Source directory to analyze
|
|
215
684
|
* @param {string} outputFile Path where the dosai JSON output is written
|
|
216
|
-
* @param {Object} [options] Options carrying dosaiCommand, dataFlowPatterns, or
|
|
685
|
+
* @param {Object} [options] Options carrying dosaiCommand, dataFlowPatterns, patternPacks, or exclude globs
|
|
217
686
|
* @returns {boolean} True when the command succeeded and produced the output file, false otherwise
|
|
218
687
|
*/
|
|
219
688
|
export function runDosaiCommand(command, src, outputFile, options = {}) {
|
|
@@ -245,13 +714,32 @@ export function runDosaiCommand(command, src, outputFile, options = {}) {
|
|
|
245
714
|
} else if (command === "crypto") {
|
|
246
715
|
args.push("--format", "dosai");
|
|
247
716
|
}
|
|
717
|
+
const excludeArgs = dosaiWithoutExclude.has(executable)
|
|
718
|
+
? []
|
|
719
|
+
: dosaiExcludeArgs(options.exclude, srcPath);
|
|
248
720
|
if (DEBUG_MODE) {
|
|
249
|
-
console.log("Executing", executable, args.join(" "));
|
|
721
|
+
console.log("Executing", executable, [...args, ...excludeArgs].join(" "));
|
|
250
722
|
}
|
|
251
|
-
|
|
723
|
+
let result = safeSpawnSync(executable, [...args, ...excludeArgs], {
|
|
252
724
|
cwd: srcPath,
|
|
253
725
|
shell: false,
|
|
254
726
|
});
|
|
727
|
+
// Dosai releases that predate --exclude reject it. Rerun without it so a
|
|
728
|
+
// user supplied exclude never turns into a missing slice.
|
|
729
|
+
if (
|
|
730
|
+
excludeArgs.length &&
|
|
731
|
+
result?.status !== 0 &&
|
|
732
|
+
dosaiRejectedExclude(result)
|
|
733
|
+
) {
|
|
734
|
+
dosaiWithoutExclude.add(executable);
|
|
735
|
+
warnDosaiOnce(
|
|
736
|
+
"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.",
|
|
737
|
+
);
|
|
738
|
+
result = safeSpawnSync(executable, args, {
|
|
739
|
+
cwd: srcPath,
|
|
740
|
+
shell: false,
|
|
741
|
+
});
|
|
742
|
+
}
|
|
255
743
|
if (dosaiSdkMessage(result)) {
|
|
256
744
|
recordDegradation("dotnet.sdk.missing", {
|
|
257
745
|
ecosystem: "csharp",
|
|
@@ -360,6 +848,38 @@ function buildCombinedDosaiReport(methodsSlice, dataFlowSlice) {
|
|
|
360
848
|
};
|
|
361
849
|
}
|
|
362
850
|
|
|
851
|
+
function* fileChunks(filePath) {
|
|
852
|
+
const fd = openSync(filePath, "r");
|
|
853
|
+
try {
|
|
854
|
+
const buffer = Buffer.allocUnsafe(8 * 1024 * 1024);
|
|
855
|
+
for (;;) {
|
|
856
|
+
const count = readSync(fd, buffer, 0, buffer.length, null);
|
|
857
|
+
if (!count) {
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
yield buffer.subarray(0, count);
|
|
861
|
+
}
|
|
862
|
+
} finally {
|
|
863
|
+
closeSync(fd);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// The combined report as JSON text in pieces, with each part copied from its
|
|
868
|
+
// native file when there is one. Keys and their order match
|
|
869
|
+
// buildCombinedDosaiReport.
|
|
870
|
+
function* combinedDosaiReportChunks(combined, parts) {
|
|
871
|
+
yield `{"Metadata":${JSON.stringify(combined.Metadata)}`;
|
|
872
|
+
for (const [key, source] of parts) {
|
|
873
|
+
yield `,${JSON.stringify(key)}:`;
|
|
874
|
+
if (source) {
|
|
875
|
+
yield* fileChunks(source.file);
|
|
876
|
+
} else {
|
|
877
|
+
yield JSON.stringify(combined[key]);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
yield "}";
|
|
881
|
+
}
|
|
882
|
+
|
|
363
883
|
/**
|
|
364
884
|
* Persist the combined native dosai report to options.semanticsSlicesFile.
|
|
365
885
|
*
|
|
@@ -390,16 +910,42 @@ export function persistDosaiSemanticsReport(
|
|
|
390
910
|
return undefined;
|
|
391
911
|
}
|
|
392
912
|
const combined = buildCombinedDosaiReport(methodsSlice, dataFlowSlice);
|
|
913
|
+
const methodsSource = dosaiReportSources.get(methodsSlice);
|
|
914
|
+
const dataFlowSource = dosaiReportSources.get(dataFlowSlice);
|
|
393
915
|
try {
|
|
394
|
-
|
|
916
|
+
let text;
|
|
917
|
+
if (!methodsSource?.trimmed && !dataFlowSource?.trimmed) {
|
|
918
|
+
try {
|
|
919
|
+
text = JSON.stringify(combined);
|
|
920
|
+
} catch (err) {
|
|
921
|
+
// Too large for one string: write it in parts below instead.
|
|
922
|
+
if (!(err instanceof RangeError)) {
|
|
923
|
+
throw err;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
if (text !== undefined) {
|
|
928
|
+
safeWriteSync(durablePath, text);
|
|
929
|
+
} else {
|
|
930
|
+
// A trimmed report lacks the sections cdxgen skipped, so each part is
|
|
931
|
+
// copied from its native file when it has one, and serialised on its
|
|
932
|
+
// own otherwise.
|
|
933
|
+
safeWriteChunksSync(
|
|
934
|
+
durablePath,
|
|
935
|
+
combinedDosaiReportChunks(combined, [
|
|
936
|
+
["methods", methodsSource],
|
|
937
|
+
["dataflows", dataFlowSource],
|
|
938
|
+
]),
|
|
939
|
+
);
|
|
940
|
+
}
|
|
395
941
|
} catch (err) {
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
recordDegradation("dosai.semantics.
|
|
942
|
+
// A part with no native file can still be too large to serialise, and the
|
|
943
|
+
// write can fail. The SBOM evidence is already collected by this point, so
|
|
944
|
+
// skip the durable copy instead of losing the whole analysis.
|
|
945
|
+
recordDegradation("dosai.semantics.persist-failed", {
|
|
400
946
|
ecosystem: "csharp",
|
|
401
947
|
tool: "dosai",
|
|
402
|
-
impact: "
|
|
948
|
+
impact: "none",
|
|
403
949
|
command: `persistDosaiSemanticsReport ${durablePath}`,
|
|
404
950
|
detail: `The combined dosai report could not be persisted: ${err.message}. The SBOM evidence is unaffected.`,
|
|
405
951
|
});
|
|
@@ -429,6 +975,74 @@ export function resolveComponentPurl(purl, purlAliasMap) {
|
|
|
429
975
|
return resolveDosaiComponentPurl(purl, purlAliasMap);
|
|
430
976
|
}
|
|
431
977
|
|
|
978
|
+
/**
|
|
979
|
+
* Copy one dosai PackageReachability fact onto a component as properties.
|
|
980
|
+
*
|
|
981
|
+
* The occurrence/location consumers of PackageReachability read SourceLocations, EdgeIds,
|
|
982
|
+
* and NodeIds only, so dosai's Confidence, EvidenceKinds, ReachabilityKind, and
|
|
983
|
+
* ConfidenceReasons never reached the BOM: an unbuilt tree (Low confidence, unresolved
|
|
984
|
+
* evidence) was indistinguishable from a package that really is only imported. These
|
|
985
|
+
* properties carry that distinction to BOM consumers. They use the consumer-facing
|
|
986
|
+
* `cdx:dosai:reachability:*` namespace - not `internal:`, which is cdxgen's private
|
|
987
|
+
* bookkeeping namespace stripped or ignored by downstream tooling.
|
|
988
|
+
*
|
|
989
|
+
* @param {Object} component BOM component object (mutated; properties created on demand)
|
|
990
|
+
* @param {Object} reachability dosai PackageReachability entry
|
|
991
|
+
*/
|
|
992
|
+
export function addDosaiReachabilityProperties(component, reachability) {
|
|
993
|
+
if (!component || !reachability) {
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
component.properties = component.properties || [];
|
|
997
|
+
const values = {
|
|
998
|
+
"cdx:dosai:reachability:kind": reachability.ReachabilityKind,
|
|
999
|
+
"cdx:dosai:reachability:confidence": reachability.Confidence,
|
|
1000
|
+
"cdx:dosai:reachability:evidence": (reachability.EvidenceKinds || []).join(
|
|
1001
|
+
", ",
|
|
1002
|
+
),
|
|
1003
|
+
"cdx:dosai:reachability:reasons": (
|
|
1004
|
+
reachability.ConfidenceReasons || []
|
|
1005
|
+
).join(" | "),
|
|
1006
|
+
};
|
|
1007
|
+
for (const [name, value] of Object.entries(values)) {
|
|
1008
|
+
if (!value) {
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
// First fact wins so a repeated enrichment pass cannot stack duplicate properties.
|
|
1012
|
+
if (component.properties.some((p) => p.name === name)) {
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
component.properties.push({ name, value });
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Attach dosai PackageReachability confidence facts to matching BOM components.
|
|
1021
|
+
*
|
|
1022
|
+
* @param {Object} methodsSlice Parsed dosai methods slice JSON
|
|
1023
|
+
* @param {Object[]} [components] BOM components used to resolve purl aliases and mutate
|
|
1024
|
+
* @returns {number} Number of distinct components enriched
|
|
1025
|
+
*/
|
|
1026
|
+
export function applyDosaiReachabilityEvidence(methodsSlice, components = []) {
|
|
1027
|
+
const purlAliasMap = buildPurlAliasMap(components);
|
|
1028
|
+
const componentsByPurl = new Map(
|
|
1029
|
+
components
|
|
1030
|
+
.filter((component) => component?.purl)
|
|
1031
|
+
.map((component) => [component.purl, component]),
|
|
1032
|
+
);
|
|
1033
|
+
const enriched = new Set();
|
|
1034
|
+
for (const reachability of methodsSlice?.PackageReachability || []) {
|
|
1035
|
+
const purl = resolveComponentPurl(reachability.Purl, purlAliasMap);
|
|
1036
|
+
const component = purl && componentsByPurl.get(purl);
|
|
1037
|
+
if (!component) {
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
1040
|
+
addDosaiReachabilityProperties(component, reachability);
|
|
1041
|
+
enriched.add(component);
|
|
1042
|
+
}
|
|
1043
|
+
return enriched.size;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
432
1046
|
/**
|
|
433
1047
|
* Map a dosai methods slice to per-purl occurrence evidence.
|
|
434
1048
|
*
|