@cdxgen/cdxgen 13.0.0 → 13.1.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 +26 -13
- package/bin/audit.js +6 -0
- package/bin/cdxgen.js +140 -8
- package/bin/convert.js +7 -0
- package/bin/evinse.js +1 -1
- package/bin/hbom.js +13 -4
- package/bin/tracebom.js +9 -1
- package/data/component-tags.json +8 -1
- package/data/frameworks-list.json +86 -1
- package/data/remediations.json +1144 -0
- package/data/rules/asar-archives.yaml +1 -1
- package/data/rules/build-fidelity.yaml +782 -0
- package/data/rules/package-integrity.yaml +13 -13
- package/lib/audit/index.js +48 -4
- package/lib/audit/scoring.js +2 -8
- package/lib/cli/bomAssembly.js +83 -15
- package/lib/cli/cliOptions.js +124 -7
- package/lib/cli/index.js +76 -48
- package/lib/cli/jsBom.js +32 -3
- package/lib/cli/jvmBom.js +1494 -1315
- package/lib/cli/managedBom.js +97 -28
- package/lib/cli/nativeBom.js +189 -10
- package/lib/core/buildLedger.js +799 -0
- package/lib/core/deferredExit.js +141 -0
- package/lib/core/env.js +69 -1
- package/lib/core/fs.js +60 -0
- package/lib/core/logger.js +76 -25
- package/lib/core/severity.js +16 -0
- package/lib/core/syncFileWriter.js +106 -0
- package/lib/core/ui.js +46 -2
- package/lib/ecosystems/ecosystems.js +9 -0
- package/lib/ecosystems/gradleutils.js +355 -15
- package/lib/ecosystems/parsers-js.js +26 -0
- package/lib/ecosystems/parsers-jvm.js +9 -0
- package/lib/ecosystems/parsers-misc.js +16 -2
- package/lib/ecosystems/parsers-python.js +171 -35
- package/lib/ecosystems/parsers-rust.js +12 -2
- package/lib/ecosystems/pypiClassifiers.js +326 -0
- package/lib/ecosystems/pythonutils.js +49 -1
- package/lib/ecosystems/rubyutils.js +13 -0
- package/lib/ecosystems/utils.js +9 -1
- package/lib/evinser/evinser.js +30 -12
- package/lib/helpers/remote/dependency-track.js +81 -14
- package/lib/inventory/auditCategories.js +9 -0
- package/lib/inventory/depsUtils.js +72 -0
- package/lib/inventory/dosai.js +431 -22
- package/lib/inventory/envcontext.js +211 -19
- package/lib/inventory/fetchBatch.js +57 -0
- package/lib/inventory/jvmToolEnv.js +593 -0
- package/lib/inventory/osinfo.js +123 -16
- package/lib/inventory/table.js +9 -5
- package/lib/inventory/toolRequirements.js +903 -0
- package/lib/managers/binary.js +216 -40
- package/lib/managers/docker.js +26 -5
- package/lib/server/server.js +1 -1
- package/lib/stages/postgen/annotationBuilder.js +97 -0
- package/lib/stages/postgen/auditBom.js +76 -79
- package/lib/stages/postgen/introspection/annotate.js +320 -0
- package/lib/stages/postgen/introspection/commandFacts.js +293 -0
- package/lib/stages/postgen/introspection/emit.js +192 -0
- package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
- package/lib/stages/postgen/introspection/markers.js +227 -0
- package/lib/stages/postgen/introspection/reflect.js +1214 -0
- package/lib/stages/postgen/introspection/report.js +1208 -0
- package/lib/stages/postgen/introspection/score.js +1067 -0
- package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
- package/lib/stages/postgen/parentEdge.js +156 -0
- package/lib/stages/postgen/postgen.js +115 -2
- package/lib/stages/postgen/ruleEngine.js +17 -0
- package/lib/stages/pregen/envAudit.js +42 -2
- package/lib/stages/pregen/pregen.js +1171 -6
- package/lib/validator/bomValidator.js +11 -2
- package/package.json +14 -14
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/audit/scoring.d.ts +2 -8
- package/types/lib/audit/scoring.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts +14 -0
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +2 -1
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +8 -0
- 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/buildLedger.d.ts +272 -0
- package/types/lib/core/buildLedger.d.ts.map +1 -0
- package/types/lib/core/deferredExit.d.ts +84 -0
- package/types/lib/core/deferredExit.d.ts.map +1 -0
- package/types/lib/core/env.d.ts +22 -1
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +21 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/logger.d.ts +14 -1
- package/types/lib/core/logger.d.ts.map +1 -1
- package/types/lib/core/severity.d.ts +11 -0
- package/types/lib/core/severity.d.ts.map +1 -0
- package/types/lib/core/syncFileWriter.d.ts +33 -0
- package/types/lib/core/syncFileWriter.d.ts.map +1 -0
- package/types/lib/core/ui.d.ts +1 -1
- package/types/lib/core/ui.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +33 -0
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- 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/pypiClassifiers.d.ts +80 -0
- package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
- package/types/lib/ecosystems/pythonutils.d.ts +20 -0
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/utils.d.ts +4 -4
- package/types/lib/ecosystems/utils.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +5 -1
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +8 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/depsUtils.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +20 -0
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +9 -0
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/jvmToolEnv.d.ts +189 -0
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
- package/types/lib/inventory/osinfo.d.ts +83 -2
- package/types/lib/inventory/osinfo.d.ts.map +1 -1
- package/types/lib/inventory/table.d.ts +4 -1
- package/types/lib/inventory/table.d.ts.map +1 -1
- package/types/lib/inventory/toolRequirements.d.ts +216 -0
- package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
- package/types/lib/managers/binary.d.ts +71 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
- package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
- package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
- package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
- package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
- package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts +57 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
- package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
- package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
- package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
- package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
- package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
- package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
- package/types/lib/stages/postgen/postgen.d.ts +2 -2
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts +131 -0
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
package/lib/inventory/dosai.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { basename, delimiter, join, resolve } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { DEBUG_MODE } from "../core/activity.js";
|
|
5
|
+
import { recordDegradation } from "../core/buildLedger.js";
|
|
5
6
|
import {
|
|
6
7
|
getTmpDir,
|
|
7
8
|
safeExistsSync,
|
|
@@ -59,6 +60,39 @@ function frameFromDosaiNode(node) {
|
|
|
59
60
|
};
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Whether this slice's ApiEndpoints carry a resolved route in `Path`.
|
|
65
|
+
*
|
|
66
|
+
* Schema 4.0.0 introduced `Path` as the resolved route ([controller] substituted, constraints
|
|
67
|
+
* stripped) alongside the verbatim `Route`. In every earlier schema `Path` is the **source file
|
|
68
|
+
* path**, so preferring it unconditionally emits file paths as service endpoints. cdxgen ships a
|
|
69
|
+
* pinned dosai via @cdxgen/cdxgen-plugins-bin and users can point DOSAI_CMD at any build, so both
|
|
70
|
+
* shapes are live and the version has to be checked.
|
|
71
|
+
*
|
|
72
|
+
* @param {Object} methodsSlice Parsed dosai methods slice JSON
|
|
73
|
+
* @returns {boolean} True when `Path` holds a resolved route
|
|
74
|
+
*/
|
|
75
|
+
function hasResolvedEndpointPaths(methodsSlice) {
|
|
76
|
+
const version = methodsSlice?.Metadata?.SchemaVersion;
|
|
77
|
+
if (typeof version !== "string") {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const major = Number.parseInt(version.split(".")[0], 10);
|
|
81
|
+
return Number.isFinite(major) && major >= 4;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Coerce a dosai string list into the array-of-strings shape serviceData.source/destination require.
|
|
86
|
+
* Returns undefined rather than an empty or malformed array so the key is omitted entirely.
|
|
87
|
+
*/
|
|
88
|
+
function toStringArray(value) {
|
|
89
|
+
if (!Array.isArray(value)) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const strings = value.filter((entry) => typeof entry === "string" && entry);
|
|
93
|
+
return strings.length ? strings : undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
62
96
|
function appendUniqueProperty(properties, name, value) {
|
|
63
97
|
if (value === undefined || value === null || value === "") {
|
|
64
98
|
return;
|
|
@@ -219,6 +253,14 @@ export function runDosaiCommand(command, src, outputFile, options = {}) {
|
|
|
219
253
|
shell: false,
|
|
220
254
|
});
|
|
221
255
|
if (dosaiSdkMessage(result)) {
|
|
256
|
+
recordDegradation("dotnet.sdk.missing", {
|
|
257
|
+
ecosystem: "csharp",
|
|
258
|
+
tool: "dotnet",
|
|
259
|
+
impact: "none",
|
|
260
|
+
command: `${basename(executable)} ${args.join(" ")}`,
|
|
261
|
+
detail:
|
|
262
|
+
"dosai reported that no .NET SDK is installed, so the requested dosai analysis could not run.",
|
|
263
|
+
});
|
|
222
264
|
console.log(
|
|
223
265
|
"Dotnet SDK is not installed. Please use the cdxgen dotnet container images to analyze this project with dosai.",
|
|
224
266
|
);
|
|
@@ -348,7 +390,21 @@ export function persistDosaiSemanticsReport(
|
|
|
348
390
|
return undefined;
|
|
349
391
|
}
|
|
350
392
|
const combined = buildCombinedDosaiReport(methodsSlice, dataFlowSlice);
|
|
351
|
-
|
|
393
|
+
try {
|
|
394
|
+
safeWriteSync(durablePath, JSON.stringify(combined));
|
|
395
|
+
} catch (err) {
|
|
396
|
+
// Combined methods + data-flow reports for large solutions can exceed the
|
|
397
|
+
// JS maximum string length. The SBOM evidence is already collected by this
|
|
398
|
+
// point, so skip the durable copy instead of losing the whole analysis.
|
|
399
|
+
recordDegradation("dosai.semantics.persistFailed", {
|
|
400
|
+
ecosystem: "csharp",
|
|
401
|
+
tool: "dosai",
|
|
402
|
+
impact: "minor",
|
|
403
|
+
command: `persistDosaiSemanticsReport ${durablePath}`,
|
|
404
|
+
detail: `The combined dosai report could not be persisted: ${err.message}. The SBOM evidence is unaffected.`,
|
|
405
|
+
});
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
352
408
|
return durablePath;
|
|
353
409
|
}
|
|
354
410
|
|
|
@@ -498,6 +554,288 @@ export function collectDosaiDataFlowFrames(dataFlowResult, components = []) {
|
|
|
498
554
|
return dataFlowFrames;
|
|
499
555
|
}
|
|
500
556
|
|
|
557
|
+
/**
|
|
558
|
+
* Consume dosai's AiComponents[] inventory (schema 4.0.0): model identifiers,
|
|
559
|
+
* on-disk model artifacts with hashes, MCP tools, prompts (redacted), and agents
|
|
560
|
+
* become CycloneDX machine-learning-model / data components with modelCard data.
|
|
561
|
+
*
|
|
562
|
+
* @param {Object} methodsSlice Parsed dosai methods slice JSON
|
|
563
|
+
* @param {Array} [components] Component list to mutate in place
|
|
564
|
+
* @returns {Array} The updated component list
|
|
565
|
+
*/
|
|
566
|
+
export function collectDosaiAiComponents(methodsSlice, components = []) {
|
|
567
|
+
for (const ai of methodsSlice?.AiComponents || []) {
|
|
568
|
+
if (!ai?.Id || !ai?.Name) {
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
if (components.some((c) => c["bom-ref"] === ai.Id)) {
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
if (ai.Kind === "model") {
|
|
575
|
+
const modelCard = {
|
|
576
|
+
modelParameters: {},
|
|
577
|
+
};
|
|
578
|
+
if (ai.Task) {
|
|
579
|
+
modelCard.modelParameters.task = ai.Task;
|
|
580
|
+
}
|
|
581
|
+
if (ai.ArchitectureFamily) {
|
|
582
|
+
modelCard.modelParameters.architectureFamily = ai.ArchitectureFamily;
|
|
583
|
+
}
|
|
584
|
+
if (ai.ModelArchitecture) {
|
|
585
|
+
// modelArchitecture lives under modelParameters; modelCard itself is
|
|
586
|
+
// additionalProperties:false, so placing it at the top level fails validation.
|
|
587
|
+
modelCard.modelParameters.modelArchitecture = ai.ModelArchitecture;
|
|
588
|
+
}
|
|
589
|
+
if (Array.isArray(ai.InputFormats) && ai.InputFormats.length) {
|
|
590
|
+
modelCard.modelParameters.inputs = ai.InputFormats.map((format) => ({
|
|
591
|
+
format,
|
|
592
|
+
}));
|
|
593
|
+
}
|
|
594
|
+
if (Array.isArray(ai.OutputFormats) && ai.OutputFormats.length) {
|
|
595
|
+
modelCard.modelParameters.outputs = ai.OutputFormats.map((format) => ({
|
|
596
|
+
format,
|
|
597
|
+
}));
|
|
598
|
+
}
|
|
599
|
+
const component = {
|
|
600
|
+
"bom-ref": ai.Id,
|
|
601
|
+
type: "machine-learning-model",
|
|
602
|
+
name: ai.Name,
|
|
603
|
+
version: ai.Version,
|
|
604
|
+
purl: ai.Purl,
|
|
605
|
+
modelCard,
|
|
606
|
+
properties: [
|
|
607
|
+
{ name: "cdx:ai:provider", value: ai.Provider || "unknown" },
|
|
608
|
+
{ name: "cdx:ai:deployment", value: ai.Deployment || "unknown" },
|
|
609
|
+
],
|
|
610
|
+
};
|
|
611
|
+
if (ai.Sha256) {
|
|
612
|
+
component.hashes = [{ alg: "SHA-256", content: ai.Sha256 }];
|
|
613
|
+
}
|
|
614
|
+
if (ai.FilePath) {
|
|
615
|
+
component.properties.push({
|
|
616
|
+
name: "cdx:ai:modelFile",
|
|
617
|
+
value: ai.FilePath,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
components.push(component);
|
|
621
|
+
} else if (ai.Kind === "prompt" || ai.Kind === "dataset") {
|
|
622
|
+
components.push({
|
|
623
|
+
"bom-ref": ai.Id,
|
|
624
|
+
type: "data",
|
|
625
|
+
name: ai.Name,
|
|
626
|
+
properties: [
|
|
627
|
+
{ name: "cdx:ai:kind", value: ai.Kind },
|
|
628
|
+
...(ai.PromptText
|
|
629
|
+
? [{ name: "cdx:ai:promptText", value: ai.PromptText }]
|
|
630
|
+
: []),
|
|
631
|
+
...Object.entries(ai.Properties || {}).map(([name, value]) => ({
|
|
632
|
+
name: `cdx:ai:${name}`,
|
|
633
|
+
value: String(value),
|
|
634
|
+
})),
|
|
635
|
+
],
|
|
636
|
+
});
|
|
637
|
+
} else if (
|
|
638
|
+
ai.Kind === "tool" ||
|
|
639
|
+
ai.Kind === "agent" ||
|
|
640
|
+
ai.Kind === "guardrail" ||
|
|
641
|
+
ai.Kind === "embedding"
|
|
642
|
+
) {
|
|
643
|
+
const properties = [
|
|
644
|
+
{ name: "cdx:ai:kind", value: ai.Kind },
|
|
645
|
+
{ name: "cdx:ai:provider", value: ai.Provider || "unknown" },
|
|
646
|
+
];
|
|
647
|
+
if (ai.ToolSchema) {
|
|
648
|
+
properties.push({ name: "cdx:ai:toolSchema", value: ai.ToolSchema });
|
|
649
|
+
}
|
|
650
|
+
components.push({
|
|
651
|
+
"bom-ref": ai.Id,
|
|
652
|
+
type: "data",
|
|
653
|
+
name: ai.Name,
|
|
654
|
+
properties,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return components;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Consume dosai's first-class Services[] inventory (schema 4.0.0) directly: richer
|
|
663
|
+
* than deriving services from ApiEndpoints alone — stable bom-refs, trust zones,
|
|
664
|
+
* data classifications, providers, and per-service evidence occurrences.
|
|
665
|
+
*
|
|
666
|
+
* @param {Object} methodsSlice Parsed dosai methods slice JSON
|
|
667
|
+
* @param {Object} [servicesMap] Map of service key to service definition, mutated in place
|
|
668
|
+
* @returns {Object} The updated services map
|
|
669
|
+
*/
|
|
670
|
+
export function collectDosaiServiceComponents(methodsSlice, servicesMap = {}) {
|
|
671
|
+
for (const service of methodsSlice?.Services || []) {
|
|
672
|
+
if (!service?.Id || !service?.Name) {
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
const key = service.Id;
|
|
676
|
+
const definition = (servicesMap[key] ??= {
|
|
677
|
+
name: service.Name,
|
|
678
|
+
bomRef: service.Id,
|
|
679
|
+
endpoints: new Set(),
|
|
680
|
+
properties: [],
|
|
681
|
+
});
|
|
682
|
+
definition.group = definition.group || service.Group;
|
|
683
|
+
definition.version = definition.version || service.Version;
|
|
684
|
+
// The same service Id can appear in several Services[] entries (one per
|
|
685
|
+
// implementing type or call site). Scalar facts left unset by earlier
|
|
686
|
+
// entries must still be filled from later ones, in particular the
|
|
687
|
+
// outbound provider: an entry that is only inbound must not lock the
|
|
688
|
+
// definition into an anonymous egress.
|
|
689
|
+
if (
|
|
690
|
+
!definition.provider &&
|
|
691
|
+
service.Provider &&
|
|
692
|
+
service.Direction === "outbound"
|
|
693
|
+
) {
|
|
694
|
+
definition.provider = { name: service.Provider };
|
|
695
|
+
}
|
|
696
|
+
if (typeof definition.authenticated === "undefined") {
|
|
697
|
+
definition.authenticated = service.Authenticated ?? undefined;
|
|
698
|
+
}
|
|
699
|
+
if (!definition.trustZone) {
|
|
700
|
+
definition.trustZone = service.TrustZone;
|
|
701
|
+
}
|
|
702
|
+
if (!definition["x-trust-boundary"]) {
|
|
703
|
+
definition["x-trust-boundary"] =
|
|
704
|
+
service.CrossesTrustBoundary === true ? true : undefined;
|
|
705
|
+
}
|
|
706
|
+
for (const endpoint of service.Endpoints || []) {
|
|
707
|
+
const sanitized = sanitizeEndpoint(endpoint);
|
|
708
|
+
if (sanitized) {
|
|
709
|
+
definition.endpoints.add(sanitized);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (Array.isArray(service.Operations) && service.Operations.length) {
|
|
713
|
+
appendUniqueProperty(
|
|
714
|
+
definition.properties,
|
|
715
|
+
"cdx:dosai:operationCount",
|
|
716
|
+
service.Operations.length,
|
|
717
|
+
);
|
|
718
|
+
for (const operation of service.Operations) {
|
|
719
|
+
// Endpoints[] can miss routes that only Operations carry (e.g. gRPC and
|
|
720
|
+
// messaging operations resolved per method), so both are collected.
|
|
721
|
+
const sanitized = sanitizeEndpoint(operation.Path);
|
|
722
|
+
if (sanitized) {
|
|
723
|
+
definition.endpoints.add(sanitized);
|
|
724
|
+
}
|
|
725
|
+
// Verbs are small enum values; they keep the HTTP surface auditable
|
|
726
|
+
// per endpoint without copying request payloads.
|
|
727
|
+
appendUniqueProperty(
|
|
728
|
+
definition.properties,
|
|
729
|
+
"cdx:service:httpMethod",
|
|
730
|
+
operation.HttpMethod,
|
|
731
|
+
);
|
|
732
|
+
if (operation.RouteTemplate && operation.Path) {
|
|
733
|
+
appendUniqueProperty(
|
|
734
|
+
definition.properties,
|
|
735
|
+
"cdx:service:pathTemplate",
|
|
736
|
+
operation.RouteTemplate,
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (Array.isArray(service.Data) && service.Data.length) {
|
|
742
|
+
// First population of services[].data[] for .NET: flow + classification
|
|
743
|
+
// with auditable descriptions from dosai.
|
|
744
|
+
definition.data = (definition.data || []).concat(
|
|
745
|
+
service.Data
|
|
746
|
+
// `flow` and `classification` are both required by serviceData in the 1.7 schema; an entry
|
|
747
|
+
// missing either fails validation and would fail the whole BOM, so drop it instead.
|
|
748
|
+
.filter((entry) => entry?.Classification && entry?.Flow)
|
|
749
|
+
.map((entry) => ({
|
|
750
|
+
classification: entry.Classification,
|
|
751
|
+
// "unknown" is a legitimate dataFlowDirection member. Coercing it to "bi-directional"
|
|
752
|
+
// turned an absence of knowledge into a positive claim that data flows both ways.
|
|
753
|
+
flow: ["inbound", "outbound", "bi-directional", "unknown"].includes(
|
|
754
|
+
entry.Flow,
|
|
755
|
+
)
|
|
756
|
+
? entry.Flow
|
|
757
|
+
: "unknown",
|
|
758
|
+
name: entry.Name,
|
|
759
|
+
description: entry.Description,
|
|
760
|
+
// source/destination are arrays of strings (iri-reference or BOM-link) in the schema.
|
|
761
|
+
source: toStringArray(entry.Source),
|
|
762
|
+
destination: toStringArray(entry.Destination),
|
|
763
|
+
})),
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
const properties = definition.properties;
|
|
767
|
+
appendUniqueProperty(properties, "cdx:service:kind", service.ServiceKind);
|
|
768
|
+
appendUniqueProperty(
|
|
769
|
+
properties,
|
|
770
|
+
"cdx:service:direction",
|
|
771
|
+
service.Direction,
|
|
772
|
+
);
|
|
773
|
+
appendUniqueProperty(
|
|
774
|
+
properties,
|
|
775
|
+
"cdx:service:framework",
|
|
776
|
+
service.Framework,
|
|
777
|
+
);
|
|
778
|
+
appendUniqueProperty(
|
|
779
|
+
properties,
|
|
780
|
+
"cdx:dosai:confidence",
|
|
781
|
+
service.Confidence,
|
|
782
|
+
);
|
|
783
|
+
for (const tag of service.Tags || []) {
|
|
784
|
+
appendUniqueProperty(properties, "cdx:dosai:tag", tag);
|
|
785
|
+
}
|
|
786
|
+
// Auth surface as counts and scheme categories only; policies, roles, and
|
|
787
|
+
// claim values never cross into the BOM verbatim.
|
|
788
|
+
appendUniqueProperty(
|
|
789
|
+
properties,
|
|
790
|
+
"cdx:dosai:allowAnonymous",
|
|
791
|
+
service.AllowAnonymous,
|
|
792
|
+
);
|
|
793
|
+
for (const scheme of service.AuthenticationSchemes || []) {
|
|
794
|
+
appendUniqueProperty(properties, "cdx:dosai:authScheme", scheme);
|
|
795
|
+
}
|
|
796
|
+
appendUniqueProperty(
|
|
797
|
+
properties,
|
|
798
|
+
"cdx:dosai:authorizationPolicyCount",
|
|
799
|
+
service.AuthorizationPolicies?.length,
|
|
800
|
+
);
|
|
801
|
+
appendUniqueProperty(
|
|
802
|
+
properties,
|
|
803
|
+
"cdx:dosai:roleCount",
|
|
804
|
+
service.Roles?.length,
|
|
805
|
+
);
|
|
806
|
+
if (service.Location?.Path) {
|
|
807
|
+
// Accumulate call sites: a service implemented across files must keep
|
|
808
|
+
// every occurrence, not just the last one processed.
|
|
809
|
+
definition.evidence ??= { occurrences: [] };
|
|
810
|
+
const location = {
|
|
811
|
+
path: service.Location.Path,
|
|
812
|
+
line: service.Location.LineNumber || undefined,
|
|
813
|
+
column: service.Location.ColumnNumber || undefined,
|
|
814
|
+
};
|
|
815
|
+
const occurrenceKey = `${location.path}#${location.line}#${location.column}`;
|
|
816
|
+
const knownOccurrences = definition.evidence.occurrences;
|
|
817
|
+
if (
|
|
818
|
+
!knownOccurrences.some(
|
|
819
|
+
(known) =>
|
|
820
|
+
`${known?.location?.path}#${known?.location?.line}#${known?.location?.column}` ===
|
|
821
|
+
occurrenceKey,
|
|
822
|
+
)
|
|
823
|
+
) {
|
|
824
|
+
knownOccurrences.push({ location });
|
|
825
|
+
}
|
|
826
|
+
// services[].evidence is a CycloneDX 2.0 field that spec-version
|
|
827
|
+
// compatibility strips below 2.0, so the location also lands in a
|
|
828
|
+
// property where it survives at 1.6 and 1.7.
|
|
829
|
+
appendUniqueProperty(
|
|
830
|
+
definition.properties,
|
|
831
|
+
"cdx:dosai:location",
|
|
832
|
+
`${location.path}:${location.line || 0}:${location.column || 0}`,
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return servicesMap;
|
|
837
|
+
}
|
|
838
|
+
|
|
501
839
|
/**
|
|
502
840
|
* Infer service and endpoint definitions from a dosai methods slice.
|
|
503
841
|
*
|
|
@@ -513,21 +851,40 @@ export function collectDosaiServicesFromMethods(
|
|
|
513
851
|
methodsSlice,
|
|
514
852
|
servicesMap = {},
|
|
515
853
|
) {
|
|
854
|
+
const resolvedPaths = hasResolvedEndpointPaths(methodsSlice);
|
|
516
855
|
for (const endpoint of methodsSlice?.ApiEndpoints || []) {
|
|
517
|
-
|
|
518
|
-
|
|
856
|
+
// Endpoints owned by a provider service (ServiceId set and already
|
|
857
|
+
// collected) enrich the owner instead of being re-derived into a duplicate
|
|
858
|
+
// service. Their method and auth metadata still lands on the owner via the
|
|
859
|
+
// shared property recording below.
|
|
860
|
+
const ownedDefinition =
|
|
861
|
+
endpoint.ServiceId && servicesMap[endpoint.ServiceId]
|
|
862
|
+
? servicesMap[endpoint.ServiceId]
|
|
863
|
+
: undefined;
|
|
864
|
+
// Schema 4.0.0: Path is the resolved route (tokens substituted, constraints
|
|
865
|
+
// stripped); Route keeps the verbatim template. Preferring Path is the fix for
|
|
866
|
+
// cdxgen discussion #4333, where [controller] shipped as %5Bcontroller%5D.
|
|
867
|
+
const route = sanitizeEndpoint(
|
|
868
|
+
resolvedPaths ? endpoint.Path || endpoint.Route : endpoint.Route,
|
|
869
|
+
);
|
|
870
|
+
if (!ownedDefinition && !route) {
|
|
519
871
|
continue;
|
|
520
872
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
endpoint.AuthorizationRequired
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
873
|
+
let definition = ownedDefinition;
|
|
874
|
+
if (!definition) {
|
|
875
|
+
const serviceName = serviceNameFromEndpoint(endpoint);
|
|
876
|
+
definition = servicesMap[serviceName] ??= {
|
|
877
|
+
endpoints: new Set(),
|
|
878
|
+
authenticated: endpoint.AuthorizationRequired,
|
|
879
|
+
xTrustBoundary:
|
|
880
|
+
endpoint.AuthorizationRequired === true ? true : undefined,
|
|
881
|
+
properties: [],
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
if (route) {
|
|
885
|
+
definition.endpoints.add(route);
|
|
886
|
+
}
|
|
887
|
+
const properties = definition.properties;
|
|
531
888
|
appendUniqueProperty(
|
|
532
889
|
properties,
|
|
533
890
|
"cdx:service:httpMethod",
|
|
@@ -571,13 +928,30 @@ export function collectDosaiServicesFromMethods(
|
|
|
571
928
|
appendUniqueProperty(
|
|
572
929
|
properties,
|
|
573
930
|
"internal:SrcFile",
|
|
574
|
-
endpoint.
|
|
931
|
+
endpoint.FilePath || endpoint.FileName,
|
|
575
932
|
);
|
|
576
933
|
if (endpoint.LineNumber) {
|
|
577
934
|
appendUniqueProperty(
|
|
578
935
|
properties,
|
|
579
936
|
"cdx:dosai:location",
|
|
580
|
-
|
|
937
|
+
// Schema 4.0.0 renamed the ApiEndpoint source file field to FilePath;
|
|
938
|
+
// Path carries the resolved route there, not a location.
|
|
939
|
+
`${endpoint.FilePath || endpoint.FileName}:${endpoint.LineNumber}:${endpoint.ColumnNumber || 0}`,
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
if (endpoint.Route && endpoint.Path && endpoint.Route !== endpoint.Path) {
|
|
943
|
+
// The verbatim template (e.g. api/[controller]/{id:int}) alongside the resolved path.
|
|
944
|
+
appendUniqueProperty(
|
|
945
|
+
properties,
|
|
946
|
+
"cdx:service:pathTemplate",
|
|
947
|
+
endpoint.Route,
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
if (endpoint.Confidence) {
|
|
951
|
+
appendUniqueProperty(
|
|
952
|
+
properties,
|
|
953
|
+
"cdx:dosai:confidence",
|
|
954
|
+
endpoint.Confidence,
|
|
581
955
|
);
|
|
582
956
|
}
|
|
583
957
|
}
|
|
@@ -591,11 +965,46 @@ export function collectDosaiServicesFromMethods(
|
|
|
591
965
|
* @returns {Object[]} Array of service objects with sorted endpoint arrays and properties
|
|
592
966
|
*/
|
|
593
967
|
export function normalizeDosaiServiceMap(servicesMap = {}) {
|
|
594
|
-
return Object.keys(servicesMap)
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
968
|
+
return Object.keys(servicesMap)
|
|
969
|
+
.map((serviceName) => {
|
|
970
|
+
const definition = servicesMap[serviceName];
|
|
971
|
+
const service = {
|
|
972
|
+
name:
|
|
973
|
+
definition.name ||
|
|
974
|
+
serviceName ||
|
|
975
|
+
`dosai-${basename(serviceName)}-service`,
|
|
976
|
+
endpoints: Array.from(definition.endpoints || []).sort(),
|
|
977
|
+
authenticated: definition.authenticated,
|
|
978
|
+
// Accept both spellings. Every pre-4.0.0 producer — collectDosaiServicesFromMethods here, and
|
|
979
|
+
// the OpenAPI/Java/Python paths in evinser.js — sets the camelCase `xTrustBoundary`; only the
|
|
980
|
+
// new Services[] path sets the hyphenated CycloneDX key. Reading just the latter silently
|
|
981
|
+
// dropped the flag for all of them, including languages dosai has nothing to do with.
|
|
982
|
+
"x-trust-boundary":
|
|
983
|
+
definition["x-trust-boundary"] ?? definition.xTrustBoundary,
|
|
984
|
+
properties: definition.properties,
|
|
985
|
+
};
|
|
986
|
+
if (definition.bomRef) {
|
|
987
|
+
service["bom-ref"] = definition.bomRef;
|
|
988
|
+
}
|
|
989
|
+
if (definition.group) {
|
|
990
|
+
service.group = definition.group;
|
|
991
|
+
}
|
|
992
|
+
if (definition.version) {
|
|
993
|
+
service.version = definition.version;
|
|
994
|
+
}
|
|
995
|
+
if (definition.trustZone) {
|
|
996
|
+
service.trustZone = definition.trustZone;
|
|
997
|
+
}
|
|
998
|
+
if (definition.provider) {
|
|
999
|
+
service.provider = definition.provider;
|
|
1000
|
+
}
|
|
1001
|
+
if (definition.data) {
|
|
1002
|
+
service.data = definition.data;
|
|
1003
|
+
}
|
|
1004
|
+
if (definition.evidence) {
|
|
1005
|
+
service.evidence = definition.evidence;
|
|
1006
|
+
}
|
|
1007
|
+
return service;
|
|
1008
|
+
})
|
|
1009
|
+
.filter((service) => service.name);
|
|
601
1010
|
}
|