@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.
@@ -2,6 +2,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
 
5
+ import esmock from "esmock";
5
6
  import { assert, describe, it } from "poku";
6
7
 
7
8
  import {
@@ -248,4 +249,103 @@ describe("cbom utils", () => {
248
249
  rmSync(projectDir, { recursive: true, force: true });
249
250
  }
250
251
  });
252
+
253
+ it("collectDosaiCryptoComponents() maps dosai algorithms to CBOM components with OIDs", async () => {
254
+ const { collectDosaiCryptoComponents } = await esmock("./cbomutils.js", {
255
+ "./dosai.js": {
256
+ analyzeDosaiCrypto: () => ({
257
+ Assets: [
258
+ {
259
+ Id: "cas1",
260
+ AssetType: "algorithm",
261
+ Name: "SHA-256",
262
+ Family: "hash",
263
+ Strength: "strong",
264
+ Location: {
265
+ Path: "Program.cs",
266
+ FileName: "Program.cs",
267
+ LineNumber: 12,
268
+ ColumnNumber: 9,
269
+ },
270
+ ReachableFromEntryPoint: true,
271
+ EntryPointIds: ["ep1"],
272
+ },
273
+ {
274
+ Id: "cas2",
275
+ AssetType: "algorithm",
276
+ Name: "UnknownCipher",
277
+ Location: {
278
+ Path: "Program.cs",
279
+ FileName: "Program.cs",
280
+ LineNumber: 20,
281
+ ColumnNumber: 9,
282
+ },
283
+ },
284
+ ],
285
+ Operations: [
286
+ {
287
+ Id: "cop1",
288
+ OperationType: "hash",
289
+ Algorithm: "SHA-256",
290
+ Location: {
291
+ Path: "Program.cs",
292
+ FileName: "Program.cs",
293
+ LineNumber: 12,
294
+ ColumnNumber: 9,
295
+ },
296
+ },
297
+ {
298
+ Id: "cop2",
299
+ OperationType: "use",
300
+ Algorithm: "SHA-2",
301
+ Symbol: "SHA256.HashData",
302
+ Location: {
303
+ Path: "Program.vb",
304
+ FileName: "Program.vb",
305
+ LineNumber: 42,
306
+ ColumnNumber: 22,
307
+ },
308
+ },
309
+ ],
310
+ }),
311
+ },
312
+ });
313
+
314
+ const components = await collectDosaiCryptoComponents("/tmp/project", {
315
+ evidence: true,
316
+ specVersion: 1.7,
317
+ });
318
+
319
+ assert.strictEqual(components.length, 1);
320
+ assert.strictEqual(components[0].name, "sha-256");
321
+ assert.strictEqual(components[0].type, "cryptographic-asset");
322
+ assert.strictEqual(components[0].cryptoProperties.assetType, "algorithm");
323
+ assert.ok(components[0].cryptoProperties.oid);
324
+ assert.ok(
325
+ components[0].properties.some(
326
+ (property) =>
327
+ property.name === "cdx:crypto:sourceType" &&
328
+ property.value === "dosai:operation",
329
+ ),
330
+ );
331
+ assert.ok(
332
+ !components[0].properties.some(
333
+ (property) =>
334
+ property.name === "cdx:crypto:sourceType" &&
335
+ ["dosai", "js-ast:dosai"].includes(property.value),
336
+ ),
337
+ );
338
+ assert.ok(
339
+ components[0].evidence.occurrences.some(
340
+ (occurrence) =>
341
+ occurrence.location === "Program.cs" && occurrence.line === 12,
342
+ ),
343
+ );
344
+ assert.ok(
345
+ components[0].evidence.occurrences.some(
346
+ (occurrence) =>
347
+ occurrence.location === "Program.vb" && occurrence.line === 42,
348
+ ),
349
+ );
350
+ });
251
351
  });
@@ -342,9 +342,21 @@ function normalizeRunnerLabels(runsOn) {
342
342
  .map((label) => label.trim())
343
343
  .filter(Boolean);
344
344
  }
345
+ if (typeof runsOn === "object") {
346
+ return normalizeRunnerLabels(runsOn.labels);
347
+ }
345
348
  return [];
346
349
  }
347
350
 
