@cyclonedx/cdxgen 12.4.0 → 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.
Files changed (71) hide show
  1. package/README.md +6 -4
  2. package/bin/cdxgen.js +32 -11
  3. package/bin/convert.js +12 -8
  4. package/bin/evinse.js +15 -0
  5. package/bin/hbom.js +13 -8
  6. package/bin/repl.js +14 -10
  7. package/bin/validate.js +10 -13
  8. package/bin/verify.js +7 -29
  9. package/data/cyclonedx-2.0-bundled.schema.json +7182 -0
  10. package/lib/audit/index.js +2 -1
  11. package/lib/cli/index.js +77 -16
  12. package/lib/cli/index.poku.js +197 -0
  13. package/lib/evinser/evinser.js +118 -3
  14. package/lib/helpers/bomUtils.js +155 -1
  15. package/lib/helpers/bomUtils.poku.js +79 -1
  16. package/lib/helpers/cbomutils.js +162 -2
  17. package/lib/helpers/cbomutils.poku.js +100 -0
  18. package/lib/helpers/ciParsers/githubActions.js +15 -3
  19. package/lib/helpers/ciParsers/githubActions.poku.js +52 -0
  20. package/lib/helpers/dosai.js +433 -0
  21. package/lib/helpers/dosai.poku.js +302 -0
  22. package/lib/helpers/dosaiParsers.js +103 -0
  23. package/lib/helpers/plugins.js +17 -16
  24. package/lib/helpers/protobom.js +53 -0
  25. package/lib/helpers/protobom.poku.js +44 -1
  26. package/lib/helpers/protobomLoader.js +43 -0
  27. package/lib/helpers/protobomLoader.poku.js +31 -0
  28. package/lib/helpers/utils.js +130 -1
  29. package/lib/helpers/utils.poku.js +295 -0
  30. package/lib/server/server.js +2 -1
  31. package/lib/stages/postgen/annotator.js +2 -1
  32. package/lib/stages/postgen/annotator.poku.js +28 -0
  33. package/lib/stages/postgen/postgen.js +219 -12
  34. package/lib/stages/postgen/postgen.poku.js +163 -0
  35. package/lib/validator/bomValidator.js +90 -38
  36. package/lib/validator/bomValidator.poku.js +90 -0
  37. package/lib/validator/complianceRules.js +4 -2
  38. package/lib/validator/index.poku.js +14 -0
  39. package/package.json +12 -12
  40. package/types/bin/repl.d.ts +1 -1
  41. package/types/bin/repl.d.ts.map +1 -1
  42. package/types/lib/audit/index.d.ts.map +1 -1
  43. package/types/lib/cli/index.d.ts.map +1 -1
  44. package/types/lib/evinser/evinser.d.ts +15 -0
  45. package/types/lib/evinser/evinser.d.ts.map +1 -1
  46. package/types/lib/helpers/bomUtils.d.ts +8 -0
  47. package/types/lib/helpers/bomUtils.d.ts.map +1 -1
  48. package/types/lib/helpers/cbomutils.d.ts +1 -0
  49. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  50. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  51. package/types/lib/helpers/dosai.d.ts +24 -0
  52. package/types/lib/helpers/dosai.d.ts.map +1 -0
  53. package/types/lib/helpers/dosaiParsers.d.ts +8 -0
  54. package/types/lib/helpers/dosaiParsers.d.ts.map +1 -0
  55. package/types/lib/helpers/hbomAnalysis.d.ts +14 -0
  56. package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -1
  57. package/types/lib/helpers/hostTopology.d.ts.map +1 -1
  58. package/types/lib/helpers/plugins.d.ts.map +1 -1
  59. package/types/lib/helpers/protobom.d.ts +2 -0
  60. package/types/lib/helpers/protobom.d.ts.map +1 -1
  61. package/types/lib/helpers/protobomLoader.d.ts +17 -0
  62. package/types/lib/helpers/protobomLoader.d.ts.map +1 -0
  63. package/types/lib/helpers/utils.d.ts.map +1 -1
  64. package/types/lib/server/server.d.ts.map +1 -1
  65. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  66. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  67. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  68. package/types/lib/third-party/arborist/lib/node.d.ts +23 -0
  69. package/types/lib/third-party/arborist/lib/node.d.ts.map +1 -1
  70. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  71. package/types/lib/validator/complianceRules.d.ts.map +1 -1
