@cyclonedx/cdxgen 12.4.1 → 12.4.2

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.
@@ -0,0 +1,302 @@
1
+ import esmock from "esmock";
2
+ import { assert, describe, it } from "poku";
3
+ import sinon from "sinon";
4
+
5
+ import {
6
+ buildPurlAliasMap,
7
+ collectDosaiDataFlowFrames,
8
+ collectDosaiPurlEvidence,
9
+ collectDosaiServicesFromMethods,
10
+ isDosaiDotnetLanguage,
11
+ normalizeDosaiServiceMap,
12
+ resolveComponentPurl,
13
+ } from "./dosai.js";
14
+
15
+ describe("dosai helpers", () => {
16
+ it("recognizes C#, VB.NET, and F# language aliases", () => {
17
+ for (const language of [
18
+ "csharp",
19
+ "dotnet",
20
+ "vb",
21
+ "vbnet",
22
+ "visualbasic",
23
+ "f#",
24
+ "fs",
25
+ "fsharp",
26
+ ]) {
27
+ assert.strictEqual(isDosaiDotnetLanguage(language), true);
28
+ }
29
+ });
30
+
31
+ it("matches versionless dosai purls to cdxgen component purls", () => {
32
+ const components = [{ purl: "pkg:nuget/System.Text.Json@10.0.0" }];
33
+ const aliases = buildPurlAliasMap(components);
34
+
35
+ assert.strictEqual(
36
+ resolveComponentPurl("pkg:nuget/System.Text.Json", aliases),
37
+ "pkg:nuget/System.Text.Json@10.0.0",
38
+ );
39
+ });
40
+
41
+ it("collects package occurrence evidence from dosai PackageReachability", () => {
42
+ const methodsSlice = {
43
+ CallGraph: {
44
+ Edges: [
45
+ {
46
+ Id: "e1",
47
+ FileName: "System.Text.Json.dll",
48
+ LineNumber: 12,
49
+ CalledMethodName: "System.Text.Json.JsonSerializer.Deserialize",
50
+ TargetName: "Deserialize",
51
+ },
52
+ ],
53
+ Nodes: [
54
+ {
55
+ Id: "n1",
56
+ FileName: "Program.cs",
57
+ LineNumber: 10,
58
+ ClassName: "Program",
59
+ Name: "Main",
60
+ },
61
+ {
62
+ Id: "n2",
63
+ FileName: "System.Text.Json.dll",
64
+ LineNumber: 0,
65
+ ClassName: "JsonSerializer",
66
+ Name: "Deserialize",
67
+ },
68
+ ],
69
+ },
70
+ PackageReachability: [
71
+ {
72
+ Purl: "pkg:nuget/System.Text.Json",
73
+ EdgeIds: ["e1"],
74
+ NodeIds: ["n1", "n2"],
75
+ SourceLocations: [
76
+ {
77
+ Path: "Controllers/Parser.cs",
78
+ FileName: "Parser.cs",
79
+ LineNumber: 42,
80
+ ColumnNumber: 13,
81
+ Kind: "CallGraphEdge",
82
+ },
83
+ ],
84
+ },
85
+ ],
86
+ };
87
+ const retMap = collectDosaiPurlEvidence(methodsSlice, [
88
+ { purl: "pkg:nuget/System.Text.Json@10.0.0" },
89
+ ]);
90
+
91
+ assert.deepStrictEqual(
92
+ Array.from(
93
+ retMap.purlLocationMap["pkg:nuget/System.Text.Json@10.0.0"],
94
+ ).sort(),
95
+ ["Controllers/Parser.cs#42"],
96
+ );
97
+ assert.ok(
98
+ retMap.purlMethodsMap["pkg:nuget/System.Text.Json@10.0.0"].has(
99
+ "System.Text.Json.JsonSerializer.Deserialize",
100
+ ),
101
+ );
102
+ });
103
+
104
+ it("keeps PackageReachability fallback occurrence evidence source-only", () => {
105
+ const retMap = collectDosaiPurlEvidence(
106
+ {
107
+ CallGraph: {
108
+ Edges: [
109
+ {
110
+ Id: "e1",
111
+ FileName: "System.Text.Json.dll",
112
+ LineNumber: 12,
113
+ CalledMethodName: "System.Text.Json.JsonSerializer.Deserialize",
114
+ CallLocation: {
115
+ FileName: "Program.fs",
116
+ LineNumber: 8,
117
+ },
118
+ },
119
+ {
120
+ Id: "e2",
121
+ FileName: "Controllers/EpisodesController.cs",
122
+ LineNumber: 17,
123
+ CalledMethodName: "System.Text.Json.JsonSerializer.Serialize",
124
+ },
125
+ ],
126
+ },
127
+ PackageReachability: [
128
+ {
129
+ Purl: "pkg:nuget/System.Text.Json",
130
+ EdgeIds: ["e1", "e2"],
131
+ },
132
+ ],
133
+ },
134
+ [{ purl: "pkg:nuget/System.Text.Json@10.0.0" }],
135
+ );
136
+
137
+ assert.deepStrictEqual(
138
+ Array.from(retMap.purlLocationMap["pkg:nuget/System.Text.Json@10.0.0"]),
139
+ ["Program.fs#8", "Controllers/EpisodesController.cs#17"],
140
+ );
141
+ assert.ok(
142
+ !Array.from(
143
+ retMap.purlLocationMap["pkg:nuget/System.Text.Json@10.0.0"],
144
+ ).some((location) => location.includes(".dll")),
145
+ );
146
+ });
147
+
148
+ it("collects package occurrence evidence from dosai Dependencies with purls", () => {
149
+ const retMap = collectDosaiPurlEvidence(
150
+ {
151
+ Dependencies: [
152
+ {
153
+ Path: "Program.vb",
154
+ FileName: "Program.vb",
155
+ Name: "Newtonsoft.Json",
156
+ Purl: "pkg:nuget/Newtonsoft.Json@13.0.3",
157
+ LineNumber: 4,
158
+ ColumnNumber: 9,
159
+ },
160
+ ],
161
+ },
162
+ [{ purl: "pkg:nuget/Newtonsoft.Json@13.0.3" }],
163
+ );
164
+
165
+ assert.deepStrictEqual(
166
+ Array.from(retMap.purlLocationMap["pkg:nuget/Newtonsoft.Json@13.0.3"]),
167
+ ["Program.vb#4"],
168
+ );
169
+ assert.ok(
170
+ retMap.purlModulesMap["pkg:nuget/Newtonsoft.Json@13.0.3"].has(
171
+ "Newtonsoft.Json",
172
+ ),
173
+ );
174
+ });
175
+
176
+ it("builds CycloneDX services from dosai ApiEndpoints without raw policy names", () => {
177
+ const servicesMap = collectDosaiServicesFromMethods({
178
+ ApiEndpoints: [
179
+ {
180
+ Route: "/api/podcasts?sig=secret",
181
+ FileName: "EpisodesController.cs",
182
+ Path: "Controllers/EpisodesController.cs",
183
+ ClassName: "EpisodesController",
184
+ MethodName: "Get",
185
+ HttpMethod: "GET",
186
+ EndpointKind: "Attribute",
187
+ AuthorizationRequired: true,
188
+ AuthorizationPolicies: ["InternalPolicyName"],
189
+ Roles: ["Admin"],
190
+ AllowAnonymous: false,
191
+ LineNumber: 42,
192
+ ColumnNumber: 9,
193
+ },
194
+ ],
195
+ });
196
+ const services = normalizeDosaiServiceMap(servicesMap);
197
+
198
+ assert.strictEqual(services.length, 1);
199
+ assert.deepStrictEqual(services[0].endpoints, ["/api/podcasts"]);
200
+ assert.strictEqual(services[0].authenticated, true);
201
+ assert.ok(
202
+ services[0].properties.some(
203
+ (property) =>
204
+ property.name === "cdx:dosai:authorizationPolicyCount" &&
205
+ property.value === "1",
206
+ ),
207
+ );
208
+ assert.ok(!JSON.stringify(services[0]).includes("InternalPolicyName"));
209
+ });
210
+
211
+ it("collects callstack frames from dosai data-flow slices", () => {
212
+ const frames = collectDosaiDataFlowFrames(
213
+ {
214
+ Nodes: [
215
+ {
216
+ Id: "dfn1",
217
+ Path: "Controllers/EpisodesController.cs",
218
+ Namespace: "Podcast.Api",
219
+ ClassName: "EpisodesController",
220
+ MethodName: "Get",
221
+ LineNumber: 12,
222
+ ColumnNumber: 5,
223
+ },
224
+ {
225
+ Id: "dfn2",
226
+ Path: "Services/JsonLoader.cs",
227
+ Namespace: "Podcast.Api",
228
+ ClassName: "JsonLoader",
229
+ MethodName: "Load",
230
+ LineNumber: 20,
231
+ ColumnNumber: 9,
232
+ },
233
+ ],
234
+ Slices: [
235
+ {
236
+ NodeIds: ["dfn1", "dfn2"],
237
+ Purls: ["pkg:nuget/System.Text.Json"],
238
+ },
239
+ ],
240
+ },
241
+ [{ purl: "pkg:nuget/System.Text.Json@10.0.0" }],
242
+ );
243
+
244
+ assert.strictEqual(frames["pkg:nuget/System.Text.Json@10.0.0"].length, 1);
245
+ assert.strictEqual(
246
+ frames["pkg:nuget/System.Text.Json@10.0.0"][0][1].function,
247
+ "Load",
248
+ );
249
+ });
250
+
251
+ it("rejects unsafe dosai command inputs before spawning", async () => {
252
+ const safeSpawnSync = sinon.stub().returns({ status: 0 });
253
+ const { runDosaiCommand } = await esmock("./dosai.js", {
254
+ "./plugins.js": { resolvePluginBinary: sinon.stub().returns("dosai") },
255
+ "./utils.js": {
256
+ DEBUG_MODE: false,
257
+ getTmpDir: sinon.stub().returns("/tmp"),
258
+ safeExistsSync: sinon.stub().returns(true),
259
+ safeMkdtempSync: sinon.stub(),
260
+ safeRmSync: sinon.stub(),
261
+ safeSpawnSync,
262
+ },
263
+ });
264
+
265
+ assert.strictEqual(
266
+ runDosaiCommand("methods;rm -rf /", "/tmp/project", "/tmp/out.json"),
267
+ false,
268
+ );
269
+ assert.strictEqual(
270
+ runDosaiCommand("methods", "/tmp/project\n--bad", "/tmp/out.json"),
271
+ false,
272
+ );
273
+ sinon.assert.notCalled(safeSpawnSync);
274
+ });
275
+
276
+ it("spawns dosai with argument arrays and shell disabled", async () => {
277
+ const safeSpawnSync = sinon.stub().returns({ status: 0 });
278
+ const { runDosaiCommand } = await esmock("./dosai.js", {
279
+ "./plugins.js": { resolvePluginBinary: sinon.stub().returns("dosai") },
280
+ "./utils.js": {
281
+ DEBUG_MODE: false,
282
+ getTmpDir: sinon.stub().returns("/tmp"),
283
+ safeExistsSync: sinon.stub().returns(true),
284
+ safeMkdtempSync: sinon.stub(),
285
+ safeRmSync: sinon.stub(),
286
+ safeSpawnSync,
287
+ },
288
+ });
289
+
290
+ assert.strictEqual(
291
+ runDosaiCommand("dataflows", "/tmp/project", "/tmp/out.json", {
292
+ dataFlowPatterns: "/tmp/patterns.json",
293
+ patternPacks: "/tmp/packs",
294
+ }),
295
+ true,
296
+ );
297
+ sinon.assert.calledOnce(safeSpawnSync);
298
+ assert.strictEqual(safeSpawnSync.firstCall.args[0], "dosai");
299
+ assert.ok(Array.isArray(safeSpawnSync.firstCall.args[1]));
300
+ assert.strictEqual(safeSpawnSync.firstCall.args[2].shell, false);
301
+ });
302
+ });
@@ -0,0 +1,103 @@
1
+ import { PackageURL } from "packageurl-js";
2
+
3
+ export function normalizeDosaiPurlKey(purl) {
4
+ if (!purl || typeof purl !== "string") {
5
+ return undefined;
6
+ }
7
+ try {
8
+ const purlObj = PackageURL.fromString(purl);
9
+ return [
10
+ purlObj.type?.toLowerCase(),
11
+ purlObj.namespace?.toLowerCase() || "",
12
+ purlObj.name?.toLowerCase(),
13
+ ].join("/");
14
+ } catch (_err) {
15
+ return purl.split("?")[0].split("#")[0].split("@")[0].toLowerCase();
16
+ }
17
+ }
18
+
19
+ export function addDosaiSetValue(map, key, value) {
20
+ if (!key || !value) {
21
+ return;
22
+ }
23
+ map[key] ??= new Set();
24
+ map[key].add(value);
25
+ }
26
+
27
+ export function dosaiLocation(item) {
28
+ const location = item?.Location || item?.CallLocation || item;
29
+ const fileName =
30
+ location?.Path || location?.FileName || item?.Path || item?.FileName;
31
+ if (!fileName || fileName === "<unknown>") {
32
+ return undefined;
33
+ }
34
+ const lineNumber = location?.LineNumber || item?.LineNumber;
35
+ if (lineNumber && lineNumber > 0) {
36
+ return `${fileName}#${lineNumber}`;
37
+ }
38
+ return fileName;
39
+ }
40
+
41
+ function dosaiSourceFileName(item) {
42
+ const location = item?.Location || item?.CallLocation || item;
43
+ return String(
44
+ location?.Path || location?.FileName || item?.Path || item?.FileName || "",
45
+ );
46
+ }
47
+
48
+ function dosaiSourceLineNumber(item) {
49
+ const location = item?.Location || item?.CallLocation || item;
50
+ return location?.LineNumber || item?.LineNumber;
51
+ }
52
+
53
+ export function dosaiSourceLocationFromNode(node) {
54
+ const location = dosaiLocation(node);
55
+ const fileName = dosaiSourceFileName(node).toLowerCase();
56
+ const lineNumber = dosaiSourceLineNumber(node);
57
+ if (!location || !/\.(cs|vb|fs|fsx)$/i.test(fileName)) {
58
+ return undefined;
59
+ }
60
+ if (!lineNumber || lineNumber <= 0) {
61
+ return undefined;
62
+ }
63
+ return location;
64
+ }
65
+
66
+ export function dosaiSourceLocation(location) {
67
+ const sourceLocation = dosaiLocation(location);
68
+ const fileName = dosaiSourceFileName(location);
69
+ const lineNumber = dosaiSourceLineNumber(location);
70
+ if (!sourceLocation || !/\.(cs|vb|fs|fsx)$/i.test(fileName)) {
71
+ return undefined;
72
+ }
73
+ if (!lineNumber || lineNumber <= 0) {
74
+ return undefined;
75
+ }
76
+ return sourceLocation;
77
+ }
78
+
79
+ export function buildDosaiPurlAliasMap(components = []) {
80
+ const purlAliasMap = new Map();
81
+ for (const component of components) {
82
+ if (!component?.purl) {
83
+ continue;
84
+ }
85
+ purlAliasMap.set(component.purl, component.purl);
86
+ const key = normalizeDosaiPurlKey(component.purl);
87
+ if (key && !purlAliasMap.has(key)) {
88
+ purlAliasMap.set(key, component.purl);
89
+ }
90
+ }
91
+ return purlAliasMap;
92
+ }
93
+
94
+ export function resolveDosaiComponentPurl(purl, purlAliasMap) {
95
+ if (!purl) {
96
+ return undefined;
97
+ }
98
+ return (
99
+ purlAliasMap.get(purl) ||
100
+ purlAliasMap.get(normalizeDosaiPurlKey(purl)) ||
101
+ purl
102
+ );
103
+ }
@@ -58,6 +58,13 @@ import Arborist from "../third-party/arborist/lib/index.js";
58
58
  import { analyzeSuspiciousJsFile } from "./analyzer.js";