351
+ function normalizeRunnerValue(runsOn) {
352
+ const labels = normalizeRunnerLabels(runsOn);
353
+ if (runsOn && typeof runsOn === "object" && !Array.isArray(runsOn)) {
354
+ const group = runsOn.group ? String(runsOn.group).trim() : "";
355
+ return [group, ...labels].filter(Boolean).join(",") || "unknown";
356
+ }
357
+ return labels.join(",") || "unknown";
358
+ }
359
+
348
360
  function isSelfHostedRunner(runsOn) {
349
361
  return normalizeRunnerLabels(runsOn).some((label) =>
350
362
  label.toLowerCase().includes("self-hosted"),
@@ -1761,7 +1773,7 @@ function buildReusableWorkflowComponent(
1761
1773
  { name: "cdx:github:job:name", value: jobName },
1762
1774
  {
1763
1775
  name: "cdx:github:job:runner",
1764
- value: Array.isArray(jobRunner) ? jobRunner.join(",") : jobRunner,
1776
+ value: normalizeRunnerValue(jobRunner),
1765
1777
  },
1766
1778
  { name: "cdx:github:reusableWorkflow:uses", value: uses },
1767
1779
  {
@@ -1951,7 +1963,7 @@ export function parseWorkflowFile(f, options) {
1951
1963
  { name: "cdx:github:job:name", value: jobName },
1952
1964
  {
1953
1965
  name: "cdx:github:job:runner",
1954
- value: Array.isArray(jobRunner) ? jobRunner.join(",") : jobRunner,
1966
+ value: normalizeRunnerValue(jobRunner),
1955
1967
  },
1956
1968
  ];
1957
1969
  if (jobEnvironment) {
@@ -2042,7 +2054,7 @@ export function parseWorkflowFile(f, options) {
2042
2054
  { name: "cdx:github:job:name", value: jobName },
2043
2055
  {
2044
2056
  name: "cdx:github:job:runner",
2045
- value: Array.isArray(jobRunner) ? jobRunner.join(",") : jobRunner,
2057
+ value: normalizeRunnerValue(jobRunner),
2046
2058
  },
2047
2059
  { name: "cdx:github:action:uses", value: step.uses },
2048
2060
  {
@@ -180,6 +180,58 @@ describe("githubActionsParser", () => {
180
180
  }
181
181
  });
182
182
 
183
+ it("normalizes object-form runs-on values", () => {
184
+ const tmpDir = mkdtempSync(path.join(os.tmpdir(), "cdxgen-gha-"));
185
+ const workflowFile = path.join(tmpDir, "object-runs-on.yml");
186
+ writeFileSync(
187
+ workflowFile,
188
+ [
189
+ "name: Object runs-on",
190
+ "on: push",
191
+ "jobs:",
192
+ " grouped:",
193
+ " runs-on:",
194
+ " group: ubuntu-runners",
195
+ " labels: ubuntu-20.04-16core",
196
+ " steps:",
197
+ " - uses: actions/checkout@v4",
198
+ " selfHosted:",
199
+ " runs-on:",
200
+ " group: larger-runners",
201
+ " labels: [self-hosted, linux, x64]",
202
+ " steps:",
203
+ ' - run: echo "ok"',
204
+ ].join("\n"),
205
+ );
206
+
207
+ try {
208
+ const result = parseWorkflowFile(workflowFile, { specVersion: 1.7 });
209
+ const actionComp = result.components.find(
210
+ (component) =>
211
+ getProp(component, "cdx:github:action:uses") ===
212
+ "actions/checkout@v4",
213
+ );
214
+ assert.strictEqual(
215
+ getProp(actionComp, "cdx:github:job:runner"),
216
+ "ubuntu-runners,ubuntu-20.04-16core",
217
+ );
218
+
219
+ const selfHostedTask = result.workflows[0].tasks.find(
220
+ (task) => task.name === "selfHosted",
221
+ );
222
+ assert.strictEqual(
223
+ getProp(selfHostedTask, "cdx:github:job:runner"),
224
+ "larger-runners,self-hosted,linux,x64",
225
+ );
226
+ assert.strictEqual(
227
+ getProp(selfHostedTask, "cdx:github:job:isSelfHosted"),
228
+ "true",
229
+ );
230
+ } finally {
231
+ rmSync(tmpDir, { force: true, recursive: true });
232
+ }
233
+ });
234
+
183
235
  it("derives unnamed workflow names from the file stem without leaking Windows-style path segments", () => {
184
236
  const tmpDir = mkdtempSync(path.join(os.tmpdir(), "cdxgen-gha-"));
185
237
  const workflowFile = path.join(tmpDir, "nested\\workflow-file.yml");
@@ -0,0 +1,433 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { basename, delimiter, join, resolve } from "node:path";
3
+
4
+ import {
5
+ addDosaiSetValue,
6
+ buildDosaiPurlAliasMap,
7
+ dosaiSourceLocation,
8
+ dosaiSourceLocationFromNode,
9
+ resolveDosaiComponentPurl,
10
+ } from "./dosaiParsers.js";
11
+ import { resolvePluginBinary } from "./plugins.js";
12
+ import {
13
+ DEBUG_MODE,
14
+ getTmpDir,
15
+ safeExistsSync,
16
+ safeMkdtempSync,
17
+ safeRmSync,
18
+ safeSpawnSync,
19
+ } from "./utils.js";
20
+
21
+ const DOTNET_LANGUAGES = new Set([
22
+ "c#",
23
+ "csharp",
24
+ "cs",
25
+ "dotnet",
26
+ "dotnet-framework",
27
+ "f#",
28
+ "fsharp",
29
+ "fs",
30
+ "nuget",
31
+ "vb",
32
+ "vbnet",
33
+ "visualbasic",
34
+ ]);
35
+
36
+ const DOSAI_COMMANDS = new Set(["crypto", "dataflows", "methods"]);
37
+
38
+ function dosaiBin() {
39
+ return resolvePluginBinary("dosai");
40
+ }
41
+
42
+ function frameFromDosaiNode(node) {
43
+ if (!node) {
44
+ return undefined;
45
+ }
46
+ const fullFilename =
47
+ node.Path || node.FileName || node.CallLocation?.FileName;
48
+ if (!fullFilename || fullFilename === "<unknown>") {
49
+ return undefined;
50
+ }
51
+ return {
52
+ package: node.Namespace || "",
53
+ module: node.ClassName || node.Module || "",
54
+ function: node.MethodName || node.Name || node.CalledMethodName || "",
55
+ line: node.LineNumber || node.CallLocation?.LineNumber || undefined,
56
+ column: node.ColumnNumber || node.CallLocation?.ColumnNumber || undefined,
57
+ fullFilename,
58
+ };
59
+ }
60
+
61
+ function appendUniqueProperty(properties, name, value) {
62
+ if (value === undefined || value === null || value === "") {
63
+ return;
64
+ }
65
+ if (
66
+ !properties.some(
67
+ (property) => property.name === name && property.value === String(value),
68
+ )
69
+ ) {
70
+ properties.push({ name, value: String(value) });
71
+ }
72
+ }
73
+
74
+ function sanitizeEndpoint(endpoint) {
75
+ const value = String(endpoint || "").trim();
76
+ if (!value) {
77
+ return undefined;
78
+ }
79
+ if (/^https?:\/\//i.test(value)) {
80
+ try {
81
+ const parsedUrl = new URL(value);
82
+ parsedUrl.username = "";
83
+ parsedUrl.password = "";
84
+ parsedUrl.search = "";
85
+ parsedUrl.hash = "";
86
+ return parsedUrl.toString();
87
+ } catch (_err) {
88
+ return undefined;
89
+ }
90
+ }
91
+ return value.split("?")[0].split("#")[0].slice(0, 512);
92
+ }
93
+
94
+ function serviceNameFromEndpoint(endpoint) {
95
+ const className = endpoint.ClassName || endpoint.FileName || "dotnet";
96
+ const methodName = endpoint.MethodName || endpoint.HttpMethod || "endpoint";
97
+ return `dosai-${className}-${methodName}-service`
98
+ .replace(/[^A-Za-z0-9_.-]+/g, "-")
99
+ .replace(/-+/g, "-");
100
+ }
101
+
102
+ function dosaiSdkMessage(result) {
103
+ return (
104
+ result?.stdout?.includes(
105
+ "You must install or update .NET to run this application",
106
+ ) ||
107
+ result?.stderr?.includes(
108
+ "You must install or update .NET to run this application",
109
+ )
110
+ );
111
+ }
112
+
113
+ function safeDosaiPath(value) {
114
+ if (!value || typeof value !== "string" || /[\0\r\n]/.test(value)) {
115
+ return undefined;
116
+ }
117
+ return resolve(value);
118
+ }
119
+
120
+ function safeDosaiPatternPacks(value) {
121
+ if (!value || typeof value !== "string" || /[\0\r\n]/.test(value)) {
122
+ return undefined;
123
+ }
124
+ return value
125
+ .split(delimiter)
126
+ .map((patternPack) => safeDosaiPath(patternPack.trim()))
127
+ .filter(Boolean)
128
+ .join(delimiter);
129
+ }
130
+
131
+ function safeDosaiExecutable(value) {
132
+ if (!value || typeof value !== "string" || /[\0\r\n]/.test(value)) {
133
+ return undefined;
134
+ }
135
+ return value.trim();
136
+ }
137
+
138
+ export function isDosaiDotnetLanguage(language) {
139
+ return DOTNET_LANGUAGES.has(String(language || "").toLowerCase());
140
+ }
141
+
142
+ export function readDosaiJsonFile(jsonFile) {
143
+ if (!jsonFile || !safeExistsSync(jsonFile)) {
144
+ return undefined;
145
+ }
146
+ try {
147
+ return JSON.parse(readFileSync(jsonFile, "utf-8"));
148
+ } catch (_err) {
149
+ return undefined;
150
+ }
151
+ }
152
+
153
+ export function runDosaiCommand(command, src, outputFile, options = {}) {
154
+ if (!DOSAI_COMMANDS.has(command)) {
155
+ return false;
156
+ }
157
+ const executable = safeDosaiExecutable(options.dosaiCommand || dosaiBin());
158
+ const srcPath = safeDosaiPath(src);
159
+ const outputPath = safeDosaiPath(outputFile);
160
+ if (!executable || !srcPath || !outputPath) {
161
+ return false;
162
+ }
163
+ const args = [command, "--path", srcPath, "--o", outputPath];
164
+ if (command === "dataflows") {
165
+ if (options.dataFlowPatterns) {
166
+ const patternsPath = safeDosaiPath(options.dataFlowPatterns);
167
+ if (patternsPath) {
168
+ args.push("--patterns", patternsPath);
169
+ }
170
+ }
171
+ if (options.dataFlowPatternPacks || options.patternPacks) {
172
+ const patternPacks = safeDosaiPatternPacks(
173
+ options.dataFlowPatternPacks || options.patternPacks,
174
+ );
175
+ if (patternPacks) {
176
+ args.push("--pattern-packs", patternPacks);
177
+ }
178
+ }
179
+ } else if (command === "crypto") {
180
+ args.push("--format", "dosai");
181
+ }
182
+ if (DEBUG_MODE) {
183
+ console.log("Executing", executable, args.join(" "));
184
+ }
185
+ const result = safeSpawnSync(executable, args, {
186
+ cwd: srcPath,
187
+ shell: false,
188
+ });
189
+ if (dosaiSdkMessage(result)) {
190
+ console.log(
191
+ "Dotnet SDK is not installed. Please use the cdxgen dotnet container images to analyze this project with dosai.",
192
+ );
193
+ console.log(
194
+ "Alternatively, download the dosai self-contained binary (-full suffix) from https://github.com/owasp-dep-scan/dosai/releases and set DOSAI_CMD to its location.",
195
+ );
196
+ }
197
+ if (result?.status !== 0 || result?.error || !safeExistsSync(outputPath)) {
198
+ if (DEBUG_MODE) {
199
+ if (result?.stderr || result?.stdout) {
200
+ console.error(result.stdout, result.stderr);
201
+ } else {
202
+ console.log("Check if the dosai plugin was installed successfully.");
203
+ }
204
+ }
205
+ return false;
206
+ }
207
+ return true;
208
+ }
209
+
210
+ export function createDosaiMethodsSlice(src, outputFile, options = {}) {
211
+ return runDosaiCommand("methods", src, outputFile, options);
212
+ }
213
+
214
+ export function createDosaiDataFlowSlice(src, outputFile, options = {}) {
215
+ return runDosaiCommand("dataflows", src, outputFile, options);
216
+ }
217
+
218
+ export function createDosaiCryptoAnalysis(src, outputFile, options = {}) {
219
+ return runDosaiCommand("crypto", src, outputFile, options);
220
+ }
221
+
222
+ export function analyzeDosaiCrypto(src, options = {}) {
223
+ const tempDir = safeMkdtempSync(join(getTmpDir(), "dosai-crypto-"));
224
+ const outputFile = join(tempDir, "dosai-crypto.json");
225
+ try {
226
+ if (!createDosaiCryptoAnalysis(src, outputFile, options)) {
227
+ return undefined;
228
+ }
229
+ return readDosaiJsonFile(outputFile);
230
+ } finally {
231
+ if (tempDir?.startsWith(getTmpDir())) {
232
+ safeRmSync(tempDir, { recursive: true, force: true });
233
+ }
234
+ }
235
+ }
236
+
237
+ export function buildPurlAliasMap(components = []) {
238
+ return buildDosaiPurlAliasMap(components);
239
+ }
240
+
241
+ export function resolveComponentPurl(purl, purlAliasMap) {
242
+ return resolveDosaiComponentPurl(purl, purlAliasMap);
243
+ }
244
+
245
+ export function collectDosaiPurlEvidence(methodsSlice, components = []) {
246
+ const purlAliasMap = buildPurlAliasMap(components);
247
+ const purlLocationMap = {};
248
+ const purlModulesMap = {};
249
+ const purlMethodsMap = {};
250
+ const edgesById = new Map(
251
+ (methodsSlice?.CallGraph?.Edges || []).map((edge) => [edge.Id, edge]),
252
+ );
253
+ const nodesById = new Map(
254
+ (methodsSlice?.CallGraph?.Nodes || []).map((node) => [node.Id, node]),
255
+ );
256
+
257
+ for (const dependency of methodsSlice?.Dependencies || []) {
258
+ const purl = resolveComponentPurl(dependency.Purl, purlAliasMap);
259
+ if (!purl) {
260
+ continue;
261
+ }
262
+ addDosaiSetValue(purlLocationMap, purl, dosaiSourceLocation(dependency));
263
+ addDosaiSetValue(
264
+ purlModulesMap,
265
+ purl,
266
+ dependency.Name || dependency.Namespace,
267
+ );
268
+ }
269
+
270
+ for (const reachability of methodsSlice?.PackageReachability || []) {
271
+ const purl = resolveComponentPurl(reachability.Purl, purlAliasMap);
272
+ if (!purl) {
273
+ continue;
274
+ }
275
+ let hasExplicitSourceLocations = false;
276
+ for (const sourceLocation of reachability.SourceLocations || []) {
277
+ const location = dosaiSourceLocation(sourceLocation);
278
+ addDosaiSetValue(purlLocationMap, purl, location);
279
+ hasExplicitSourceLocations ||= Boolean(location);
280
+ }
281
+ for (const edgeId of reachability.EdgeIds || []) {
282
+ const edge = edgesById.get(edgeId);
283
+ if (!hasExplicitSourceLocations) {
284
+ addDosaiSetValue(purlLocationMap, purl, dosaiSourceLocation(edge));
285
+ }
286
+ addDosaiSetValue(
287
+ purlMethodsMap,
288
+ purl,
289
+ edge?.CalledMethodName || edge?.TargetName,
290
+ );
291
+ }
292
+ for (const nodeId of reachability.NodeIds || []) {
293
+ const node = nodesById.get(nodeId);
294
+ if (!hasExplicitSourceLocations) {
295
+ addDosaiSetValue(
296
+ purlLocationMap,
297
+ purl,
298
+ dosaiSourceLocationFromNode(node),
299
+ );
300
+ }
301
+ addDosaiSetValue(purlModulesMap, purl, node?.ClassName || node?.Module);
302
+ addDosaiSetValue(
303
+ purlMethodsMap,
304
+ purl,
305
+ node?.Name || node?.Identity?.MethodName,
306
+ );
307
+ }
308
+ }
309
+ return { purlLocationMap, purlModulesMap, purlMethodsMap };
310
+ }
311
+
312
+ export function collectDosaiDataFlowFrames(dataFlowResult, components = []) {
313
+ const purlAliasMap = buildPurlAliasMap(components);
314
+ const nodesById = new Map(
315
+ (dataFlowResult?.Nodes || []).map((node) => [node.Id, node]),
316
+ );
317
+ const dataFlowFrames = {};
318
+ const addFramesForPurl = (purl, frames) => {
319
+ const componentPurl = resolveComponentPurl(purl, purlAliasMap);
320
+ if (!componentPurl || !frames.length) {
321
+ return;
322
+ }
323
+ dataFlowFrames[componentPurl] ??= [];
324
+ dataFlowFrames[componentPurl].push(frames);
325
+ };
326
+
327
+ for (const slice of dataFlowResult?.Slices || []) {
328
+ const frames = (slice.NodeIds || [])
329
+ .map((nodeId) => frameFromDosaiNode(nodesById.get(nodeId)))
330
+ .filter(Boolean);
331
+ const purls = new Set(
332
+ [...(slice.Purls || []), slice.SourcePurl, slice.SinkPurl].filter(
333
+ Boolean,
334
+ ),
335
+ );
336
+ for (const purl of purls) {
337
+ addFramesForPurl(purl, frames);
338
+ }
339
+ }
340
+
341
+ for (const reachability of dataFlowResult?.PackageReachability || []) {
342
+ const frames = (reachability.NodeIds || [])
343
+ .map((nodeId) => frameFromDosaiNode(nodesById.get(nodeId)))
344
+ .filter(Boolean);
345
+ addFramesForPurl(reachability.Purl, frames);
346
+ }
347
+ return dataFlowFrames;
348
+ }
349
+
350
+ export function collectDosaiServicesFromMethods(
351
+ methodsSlice,
352
+ servicesMap = {},
353
+ ) {
354
+ for (const endpoint of methodsSlice?.ApiEndpoints || []) {
355
+ const route = sanitizeEndpoint(endpoint.Route || endpoint.Path);
356
+ if (!route) {
357
+ continue;
358
+ }
359
+ const serviceName = serviceNameFromEndpoint(endpoint);
360
+ servicesMap[serviceName] ??= {
361
+ endpoints: new Set(),
362
+ authenticated: endpoint.AuthorizationRequired,
363
+ xTrustBoundary:
364
+ endpoint.AuthorizationRequired === true ? true : undefined,
365
+ properties: [],
366
+ };
367
+ servicesMap[serviceName].endpoints.add(route);
368
+ const properties = servicesMap[serviceName].properties;
369
+ appendUniqueProperty(
370
+ properties,
371
+ "cdx:service:httpMethod",
372
+ endpoint.HttpMethod || "ANY",
373
+ );
374
+ appendUniqueProperty(
375
+ properties,
376
+ "cdx:dosai:endpointKind",
377
+ endpoint.EndpointKind,
378
+ );
379
+ appendUniqueProperty(
380
+ properties,
381
+ "cdx:dosai:authorizationRequired",
382
+ endpoint.AuthorizationRequired,
383
+ );
384
+ appendUniqueProperty(
385
+ properties,
386
+ "cdx:dosai:allowAnonymous",
387
+ endpoint.AllowAnonymous,
388
+ );
389
+ appendUniqueProperty(
390
+ properties,
391
+ "cdx:dosai:authorizationPolicyCount",
392
+ endpoint.AuthorizationPolicies?.length,
393
+ );
394
+ appendUniqueProperty(
395
+ properties,
396
+ "cdx:dosai:roleCount",
397
+ endpoint.Roles?.length,
398
+ );
399
+ appendUniqueProperty(
400
+ properties,
401
+ "cdx:dosai:requiredClaimCount",
402
+ endpoint.RequiredClaims?.length,
403
+ );
404
+ appendUniqueProperty(
405
+ properties,
406
+ "cdx:dosai:requiredScopeCount",
407
+ endpoint.RequiredScopes?.length,
408
+ );
409
+ appendUniqueProperty(
410
+ properties,
411
+ "SrcFile",
412
+ endpoint.Path || endpoint.FileName,
413
+ );
414
+ if (endpoint.LineNumber) {
415
+ appendUniqueProperty(
416
+ properties,
417
+ "cdx:dosai:location",
418
+ `${endpoint.Path || endpoint.FileName}:${endpoint.LineNumber}:${endpoint.ColumnNumber || 0}`,
419
+ );
420
+ }
421
+ }
422
+ return servicesMap;
423
+ }
424
+
425
+ export function normalizeDosaiServiceMap(servicesMap = {}) {
426
+ return Object.keys(servicesMap).map((serviceName) => ({
427
+ name: serviceName || `dosai-${basename(serviceName)}-service`,
428
+ endpoints: Array.from(servicesMap[serviceName].endpoints || []).sort(),
429
+ authenticated: servicesMap[serviceName].authenticated,
430
+ "x-trust-boundary": servicesMap[serviceName].xTrustBoundary,
431
+ properties: servicesMap[serviceName].properties,
432
+ }));
433
+ }