@cyclonedx/cdxgen 12.4.1 → 12.4.3

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.
Files changed (34) hide show
  1. package/bin/evinse.js +15 -0
  2. package/lib/cli/index.js +60 -9
  3. package/lib/cli/index.poku.js +161 -0
  4. package/lib/evinser/evinser.js +118 -3
  5. package/lib/helpers/cbomutils.js +162 -2
  6. package/lib/helpers/cbomutils.poku.js +100 -0
  7. package/lib/helpers/ciParsers/githubActions.js +15 -3
  8. package/lib/helpers/ciParsers/githubActions.poku.js +52 -0
  9. package/lib/helpers/display.js +12 -6
  10. package/lib/helpers/display.poku.js +38 -0
  11. package/lib/helpers/dosai.js +433 -0
  12. package/lib/helpers/dosai.poku.js +302 -0
  13. package/lib/helpers/dosaiParsers.js +103 -0
  14. package/lib/helpers/utils.js +198 -1
  15. package/lib/helpers/utils.poku.js +352 -0
  16. package/lib/stages/postgen/annotator.js +2 -1
  17. package/lib/stages/postgen/annotator.poku.js +28 -0
  18. package/package.json +12 -12
  19. package/types/lib/cli/index.d.ts.map +1 -1
  20. package/types/lib/evinser/evinser.d.ts +15 -0
  21. package/types/lib/evinser/evinser.d.ts.map +1 -1
  22. package/types/lib/helpers/bomUtils.d.ts +1 -3
  23. package/types/lib/helpers/bomUtils.d.ts.map +1 -1
  24. package/types/lib/helpers/cbomutils.d.ts +1 -0
  25. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  26. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  27. package/types/lib/helpers/display.d.ts.map +1 -1
  28. package/types/lib/helpers/dosai.d.ts +24 -0
  29. package/types/lib/helpers/dosai.d.ts.map +1 -0
  30. package/types/lib/helpers/dosaiParsers.d.ts +8 -0
  31. package/types/lib/helpers/dosaiParsers.d.ts.map +1 -0
  32. package/types/lib/helpers/utils.d.ts.map +1 -1
  33. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  34. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
 
4
4
  import { executeOsQuery } from "../managers/binary.js";
5
5
  import { detectJsCryptoInventory } from "./analyzer.js";
6
+ import { analyzeDosaiCrypto } from "./dosai.js";
6
7
  import {
7
8
  createOccurrenceEvidence,
8
9
  formatOccurrenceEvidence,
@@ -239,16 +240,19 @@ function mergeAlgorithmComponentUsage(component, usage, src, options) {
239
240
  }
240
241
  }
241
242
  if (usage.source) {
243
+ const sourceType =
244
+ usage.source === "dosai" ? undefined : `js-ast:${usage.source}`;
242
245
  if (
246
+ sourceType &&
243
247
  !properties.some(
244
248
  (property) =>
245
249
  property.name === "cdx:crypto:sourceType" &&
246
- property.value === `js-ast:${usage.source}`,
250
+ property.value === sourceType,
247
251
  )
248
252
  ) {
249
253
  properties.push({
250
254
  name: "cdx:crypto:sourceType",
251
- value: `js-ast:${usage.source}`,
255
+ value: sourceType,
252
256
  });
253
257
  }
254
258
  }
@@ -271,6 +275,93 @@ function mergeAlgorithmComponentUsage(component, usage, src, options) {
271
275
  return component;
272
276
  }
273
277
 