@@ -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
  }
@@ -54,6 +54,7 @@ import {
54
54
  isPartialTree,
55
55
  isValidIriReference,
56
56
  mapConanPkgRefToPurlStringAndNameAndVersion,
57
+ PROJECT_TYPE_ALIASES,
57
58
  parseBazelActionGraph,
58
59
  parseBazelBuild,
59
60
  parseBazelSkyframe,
@@ -4327,6 +4328,296 @@ it("addEvidenceForDotnet() initializes evidence before adding occurrences", () =
4327
4328
  }
4328
4329
  });
4329
4330
 
4331
+ it("addEvidenceForDotnet() ignores unreadable dosai JSON", () => {
4332
+ const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-dotnet-bad-json-"));
4333
+ const slicesFile = path.join(tempDir, "dosai.json");
4334
+ try {
4335
+ writeFileSync(slicesFile, "");
4336
+ const inputPkgList = [
4337
+ {
4338
+ name: "Example",
4339
+ purl: "pkg:nuget/Example@1.0.0",
4340
+ properties: [{ name: "PackageFiles", value: "Example.dll" }],
4341
+ },
4342
+ ];
4343
+ const pkgList = addEvidenceForDotnet(inputPkgList, slicesFile);
4344
+
4345
+ assert.strictEqual(pkgList, inputPkgList);
4346
+ assert.strictEqual(pkgList[0].evidence, undefined);
4347
+ assert.strictEqual(pkgList[0].scope, undefined);
4348
+ } finally {
4349
+ rmSync(tempDir, { recursive: true, force: true });
4350
+ }
4351
+ });
4352
+
4353
+ it("addEvidenceForDotnet() consumes dosai v3 PackageReachability", () => {
4354
+ const tempDir = mkdtempSync(
4355
+ path.join(tmpdir(), "cdxgen-dotnet-reachability-"),
4356
+ );
4357
+ const slicesFile = path.join(tempDir, "dosai.json");
4358
+ try {
4359
+ writeFileSync(
4360
+ slicesFile,
4361
+ JSON.stringify({
4362
+ CallGraph: {
4363
+ Edges: [
4364
+ {
4365
+ Id: "e1",
4366
+ FileName: "Controllers/EpisodesController.cs",
4367
+ LineNumber: 17,
4368
+ CalledMethodName: "System.Text.Json.JsonSerializer.Deserialize",
4369
+ },
4370
+ ],
4371
+ Nodes: [
4372
+ {
4373
+ Id: "n1",
4374
+ FileName: "System.Text.Json.dll",
4375
+ LineNumber: 0,
4376
+ ClassName: "JsonSerializer",
4377
+ Name: "Deserialize",
4378
+ },
4379
+ ],
4380
+ },
4381
+ PackageReachability: [
4382
+ {
4383
+ Purl: "pkg:nuget/System.Text.Json",
4384
+ EdgeIds: ["e1"],
4385
+ NodeIds: ["n1"],
4386
+ SourceLocations: [
4387
+ {
4388
+ Path: "Controllers/Parser.cs",
4389
+ FileName: "Parser.cs",
4390
+ LineNumber: 42,
4391
+ ColumnNumber: 13,
4392
+ Kind: "CallGraphEdge",
4393
+ },
4394
+ {
4395
+ Path: "System.Text.Json.dll",
4396
+ FileName: "System.Text.Json.dll",
4397
+ LineNumber: 1,
4398
+ Kind: "CallGraphNode",
4399
+ },
4400
+ ],
4401
+ },
4402
+ ],
4403
+ }),
4404
+ );
4405
+ const pkgList = addEvidenceForDotnet(
4406
+ [
4407
+ {
4408
+ name: "System.Text.Json",
4409
+ purl: "pkg:nuget/System.Text.Json@10.0.0",
4410
+ properties: [],
4411
+ },
4412
+ ],
4413
+ slicesFile,
4414
+ );
4415
+
4416
+ assert.deepStrictEqual(pkgList[0].evidence?.occurrences, [
4417
+ {
4418
+ location: "Controllers/Parser.cs",
4419
+ line: 42,
4420
+ },
4421
+ ]);
4422
+ assert.ok(
4423
+ pkgList[0].evidence.identity.methods.some(
4424
+ (method) =>
4425
+ method.technique === "source-code-analysis" &&
4426
+ method.value === "Controllers/Parser.cs#42",
4427
+ ),
4428
+ );
4429
+ assert.ok(
4430
+ pkgList[0].properties.some(
4431
+ (property) =>
4432
+ property.name === "CalledMethods" &&
4433
+ property.value.includes(
4434
+ "System.Text.Json.JsonSerializer.Deserialize",
4435
+ ),
4436
+ ),
4437
+ );
4438
+ } finally {
4439
+ rmSync(tempDir, { recursive: true, force: true });
4440
+ }
4441
+ });
4442
+
4443
+ it("addEvidenceForDotnet() keeps PackageReachability fallback evidence source-only", () => {
4444
+ const tempDir = mkdtempSync(
4445
+ path.join(tmpdir(), "cdxgen-dotnet-source-fallback-"),
4446
+ );
4447
+ const slicesFile = path.join(tempDir, "dosai.json");
4448
+ try {
4449
+ writeFileSync(
4450
+ slicesFile,
4451
+ JSON.stringify({
4452
+ CallGraph: {
4453
+ Edges: [
4454
+ {
4455
+ Id: "e1",
4456
+ FileName: "System.Text.Json.dll",
4457
+ LineNumber: 12,
4458
+ CalledMethodName: "System.Text.Json.JsonSerializer.Deserialize",
4459
+ CallLocation: {
4460
+ FileName: "Program.fs",
4461
+ LineNumber: 8,
4462
+ },
4463
+ },
4464
+ {
4465
+ Id: "e2",
4466
+ FileName: "Controllers/EpisodesController.cs",
4467
+ LineNumber: 17,
4468
+ CalledMethodName: "System.Text.Json.JsonSerializer.Serialize",
4469
+ },
4470
+ ],
4471
+ },
4472
+ PackageReachability: [
4473
+ {
4474
+ Purl: "pkg:nuget/System.Text.Json",
4475
+ EdgeIds: ["e1", "e2"],
4476
+ },
4477
+ ],
4478
+ }),
4479
+ );
4480
+ const pkgList = addEvidenceForDotnet(
4481
+ [
4482
+ {
4483
+ name: "System.Text.Json",
4484
+ purl: "pkg:nuget/System.Text.Json@10.0.0",
4485
+ properties: [],
4486
+ },
4487
+ ],
4488
+ slicesFile,
4489
+ );
4490
+
4491
+ assert.deepStrictEqual(pkgList[0].evidence?.occurrences, [
4492
+ {
4493
+ location: "Controllers/EpisodesController.cs",
4494
+ line: 17,
4495
+ },
4496
+ {
4497
+ location: "Program.fs",
4498
+ line: 8,
4499
+ },
4500
+ ]);
4501
+ assert.ok(!JSON.stringify(pkgList[0].evidence).includes(".dll"));
4502
+ } finally {
4503
+ rmSync(tempDir, { recursive: true, force: true });
4504
+ }
4505
+ });
4506
+
4507
+ it("addEvidenceForDotnet() preserves additional identity entries", () => {
4508
+ const tempDir = mkdtempSync(
4509
+ path.join(tmpdir(), "cdxgen-dotnet-identity-array-"),
4510
+ );
4511
+ const slicesFile = path.join(tempDir, "dosai.json");
4512
+ try {
4513
+ writeFileSync(
4514
+ slicesFile,
4515
+ JSON.stringify({
4516
+ Dependencies: [
4517
+ {
4518
+ Path: "Program.cs",
4519
+ FileName: "Program.cs",
4520
+ Name: "System.Text.Json",
4521
+ Purl: "pkg:nuget/System.Text.Json",
4522
+ LineNumber: 12,
4523
+ },
4524
+ ],
4525
+ }),
4526
+ );
4527
+ const pkgList = addEvidenceForDotnet(
4528
+ [
4529
+ {
4530
+ name: "System.Text.Json",
4531
+ purl: "pkg:nuget/System.Text.Json@10.0.0",
4532
+ evidence: {
4533
+ identity: [
4534
+ {
4535
+ field: "name",
4536
+ confidence: 0.8,
4537
+ methods: [
4538
+ { technique: "filename", value: "packages.lock.json" },
4539
+ ],
4540
+ },
4541
+ {
4542
+ field: "purl",
4543
+ confidence: 1,
4544
+ methods: [
4545
+ {
4546
+ technique: "manifest-analysis",
4547
+ value: "packages.lock.json",
4548
+ },
4549
+ ],
4550
+ },
4551
+ ],
4552
+ },
4553
+ properties: [],
4554
+ },
4555
+ ],
4556
+ slicesFile,
4557
+ );
4558
+
4559
+ assert.strictEqual(pkgList[0].evidence.identity.length, 2);
4560
+ assert.strictEqual(pkgList[0].evidence.identity[0].field, "name");
4561
+ assert.ok(
4562
+ pkgList[0].evidence.identity[1].methods.some(
4563
+ (method) =>
4564
+ method.technique === "source-code-analysis" &&
4565
+ method.value === "Program.cs#12",
4566
+ ),
4567
+ );
4568
+ } finally {
4569
+ rmSync(tempDir, { recursive: true, force: true });
4570
+ }
4571
+ });
4572
+
4573
+ it("addEvidenceForDotnet() consumes dosai Dependencies with purls", () => {
4574
+ const tempDir = mkdtempSync(path.join(tmpdir(), "cdxgen-dotnet-vb-deps-"));
4575
+ const slicesFile = path.join(tempDir, "dosai.json");
4576
+ try {
4577
+ writeFileSync(
4578
+ slicesFile,
4579
+ JSON.stringify({
4580
+ Dependencies: [
4581
+ {
4582
+ Path: "Program.vb",
4583
+ FileName: "Program.vb",
4584
+ Name: "Newtonsoft.Json",
4585
+ Purl: "pkg:nuget/Newtonsoft.Json@13.0.3",
4586
+ LineNumber: 4,
4587
+ ColumnNumber: 9,
4588
+ },
4589
+ ],
4590
+ }),
4591
+ );
4592
+ const pkgList = addEvidenceForDotnet(
4593
+ [
4594
+ {
4595
+ name: "Newtonsoft.Json",
4596
+ purl: "pkg:nuget/Newtonsoft.Json@13.0.3",
4597
+ properties: [],
4598
+ },
4599
+ ],
4600
+ slicesFile,
4601
+ );
4602
+
4603
+ assert.deepStrictEqual(pkgList[0].evidence?.occurrences, [
4604
+ {
4605
+ location: "Program.vb",
4606
+ line: 4,
4607
+ },
4608
+ ]);
4609
+ assert.ok(
4610
+ pkgList[0].properties.some(
4611
+ (property) =>
4612
+ property.name === "ImportedModules" &&
4613
+ property.value.includes("Newtonsoft.Json"),
4614
+ ),
4615
+ );
4616
+ } finally {
4617
+ rmSync(tempDir, { recursive: true, force: true });
4618
+ }
4619
+ });
4620
+
4330
4621
  it("parse packages.lock.json", () => {
4331
4622
  assert.deepStrictEqual(parseCsPkgLockData(null), {
4332
4623
  dependenciesList: [],
@@ -10229,6 +10520,10 @@ it("parseMakeDFile tests", () => {
10229
10520
  });
10230
10521
 
10231
10522
  it("hasAnyProjectType tests", () => {
10523
+ for (const language of ["vb", "vbnet", "visualbasic", "f#", "fs", "fsharp"]) {
10524
+ assert.ok(PROJECT_TYPE_ALIASES.csharp.includes(language));
10525
+ }
10526
+
10232
10527
  assert.deepStrictEqual(
10233
10528
  hasAnyProjectType(["docker"], {
10234
10529
  projectType: [],
@@ -8,6 +8,7 @@ import compression from "compression";
8
8
  import connect from "connect";
9
9
 
10
10
  import { createBom, submitBom } from "../cli/index.js";
11
+ import { isCycloneDxBom } from "../helpers/bomUtils.js";
11
12
  import { normalizeOutputFormats } from "../helpers/exportUtils.js";
12
13
  import {
13
14
  cleanupSourceDir,
@@ -467,7 +468,7 @@ const start = (options) => {
467
468
  let responseBomJson = bomNSData.bomJson;
468
469
  if (
469
470
  requestedFormats.includes("spdx") &&
470
- bomNSData?.bomJson?.bomFormat === "CycloneDX"
471
+ isCycloneDxBom(bomNSData?.bomJson)
471
472
  ) {
472
473
  responseBomJson = convertCycloneDxToSpdx(bomNSData.bomJson, reqOptions);
473
474
  }
@@ -23,6 +23,7 @@ function humanifyTimestamp(timestamp) {
23
23
  month: "long",
24
24
  day: "numeric",
25
25
  weekday: "long",
26
+ timeZone: "UTC",
26
27
  });
27
28
  }
28
29
 
@@ -123,7 +124,7 @@ function getGitHubWorkflowStats(components) {
123
124
  stats.continueOnError++;
124
125
  }
125
126
  if (propMap["cdx:github:job:runner"]) {
126
- propMap["cdx:github:job:runner"]
127
+ String(propMap["cdx:github:job:runner"])
127
128
  .split(",")
128
129
  .filter((r) => r.includes("$"))
129
130
  .forEach((r) => {
@@ -432,3 +432,31 @@ it("recognizes HBOMs and summarizes hardware-specific metadata", () => {
432
432
  );
433
433
  assert.match(summary, /Identifier policy is 'redacted-by-default'\./u);
434
434
  });
435
+
436
+ it("handles non-string GitHub runner properties while summarizing metadata", () => {
437
+ const summary = textualMetadata({
438
+ bomFormat: "CycloneDX",
439
+ specVersion: "1.7",
440
+ metadata: {
441
+ timestamp: "2026-01-01T00:00:00Z",
442
+ tools: { components: [{ name: "cdxgen" }] },
443
+ component: { name: "workflow", type: "application" },
444
+ },
445
+ components: [
446
+ {
447
+ name: "checkout",
448
+ purl: "pkg:github/actions/checkout@v4",
449
+ properties: [
450
+ { name: "cdx:github:workflow:name", value: "CI" },
451
+ { name: "cdx:github:job:name", value: "build" },
452
+ {
453
+ name: "cdx:github:job:runner",
454
+ value: { group: "ubuntu-runners", labels: "ubuntu-latest" },
455
+ },
456
+ ],
457
+ },
458
+ ],
459
+ });
460
+
461
+ assert.match(summary, /GitHub Action references/u);
462
+ });