@cdxgen/cdxgen 13.2.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 +28 -12
- package/bin/cdxgen.js +227 -119
- package/bin/evinse.js +5 -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/remediations.json +24 -0
- package/lib/cli/bomAssembly.js +105 -15
- package/lib/cli/cliOptions.js +93 -1
- package/lib/cli/index.js +43 -4
- package/lib/cli/jsBom.js +49 -24
- package/lib/cli/jvmBom.js +46 -12
- package/lib/cli/managedBom.js +112 -41
- package/lib/cli/nativeBom.js +125 -14
- package/lib/core/env.js +5 -6
- package/lib/core/fs.js +174 -6
- package/lib/core/metadataProperties.js +97 -0
- package/lib/core/projectConfig.js +31 -4
- package/lib/ecosystems/bunutils.js +269 -101
- package/lib/ecosystems/caxa.js +434 -2
- package/lib/ecosystems/cmakeResolver.js +66 -0
- package/lib/ecosystems/cppEvidence.js +64 -31
- package/lib/ecosystems/ecosystems.js +19 -8
- package/lib/ecosystems/gradleutils.js +42 -7
- package/lib/ecosystems/jsEvidence.js +19 -20
- package/lib/ecosystems/kosi.js +10 -3
- package/lib/ecosystems/npmutils.js +110 -9
- package/lib/ecosystems/parsers-dotnet.js +51 -10
- package/lib/ecosystems/parsers-go.js +47 -9
- package/lib/ecosystems/parsers-js.js +171 -50
- package/lib/ecosystems/parsers-misc.js +747 -161
- package/lib/ecosystems/parsers-python.js +26 -14
- package/lib/ecosystems/parsers-rust.js +24 -18
- package/lib/ecosystems/rubyutils.js +1426 -166
- package/lib/ecosystems/rusi.js +32 -1
- package/lib/evinser/evinser.js +436 -128
- package/lib/evinser/swiftsem.js +1914 -302
- package/lib/helpers/bomSigner.js +652 -221
- package/lib/inventory/atomUtils.js +568 -31
- package/lib/inventory/cdxrs.js +103 -12
- package/lib/inventory/depsUtils.js +183 -11
- package/lib/inventory/display.js +4 -5
- package/lib/inventory/dosai.js +550 -13
- package/lib/inventory/dynamic.js +39 -1
- package/lib/inventory/evidenceUtils.js +14 -6
- package/lib/inventory/fetchBatch.js +116 -17
- package/lib/inventory/protobom.js +8 -1
- package/lib/inventory/purl.js +152 -3
- package/lib/inventory/sbomDocument.js +2 -70
- package/lib/inventory/source.js +37 -19
- 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/largeJson.js +454 -0
- package/lib/parsers/npmrc.js +19 -1
- package/lib/stages/postgen/postgen.js +68 -15
- 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 +20 -20
- 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.map +1 -1
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- 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/ecosystems/bunutils.d.ts +12 -5
- package/types/lib/ecosystems/bunutils.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 +23 -0
- package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -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 +20 -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 +42 -0
- package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- 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-misc.d.ts +67 -3
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- 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/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 +59 -9
- 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 +106 -13
- package/types/lib/helpers/bomSigner.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/cdxrs.d.ts +8 -3
- package/types/lib/inventory/cdxrs.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 +64 -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/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- 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/sbomDocument.d.ts.map +1 -1
- package/types/lib/inventory/source.d.ts.map +1 -1
- 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/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/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,
|
|
@@ -181,11 +196,318 @@ function safeDosaiPatternPacks(value) {
|
|
|
181
196
|
.join(delimiter);
|
|
182
197
|
}
|
|
183
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
|
+
|
|
184
502
|
function safeDosaiExecutable(value) {
|
|
185
|
-
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)) {
|
|
186
508
|
return undefined;
|
|
187
509
|
}
|
|
188
|
-
return
|
|
510
|
+
return executable;
|
|
189
511
|
}
|
|
190
512
|
|
|
191
513
|
/**
|
|
@@ -215,6 +537,144 @@ export function readDosaiJsonFile(jsonFile) {
|
|
|
215
537
|
}
|
|
216
538
|
}
|
|
217
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
|
+
|
|
218
678
|
/**
|
|
219
679
|
* Run a dosai subcommand ("methods", "dataflows", or "crypto") against a source
|
|
220
680
|
* tree and write its JSON output to the given file.
|
|
@@ -222,7 +682,7 @@ export function readDosaiJsonFile(jsonFile) {
|
|
|
222
682
|
* @param {string} command Dosai subcommand to execute
|
|
223
683
|
* @param {string} src Source directory to analyze
|
|
224
684
|
* @param {string} outputFile Path where the dosai JSON output is written
|
|
225
|
-
* @param {Object} [options] Options carrying dosaiCommand, dataFlowPatterns, or
|
|
685
|
+
* @param {Object} [options] Options carrying dosaiCommand, dataFlowPatterns, patternPacks, or exclude globs
|
|
226
686
|
* @returns {boolean} True when the command succeeded and produced the output file, false otherwise
|
|
227
687
|
*/
|
|
228
688
|
export function runDosaiCommand(command, src, outputFile, options = {}) {
|
|
@@ -254,13 +714,32 @@ export function runDosaiCommand(command, src, outputFile, options = {}) {
|
|
|
254
714
|
} else if (command === "crypto") {
|
|
255
715
|
args.push("--format", "dosai");
|
|
256
716
|
}
|
|
717
|
+
const excludeArgs = dosaiWithoutExclude.has(executable)
|
|
718
|
+
? []
|
|
719
|
+
: dosaiExcludeArgs(options.exclude, srcPath);
|
|
257
720
|
if (DEBUG_MODE) {
|
|
258
|
-
console.log("Executing", executable, args.join(" "));
|
|
721
|
+
console.log("Executing", executable, [...args, ...excludeArgs].join(" "));
|
|
259
722
|
}
|
|
260
|
-
|
|
723
|
+
let result = safeSpawnSync(executable, [...args, ...excludeArgs], {
|
|
261
724
|
cwd: srcPath,
|
|
262
725
|
shell: false,
|
|
263
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
|
+
}
|
|
264
743
|
if (dosaiSdkMessage(result)) {
|
|
265
744
|
recordDegradation("dotnet.sdk.missing", {
|
|
266
745
|
ecosystem: "csharp",
|
|
@@ -369,6 +848,38 @@ function buildCombinedDosaiReport(methodsSlice, dataFlowSlice) {
|
|
|
369
848
|
};
|
|
370
849
|
}
|
|
371
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
|
+
|
|
372
883
|
/**
|
|
373
884
|
* Persist the combined native dosai report to options.semanticsSlicesFile.
|
|
374
885
|
*
|
|
@@ -399,16 +910,42 @@ export function persistDosaiSemanticsReport(
|
|
|
399
910
|
return undefined;
|
|
400
911
|
}
|
|
401
912
|
const combined = buildCombinedDosaiReport(methodsSlice, dataFlowSlice);
|
|
913
|
+
const methodsSource = dosaiReportSources.get(methodsSlice);
|
|
914
|
+
const dataFlowSource = dosaiReportSources.get(dataFlowSlice);
|
|
402
915
|
try {
|
|
403
|
-
|
|
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
|
+
}
|
|
404
941
|
} catch (err) {
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
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", {
|
|
409
946
|
ecosystem: "csharp",
|
|
410
947
|
tool: "dosai",
|
|
411
|
-
impact: "
|
|
948
|
+
impact: "none",
|
|
412
949
|
command: `persistDosaiSemanticsReport ${durablePath}`,
|
|
413
950
|
detail: `The combined dosai report could not be persisted: ${err.message}. The SBOM evidence is unaffected.`,
|
|
414
951
|
});
|
package/lib/inventory/dynamic.js
CHANGED
|
@@ -34,6 +34,7 @@ export async function buildDynamicComponents(
|
|
|
34
34
|
const result = await executeAndTrace(commandStr, workingDir, traceOptions);
|
|
35
35
|
const libPaths = result.libPaths || [];
|
|
36
36
|
const httpAccessEntries = result.httpAccessEntries || [];
|
|
37
|
+
const egressEntries = result.egressEntries || [];
|
|
37
38
|
const cryptoComponents = result.cryptoComponents || [];
|
|
38
39
|
const components = [];
|
|
39
40
|
|
|
@@ -141,5 +142,42 @@ export async function buildDynamicComponents(
|
|
|
141
142
|
};
|
|
142
143
|
});
|
|
143
144
|
|
|
144
|
-
|
|
145
|
+
// Egress seen by the safer-exec proxy: hostnames the command connected to
|
|
146
|
+
// (allowed) or tried to reach (denied). This works on every platform,
|
|
147
|
+
// including macOS where eBPF URL tracing is unavailable.
|
|
148
|
+
const servicesByName = new Map(services.map((svc) => [svc.name, svc]));
|
|
149
|
+
for (const egress of egressEntries) {
|
|
150
|
+
const name = `dynamic-${egress.host}-${egress.port}`;
|
|
151
|
+
const defaultPort = egress.protocol === "http" ? 80 : 443;
|
|
152
|
+
const endpoint = `${egress.protocol}://${egress.host}${egress.port !== defaultPort ? `:${egress.port}` : ""}`;
|
|
153
|
+
let svc = servicesByName.get(name);
|
|
154
|
+
if (!svc) {
|
|
155
|
+
svc = {
|
|
156
|
+
name,
|
|
157
|
+
bomRef: `urn:service:dynamic:${name}`,
|
|
158
|
+
endpoints: [],
|
|
159
|
+
properties: [],
|
|
160
|
+
};
|
|
161
|
+
servicesByName.set(name, svc);
|
|
162
|
+
services.push(svc);
|
|
163
|
+
}
|
|
164
|
+
if (!svc.endpoints.some((e) => e.startsWith(endpoint))) {
|
|
165
|
+
svc.endpoints.push(endpoint);
|
|
166
|
+
svc.endpoints.sort();
|
|
167
|
+
}
|
|
168
|
+
if (
|
|
169
|
+
!svc.properties.some(
|
|
170
|
+
(p) =>
|
|
171
|
+
p.name === "cdx:dynamic:egressDecision" &&
|
|
172
|
+
p.value === egress.decision,
|
|
173
|
+
)
|
|
174
|
+
) {
|
|
175
|
+
svc.properties.push({
|
|
176
|
+
name: "cdx:dynamic:egressDecision",
|
|
177
|
+
value: egress.decision,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { components, services, dryRunSummary: result.dryRunSummary };
|
|
145
183
|
}
|