278
+ function normalizeDosaiCryptoNames(cryptoObject) {
279
+ const rawName = cryptoObject?.Name || cryptoObject;
280
+ const names = new Set([rawName]);
281
+ const cleanName = String(rawName || "").trim();
282
+ if (!cleanName) {
283
+ return [];
284
+ }
285
+ if (cleanName.includes("/")) {
286
+ for (const part of cleanName
287
+ .split("/")
288
+ .map((candidate) => candidate.trim())
289
+ .filter(Boolean)) {
290
+ names.add(part);
291
+ }
292
+ }
293
+ if (/^SHA-?256$/i.test(cleanName)) {
294
+ names.add("sha-256");
295
+ } else if (/^SHA-?384$/i.test(cleanName)) {
296
+ names.add("sha-384");
297
+ } else if (/^SHA-?512$/i.test(cleanName)) {
298
+ names.add("sha-512");
299
+ } else if (/^SHA-?1$/i.test(cleanName)) {
300
+ names.add("sha-1");
301
+ }
302
+ const context = [
303
+ cryptoObject?.Symbol,
304
+ cryptoObject?.Code,
305
+ cryptoObject?.Algorithm,
306
+ ]
307
+ .filter(Boolean)
308
+ .join(" ");
309
+ if (/^SHA-?2$/i.test(cleanName)) {
310
+ if (/SHA-?256/i.test(context)) {
311
+ names.add("sha-256");
312
+ }
313
+ if (/SHA-?384/i.test(context)) {
314
+ names.add("sha-384");
315
+ }
316
+ if (/SHA-?512/i.test(context)) {
317
+ names.add("sha-512");
318
+ }
319
+ }
320
+ return Array.from(names);
321
+ }
322
+
323
+ function dosaiCryptoUsage(assetOrOperation) {
324
+ const location = assetOrOperation.Location || {};
325
+ return {
326
+ fileName: location.Path || location.FileName,
327
+ lineNumber: location.LineNumber || undefined,
328
+ columnNumber: location.ColumnNumber || undefined,
329
+ primitive: assetOrOperation.Family || assetOrOperation.OperationType,
330
+ source: "dosai",
331
+ };
332
+ }
333
+
334
+ function addDosaiProperties(component, dosaiObject, evidenceType) {
335
+ const properties = component.properties || [];
336
+ const addProperty = (name, value) => {
337
+ if (value === undefined || value === null || value === "") {
338
+ return;
339
+ }
340
+ if (
341
+ !properties.some(
342
+ (property) =>
343
+ property.name === name && property.value === String(value),
344
+ )
345
+ ) {
346
+ properties.push({ name, value: String(value) });
347
+ }
348
+ };
349
+ addProperty("cdx:crypto:sourceType", `dosai:${evidenceType}`);
350
+ addProperty("cdx:dosai:crypto:id", dosaiObject.Id);
351
+ addProperty("cdx:dosai:crypto:strength", dosaiObject.Strength);
352
+ addProperty(
353
+ "cdx:dosai:crypto:reachableFromEntryPoint",
354
+ dosaiObject.ReachableFromEntryPoint,
355
+ );
356
+ if (dosaiObject.EntryPointIds?.length) {
357
+ addProperty(
358
+ "cdx:dosai:crypto:entryPointCount",
359
+ dosaiObject.EntryPointIds.length,
360
+ );
361
+ }
362
+ component.properties = properties;
363
+ }
364
+
274
365
  export async function collectSourceCryptoComponents(src, options = {}) {
275
366
  const inventory = await detectJsCryptoInventory(src, Boolean(options.deep));
276
367
  const componentsByRef = new Map();
@@ -317,6 +408,75 @@ export async function collectSourceCryptoComponents(src, options = {}) {
317
408
  );
318
409
  }
319
410
 