59
59
  import { DEFAULT_HBOM_AUDIT_CATEGORIES } from "./auditCategories.js";
60
60
  import { parseWorkflowFile } from "./ciParsers/githubActions.js";
61
+ import {
62
+ addDosaiSetValue,
63
+ buildDosaiPurlAliasMap,
64
+ dosaiSourceLocation,
65
+ dosaiSourceLocationFromNode,
66
+ resolveDosaiComponentPurl,
67
+ } from "./dosaiParsers.js";
61
68
  import { extractPackageInfoFromHintPath } from "./dotnetutils.js";
62
69
  import {
63
70
  createOccurrenceEvidence,
@@ -1286,6 +1293,8 @@ export function safeSpawnSync(command, args, options) {
1286
1293
  options = {
1287
1294
  ...options,
1288
1295
  };
1296
+ }
1297
+ if (options.cdxgenActivity) {
1289
1298
  delete options.cdxgenActivity;
1290
1299
  }
1291
1300
  // Inject maxBuffer
@@ -1753,6 +1762,10 @@ export const PROJECT_TYPE_ALIASES = {
1753
1762
  "dotnet-framework47",
1754
1763
  "dotnet-framework48",
1755
1764
  "vb",
1765
+ "vbnet",
1766
+ "visualbasic",
1767
+ "f#",
1768
+ "fs",
1756
1769
  "fsharp",
1757
1770
  "twincat",
1758
1771
  "csproj",
@@ -21739,6 +21752,42 @@ export async function getNugetMetadata(pkgList, dependencies = undefined) {
21739
21752
  };
21740
21753
  }
21741
21754
 
21755
+ function addDotnetIdentityMethod(apkg, value) {
21756
+ if (!value) {
21757
+ return;
21758
+ }
21759
+ apkg.evidence = apkg.evidence || {};
21760
+ const identityList = Array.isArray(apkg.evidence.identity)
21761
+ ? apkg.evidence.identity
21762
+ : undefined;
21763
+ let identity = identityList
21764
+ ? identityList.find((entry) => entry?.field === "purl") ||
21765
+ identityList.find((entry) => !entry?.field)
21766
+ : apkg.evidence.identity;
21767
+ if (!identity) {
21768
+ identity = { field: "purl", confidence: 1, methods: [] };
21769
+ if (identityList) {
21770
+ identityList.push(identity);
21771
+ }
21772
+ }
21773
+ identity.field ??= "purl";
21774
+ identity.confidence ??= 1;
21775
+ identity.methods ??= [];
21776
+ if (
21777
+ !identity.methods.some(
21778
+ (method) =>
21779
+ method.technique === "source-code-analysis" && method.value === value,
21780
+ )
21781
+ ) {
21782
+ identity.methods.push({
21783
+ technique: "source-code-analysis",
21784
+ confidence: 1,
21785
+ value,
21786
+ });
21787
+ }
21788
+ apkg.evidence.identity = identityList || identity;
21789
+ }
21790
+
21742
21791
  /**
21743
21792
  * Enrich .NET package components with occurrence evidence and imported module/method
21744
21793
  * information from a dosai dependency slices file.
@@ -21762,6 +21811,7 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
21762
21811
  const purlLocationMap = {};
21763
21812
  const purlModulesMap = {};
21764
21813
  const purlMethodsMap = {};
21814
+ const purlAliasMap = buildDosaiPurlAliasMap(pkgList);
21765
21815
  for (const apkg of pkgList) {
21766
21816
  if (apkg.properties && Array.isArray(apkg.properties)) {
21767
21817
  apkg.properties
@@ -21778,13 +21828,33 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
21778
21828
  });
21779
21829
  }
21780
21830
  }
21781
- const slicesData = JSON.parse(readFileSync(slicesFile, "utf-8"));
21831
+ let slicesData;
21832
+ try {
21833
+ slicesData = JSON.parse(readFileSync(slicesFile, "utf-8"));
21834
+ } catch (_err) {
21835
+ return pkgList;
21836
+ }
21782
21837
  if (slicesData && Object.keys(slicesData)) {
21783
21838
  thoughtLog(
21784
21839
  "Let's thoroughly inspect the dependency slice to identify where and how the components are used.",
21785
21840
  );
21786
21841
  if (slicesData.Dependencies) {
21787
21842
  for (const adep of slicesData.Dependencies) {
21843
+ if (adep.Purl) {
21844
+ const modPurl = resolveDosaiComponentPurl(adep.Purl, purlAliasMap);
21845
+ if (modPurl) {
21846
+ addDosaiSetValue(
21847
+ purlLocationMap,
21848
+ modPurl,
21849
+ dosaiSourceLocation(adep),
21850
+ );
21851
+ addDosaiSetValue(
21852
+ purlModulesMap,
21853
+ modPurl,
21854
+ adep.Name || adep.Namespace,
21855
+ );
21856
+ }
21857
+ }
21788
21858
  // Case 1: Dependencies slice has the .dll file
21789
21859
  if (adep.Module?.endsWith(".dll") && pkgFilePurlMap[adep.Module]) {
21790
21860
  const modPurl = pkgFilePurlMap[adep.Module];
@@ -21810,6 +21880,64 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
21810
21880
  }
21811
21881
  }
21812
21882
  }
21883
+ if (slicesData.PackageReachability) {
21884
+ const graphEdges = Object.fromEntries(
21885
+ (slicesData.CallGraph?.Edges || []).map((edge) => [edge.Id, edge]),
21886
+ );
21887
+ const graphNodes = Object.fromEntries(
21888
+ (slicesData.CallGraph?.Nodes || []).map((node) => [node.Id, node]),
21889
+ );
21890
+ for (const reachability of slicesData.PackageReachability) {
21891
+ const modPurl = resolveDosaiComponentPurl(
21892
+ reachability.Purl,
21893
+ purlAliasMap,
21894
+ );
21895
+ if (!modPurl) {
21896
+ continue;
21897
+ }
21898
+ let hasExplicitSourceLocations = false;
21899
+ for (const sourceLocation of reachability.SourceLocations || []) {
21900
+ const location = dosaiSourceLocation(sourceLocation);
21901
+ addDosaiSetValue(purlLocationMap, modPurl, location);
21902
+ hasExplicitSourceLocations ||= Boolean(location);
21903
+ }
21904
+ for (const edgeId of reachability.EdgeIds || []) {
21905
+ const edge = graphEdges[edgeId];
21906
+ if (!hasExplicitSourceLocations) {
21907
+ addDosaiSetValue(
21908
+ purlLocationMap,
21909
+ modPurl,
21910
+ dosaiSourceLocation(edge),
21911
+ );
21912
+ }
21913
+ addDosaiSetValue(
21914
+ purlMethodsMap,
21915
+ modPurl,
21916
+ edge?.CalledMethodName || edge?.TargetName,
21917
+ );
21918
+ }
21919
+ for (const nodeId of reachability.NodeIds || []) {
21920
+ const node = graphNodes[nodeId];
21921
+ if (!hasExplicitSourceLocations) {
21922
+ addDosaiSetValue(
21923
+ purlLocationMap,
21924
+ modPurl,
21925
+ dosaiSourceLocationFromNode(node),
21926
+ );
21927
+ }
21928
+ addDosaiSetValue(
21929
+ purlModulesMap,
21930
+ modPurl,
21931
+ node?.ClassName || node?.Module,
21932
+ );
21933
+ addDosaiSetValue(
21934
+ purlMethodsMap,
21935
+ modPurl,
21936
+ node?.Name || node?.Identity?.MethodName,
21937
+ );
21938
+ }
21939
+ }
21940
+ }
21813
21941
  if (slicesData.MethodCalls) {
21814
21942
  for (const amethodCall of slicesData.MethodCalls) {
21815
21943
  if (
@@ -21857,6 +21985,7 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
21857
21985
  apkg.evidence.occurrences = locationOccurrences.map((l) =>
21858
21986
  parseOccurrenceEvidenceLocation(l),
21859
21987
  );
21988
+ addDotnetIdentityMethod(apkg, locationOccurrences[0]);
21860
21989
  // Set the package scope
21861
21990
  apkg.scope = "required";
21862
21991
  }