411
+ export async function collectDosaiCryptoComponents(src, options = {}) {
412
+ const dosaiCrypto = analyzeDosaiCrypto(src, options);
413
+ if (!dosaiCrypto) {
414
+ return [];
415
+ }
416
+ const componentsByRef = new Map();
417
+ const cryptoObjects = [
418
+ ...(dosaiCrypto.Assets || []).filter(
419
+ (asset) => asset.AssetType === "algorithm",
420
+ ),
421
+ ...(dosaiCrypto.Operations || []).map((operation) => ({
422
+ ...operation,
423
+ Name: operation.Algorithm,
424
+ Family: operation.OperationType,
425
+ })),
426
+ ];
427
+ for (const cryptoObject of cryptoObjects) {
428
+ for (const candidateName of normalizeDosaiCryptoNames(cryptoObject)) {
429
+ const normalizedName = normalizeDetectedCryptoAlgorithmName(
430
+ candidateName,
431
+ "algorithm",
432
+ );
433
+ const algorithmMetadata =
434
+ cbomCryptoOids[normalizedName] || cbomCryptoOids[candidateName];
435
+ if (!algorithmMetadata?.oid) {
436
+ continue;
437
+ }
438
+ const bomRef = cryptoAlgorithmBomRef(
439
+ normalizedName,
440
+ algorithmMetadata.oid,
441
+ );
442
+ const component = componentsByRef.get(bomRef) || {
443
+ type: "cryptographic-asset",
444
+ name: normalizedName,
445
+ "bom-ref": bomRef,
446
+ description:
447
+ algorithmMetadata.description ||
448
+ "Cryptographic algorithm detected by dosai source analysis",
449
+ cryptoProperties: {
450
+ assetType: "algorithm",
451
+ oid: algorithmMetadata.oid,
452
+ },
453
+ properties: [],
454
+ };
455
+ mergeAlgorithmComponentUsage(
456
+ component,
457
+ dosaiCryptoUsage(cryptoObject),
458
+ src,
459
+ options,
460
+ );
461
+ addDosaiProperties(
462
+ component,
463
+ cryptoObject,
464
+ cryptoObject.OperationType ? "operation" : "asset",
465
+ );
466
+ componentsByRef.set(bomRef, component);
467
+ }
468
+ }
469
+ const components = Array.from(componentsByRef.values());
470
+ components.forEach((component) => {
471
+ normalizeCryptoComponentEvidence(component, options);
472
+ });
473
+ return components.sort((left, right) =>
474
+ `${left.name}:${left["bom-ref"]}`.localeCompare(
475
+ `${right.name}:${right["bom-ref"]}`,
476
+ ),
477
+ );
478
+ }
479
+
320
480
  /**
321
481
  * Find crypto algorithm in the given code snippet
322
482
  *
@@ -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");
@@ -42,6 +42,7 @@ const MULTIVALUE_ACTIVITY_TARGET_KEYS = new Set([
42
42
  "SrcFiles",
43
43
  ]);
44
44
  const PATH_SEPARATOR_REGEX = /[\\/]+/;
45
+ const SUSPICIOUS_SHELL_PATH_LABEL = "⚠ shell-metacharacters";
45
46
  const ENV_AUDIT_SEVERITY_RANK = {
46
47
  low: 1,
47
48
  medium: 2,
@@ -1220,16 +1221,21 @@ export function printActivitySummary(reportType = undefined) {
1220
1221
  }
1221
1222
  return;
1222
1223
  }
1223
- const formatActivityTarget = (target) => {
1224
+ const formatActivityTarget = (activity) => {
1225
+ const target = activity?.target;
1226
+ const suspiciousPrefix =
1227
+ activity?.risk === "shell-metacharacters"
1228
+ ? `${SUSPICIOUS_SHELL_PATH_LABEL}\n`
1229
+ : "";
1224
1230
  if (typeof target !== "string" || !target.includes(",")) {
1225
- return target || "";
1231
+ return `${suspiciousPrefix}${target || ""}`;
1226
1232
  }
1227
1233
  const targetEntries = splitCommaSeparatedActivityEntries(target);
1228
1234
  if (isLikelyActivityPathList(targetEntries)) {
1229
- return sortActivityTargetEntries(targetEntries).join("\n");
1235
+ return `${suspiciousPrefix}${sortActivityTargetEntries(targetEntries).join("\n")}`;
1230
1236
  }
1231
1237
  if (!(target.includes(":") || target.includes("="))) {
1232
- return target || "";
1238
+ return `${suspiciousPrefix}${target || ""}`;
1233
1239
  }
1234
1240
  const targetSegments = target.split(/,\s*(?=[A-Za-z][\w-]*\s*[:=])/);
1235
1241
  let didFormat = false;
@@ -1249,7 +1255,7 @@ export function printActivitySummary(reportType = undefined) {
1249
1255
  .map((entry) => `- ${entry}`)
1250
1256
  .join("\n")}`;
1251
1257
  });
1252
- return didFormat ? renderedSegments.join("\n") : target;
1258
+ return `${suspiciousPrefix}${didFormat ? renderedSegments.join("\n") : target}`;
1253
1259
  };
1254
1260
  const formatActivityType = (type) => {
1255
1261
  if (typeof type !== "string" || !type.includes(",")) {
@@ -1275,7 +1281,7 @@ export function printActivitySummary(reportType = undefined) {
1275
1281
  formatActivityType(activity.projectType),
1276
1282
  activity.packageType || "",
1277
1283
  activity.kind || "",
1278
- formatActivityTarget(activity.target),
1284
+ formatActivityTarget(activity),
1279
1285
  activity.reason
1280
1286
  ? `${formatStatus(activity.status)}\n${activity.reason}`.trim()
1281
1287
  : formatStatus(activity.status),
@@ -481,6 +481,44 @@ it("renders plain comma-separated activity paths one per line sorted by depth",
481
481
  }
482
482
  });
483
483
 
484
+ it("highlights suspicious shell-metacharacter paths in the activity summary", async () => {
485
+ const tableStub = sinon.stub().returns("activity-table");
486
+ const shellIfs = "$" + "{IFS}";
487
+ try {
488
+ const { printActivitySummary: printActivitySummaryMocked } = await esmock(
489
+ "./display.js",
490
+ {
491
+ "./table.js": {
492
+ createStream: sinon.stub(),
493
+ table: tableStub,
494
+ },
495
+ "./utils.js": {
496
+ getRecordedActivities: sinon.stub().returns([
497
+ {
498
+ identifier: "ACT-0005",
499
+ kind: "inspect",
500
+ reason: "Suspicious path contains shell metacharacters.",
501
+ risk: "shell-metacharacters",
502
+ status: "completed",
503
+ target: `/tmp/repo/evil;cd${shellIfs}..;printf${shellIfs}marker>CDXGEN_GITURL_E2E_MARKER;#/pom.xml`,
504
+ },
505
+ ]),
506
+ isDryRun: true,
507
+ isSecureMode: false,
508
+ safeExistsSync: sinon.stub(),
509
+ toCamel: sinon.stub(),
510
+ },
511
+ },
512
+ );
513
+ printActivitySummaryMocked();
514
+ const [data] = tableStub.firstCall.args;
515
+ assert.ok(data[1][4].startsWith("⚠ shell-metacharacters\n"));
516
+ assert.ok(data[1][4].includes(`evil;cd${shellIfs}..`));
517
+ } finally {
518
+ sinon.restore();
519
+ }
520
+ });
521
+
484
522
  it("prints grouped environment audit findings in a secure-mode panel", async () => {
485
523
  const tableStub = sinon.stub().returns("env-audit-table");
486
524
  try {