@cyclonedx/cdxgen 12.3.2 → 12.4.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.
Files changed (182) hide show
  1. package/README.md +70 -22
  2. package/bin/audit.js +21 -7
  3. package/bin/cdxgen.js +238 -116
  4. package/bin/convert.js +28 -13
  5. package/bin/hbom.js +490 -0
  6. package/bin/repl.js +580 -29
  7. package/bin/validate.js +34 -4
  8. package/bin/verify.js +40 -5
  9. package/data/README.md +298 -25
  10. package/data/component-tags.json +6 -0
  11. package/data/crypto-oid.json +16 -0
  12. package/data/predictive-audit-allowlist.json +11 -0
  13. package/data/queries-darwin.json +12 -1
  14. package/data/queries-win.json +7 -1
  15. package/data/queries.json +39 -2
  16. package/data/rules/ai-agent-governance.yaml +16 -0
  17. package/data/rules/asar-archives.yaml +150 -0
  18. package/data/rules/chrome-extensions.yaml +8 -0
  19. package/data/rules/ci-permissions.yaml +171 -15
  20. package/data/rules/container-risk.yaml +14 -7
  21. package/data/rules/dependency-sources.yaml +76 -5
  22. package/data/rules/hbom-compliance.yaml +325 -0
  23. package/data/rules/hbom-performance.yaml +307 -0
  24. package/data/rules/hbom-security.yaml +248 -0
  25. package/data/rules/host-topology.yaml +165 -0
  26. package/data/rules/mcp-servers.yaml +18 -3
  27. package/data/rules/obom-runtime.yaml +907 -22
  28. package/data/rules/package-integrity.yaml +36 -0
  29. package/data/rules/rootfs-hardening.yaml +179 -0
  30. package/data/rules/vscode-extensions.yaml +9 -0
  31. package/lib/audit/index.js +209 -8
  32. package/lib/audit/index.poku.js +332 -0
  33. package/lib/audit/reporters.js +222 -0
  34. package/lib/audit/targets.js +146 -1
  35. package/lib/audit/targets.poku.js +186 -0
  36. package/lib/cli/asar.poku.js +328 -0
  37. package/lib/cli/index.js +647 -127
  38. package/lib/cli/index.poku.js +1905 -187
  39. package/lib/evinser/evinser.js +14 -9
  40. package/lib/helpers/agentFormulationParser.js +6 -2
  41. package/lib/helpers/agentFormulationParser.poku.js +42 -0
  42. package/lib/helpers/analyzer.js +1444 -38
  43. package/lib/helpers/analyzer.poku.js +409 -0
  44. package/lib/helpers/analyzerScope.js +712 -0
  45. package/lib/helpers/asarutils.js +1556 -0
  46. package/lib/helpers/asarutils.poku.js +443 -0
  47. package/lib/helpers/auditCategories.js +12 -0
  48. package/lib/helpers/auditCategories.poku.js +32 -0
  49. package/lib/helpers/cbomutils.js +271 -1
  50. package/lib/helpers/cbomutils.poku.js +248 -5
  51. package/lib/helpers/chromextutils.js +25 -3
  52. package/lib/helpers/chromextutils.poku.js +68 -0
  53. package/lib/helpers/ciParsers/githubActions.js +79 -0
  54. package/lib/helpers/ciParsers/githubActions.poku.js +103 -0
  55. package/lib/helpers/communityAiConfigParser.js +15 -5
  56. package/lib/helpers/communityAiConfigParser.poku.js +71 -0
  57. package/lib/helpers/depsUtils.js +5 -0
  58. package/lib/helpers/depsUtils.poku.js +55 -0
  59. package/lib/helpers/display.js +336 -23
  60. package/lib/helpers/display.poku.js +179 -43
  61. package/lib/helpers/evidenceUtils.js +58 -0
  62. package/lib/helpers/evidenceUtils.poku.js +54 -0
  63. package/lib/helpers/exportUtils.js +9 -0
  64. package/lib/helpers/gtfobins.js +142 -8
  65. package/lib/helpers/gtfobins.poku.js +24 -1
  66. package/lib/helpers/hbom.js +710 -0
  67. package/lib/helpers/hbom.poku.js +496 -0
  68. package/lib/helpers/hbomAnalysis.js +268 -0
  69. package/lib/helpers/hbomAnalysis.poku.js +249 -0
  70. package/lib/helpers/hbomLoader.js +35 -0
  71. package/lib/helpers/hostTopology.js +803 -0
  72. package/lib/helpers/hostTopology.poku.js +363 -0
  73. package/lib/helpers/inventoryStats.js +69 -0
  74. package/lib/helpers/inventoryStats.poku.js +86 -0
  75. package/lib/helpers/lolbas.js +19 -1
  76. package/lib/helpers/lolbas.poku.js +23 -0
  77. package/lib/helpers/mcpConfigParser.js +21 -5
  78. package/lib/helpers/mcpConfigParser.poku.js +39 -2
  79. package/lib/helpers/osqueryTransform.js +47 -0
  80. package/lib/helpers/osqueryTransform.poku.js +47 -0
  81. package/lib/helpers/plugins.js +349 -0
  82. package/lib/helpers/plugins.poku.js +57 -0
  83. package/lib/helpers/propertySanitizer.js +121 -0
  84. package/lib/helpers/protobom.js +156 -45
  85. package/lib/helpers/protobom.poku.js +140 -5
  86. package/lib/helpers/remote/dependency-track.js +36 -3
  87. package/lib/helpers/remote/dependency-track.poku.js +44 -0
  88. package/lib/helpers/source.js +24 -0
  89. package/lib/helpers/source.poku.js +32 -0
  90. package/lib/helpers/utils.js +2454 -198
  91. package/lib/helpers/utils.poku.js +1798 -74
  92. package/lib/managers/binary.e2e.poku.js +367 -0
  93. package/lib/managers/binary.js +2306 -350
  94. package/lib/managers/binary.poku.js +1700 -1
  95. package/lib/managers/docker.js +441 -95
  96. package/lib/managers/docker.poku.js +1479 -14
  97. package/lib/server/server.js +2 -24
  98. package/lib/server/server.poku.js +36 -1
  99. package/lib/stages/postgen/annotator.js +38 -0
  100. package/lib/stages/postgen/annotator.poku.js +107 -1
  101. package/lib/stages/postgen/auditBom.js +121 -18
  102. package/lib/stages/postgen/auditBom.poku.js +2967 -990
  103. package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
  104. package/lib/stages/postgen/postgen.js +192 -1
  105. package/lib/stages/postgen/postgen.poku.js +321 -0
  106. package/lib/stages/postgen/ruleEngine.js +116 -0
  107. package/lib/stages/pregen/envAudit.js +14 -3
  108. package/package.json +24 -21
  109. package/types/bin/hbom.d.ts +3 -0
  110. package/types/bin/hbom.d.ts.map +1 -0
  111. package/types/bin/repl.d.ts.map +1 -1
  112. package/types/lib/audit/index.d.ts +44 -0
  113. package/types/lib/audit/index.d.ts.map +1 -1
  114. package/types/lib/audit/reporters.d.ts +16 -0
  115. package/types/lib/audit/reporters.d.ts.map +1 -1
  116. package/types/lib/audit/targets.d.ts.map +1 -1
  117. package/types/lib/cli/index.d.ts +16 -0
  118. package/types/lib/cli/index.d.ts.map +1 -1
  119. package/types/lib/evinser/evinser.d.ts +4 -0
  120. package/types/lib/evinser/evinser.d.ts.map +1 -1
  121. package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -1
  122. package/types/lib/helpers/analyzer.d.ts +33 -0
  123. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  124. package/types/lib/helpers/analyzerScope.d.ts +11 -0
  125. package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
  126. package/types/lib/helpers/asarutils.d.ts +34 -0
  127. package/types/lib/helpers/asarutils.d.ts.map +1 -0
  128. package/types/lib/helpers/auditCategories.d.ts +5 -0
  129. package/types/lib/helpers/auditCategories.d.ts.map +1 -1
  130. package/types/lib/helpers/cbomutils.d.ts +3 -2
  131. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  132. package/types/lib/helpers/chromextutils.d.ts.map +1 -1
  133. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  134. package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -1
  135. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  136. package/types/lib/helpers/display.d.ts +1 -0
  137. package/types/lib/helpers/display.d.ts.map +1 -1
  138. package/types/lib/helpers/evidenceUtils.d.ts +8 -0
  139. package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
  140. package/types/lib/helpers/exportUtils.d.ts.map +1 -1
  141. package/types/lib/helpers/gtfobins.d.ts +8 -0
  142. package/types/lib/helpers/gtfobins.d.ts.map +1 -1
  143. package/types/lib/helpers/hbom.d.ts +49 -0
  144. package/types/lib/helpers/hbom.d.ts.map +1 -0
  145. package/types/lib/helpers/hbomAnalysis.d.ts +62 -0
  146. package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
  147. package/types/lib/helpers/hbomLoader.d.ts +7 -0
  148. package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
  149. package/types/lib/helpers/hostTopology.d.ts +12 -0
  150. package/types/lib/helpers/hostTopology.d.ts.map +1 -0
  151. package/types/lib/helpers/inventoryStats.d.ts +11 -0
  152. package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
  153. package/types/lib/helpers/lolbas.d.ts.map +1 -1
  154. package/types/lib/helpers/mcpConfigParser.d.ts +1 -1
  155. package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -1
  156. package/types/lib/helpers/osqueryTransform.d.ts +3 -0
  157. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
  158. package/types/lib/helpers/plugins.d.ts +58 -0
  159. package/types/lib/helpers/plugins.d.ts.map +1 -0
  160. package/types/lib/helpers/propertySanitizer.d.ts +3 -0
  161. package/types/lib/helpers/propertySanitizer.d.ts.map +1 -0
  162. package/types/lib/helpers/protobom.d.ts +3 -4
  163. package/types/lib/helpers/protobom.d.ts.map +1 -1
  164. package/types/lib/helpers/remote/dependency-track.d.ts +10 -3
  165. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  166. package/types/lib/helpers/source.d.ts.map +1 -1
  167. package/types/lib/helpers/utils.d.ts +74 -8
  168. package/types/lib/helpers/utils.d.ts.map +1 -1
  169. package/types/lib/managers/binary.d.ts +5 -0
  170. package/types/lib/managers/binary.d.ts.map +1 -1
  171. package/types/lib/managers/docker.d.ts +3 -0
  172. package/types/lib/managers/docker.d.ts.map +1 -1
  173. package/types/lib/server/server.d.ts +2 -0
  174. package/types/lib/server/server.d.ts.map +1 -1
  175. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  176. package/types/lib/stages/postgen/auditBom.d.ts +26 -1
  177. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  178. package/types/lib/stages/postgen/postgen.d.ts +2 -1
  179. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  180. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  181. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  182. package/data/spdx-model-v3.0.1.jsonld +0 -15999
@@ -112,6 +112,34 @@ const CARGO_CACHE_ACTION_PATTERNS = [/^swatinem\/rust-cache(?:@|$)/i];
112
112
 
113
113
  const CARGO_TOOL_INSTALL_ACTION_PATTERNS = [/^taiki-e\/install-action(?:@|$)/i];
114
114
 
115
+ const DEPENDENCY_CACHE_SETUP_ACTIONS = [
116
+ {
117
+ pattern: /^actions\/setup-node(?:@|$)/i,
118
+ ecosystem: "npm",
119
+ inputNames: ["package-manager-cache", "cache"],
120
+ },
121
+ {
122
+ pattern: /^actions\/setup-python(?:@|$)/i,
123
+ ecosystem: "pypi",
124
+ inputNames: ["cache"],
125
+ },
126
+ {
127
+ pattern: /^actions\/setup-go(?:@|$)/i,
128
+ ecosystem: "go",
129
+ inputNames: ["cache"],
130
+ },
131
+ {
132
+ pattern: /^actions\/setup-java(?:@|$)/i,
133
+ ecosystem: "java",
134
+ inputNames: ["cache"],
135
+ },
136
+ {
137
+ pattern: /^moonrepo\/setup-rust(?:@|$)/i,
138
+ ecosystem: "cargo",
139
+ inputNames: ["cache"],
140
+ },
141
+ ];
142
+
115
143
  const FORK_CONTEXT_PATTERNS = [
116
144
  [
117
145
  "github.event.pull_request.head.repo.fork",
@@ -479,6 +507,56 @@ function analyzeCargoActionStep(step) {
479
507
  return props;
480
508
  }
481
509
 
510
+ function isExplicitFalseLikeValue(value) {
511
+ if (value === false) {
512
+ return true;
513
+ }
514
+ if (typeof value !== "string") {
515
+ return false;
516
+ }
517
+ return ["0", "false", "no", "off", "disabled"].includes(
518
+ value.trim().toLowerCase(),
519
+ );
520
+ }
521
+
522
+ function analyzeSetupActionCacheStep(step) {
523
+ const props = [];
524
+ if (!step?.uses || typeof step.uses !== "string") {
525
+ return props;
526
+ }
527
+ const setupAction = DEPENDENCY_CACHE_SETUP_ACTIONS.find((candidate) =>
528
+ candidate.pattern.test(step.uses),
529
+ );
530
+ if (!setupAction || !step.with || typeof step.with !== "object") {
531
+ return props;
532
+ }
533
+ const disableInputName = setupAction.inputNames.find(
534
+ (inputName) =>
535
+ Object.hasOwn(step.with, inputName) &&
536
+ isExplicitFalseLikeValue(step.with[inputName]),
537
+ );
538
+ if (!disableInputName) {
539
+ return props;
540
+ }
541
+ props.push({
542
+ name: "cdx:github:action:disablesBuildCache",
543
+ value: "true",
544
+ });
545
+ props.push({
546
+ name: "cdx:github:action:buildCacheEcosystem",
547
+ value: setupAction.ecosystem,
548
+ });
549
+ props.push({
550
+ name: "cdx:github:action:buildCacheDisableInput",
551
+ value: disableInputName,
552
+ });
553
+ props.push({
554
+ name: "cdx:github:action:buildCacheDisableValue",
555
+ value: String(step.with[disableInputName]),
556
+ });
557
+ return props;
558
+ }
559
+
482
560
  function analyzeCargoRunStep(normalizedRun) {
483
561
  const props = [];
484
562
  if (!normalizedRun || typeof normalizedRun !== "string") {
@@ -2018,6 +2096,7 @@ export function parseWorkflowFile(f, options) {
2018
2096
  actionProperties.push(...analyzeCheckoutStep(step));
2019
2097
  actionProperties.push(...analyzeCacheStep(step));
2020
2098
  actionProperties.push(...analyzeCargoActionStep(step));
2099
+ actionProperties.push(...analyzeSetupActionCacheStep(step));
2021
2100
  actionProperties.push(...analyzeDispatchActionStep(step));
2022
2101
  if (
2023
2102
  step.uses?.includes("actions/github-script") &&
@@ -505,6 +505,109 @@ describe("githubActionsParser", () => {
505
505
  });
506
506
  });
507
507
 
508
+ describe("setup action cache disable property emission", () => {
509
+ it("emits cache disable properties for setup-node, setup-python, and setup-rust", () => {
510
+ const tmpDir = mkdtempSync(path.join(os.tmpdir(), "cdxgen-gha-cache-"));
511
+ const workflowFile = path.join(tmpDir, "cache-disable.yml");
512
+ writeFileSync(
513
+ workflowFile,
514
+ [
515
+ "name: Cache disable",
516
+ "on: push",
517
+ "jobs:",
518
+ " build:",
519
+ " runs-on: ubuntu-latest",
520
+ " steps:",
521
+ " - uses: actions/setup-node@v4",
522
+ " with:",
523
+ " node-version: 20",
524
+ " package-manager-cache: false",
525
+ " - uses: actions/setup-python@v5",
526
+ " with:",
527
+ " python-version: '3.12'",
528
+ " cache: false",
529
+ " - uses: moonrepo/setup-rust@v1",
530
+ " with:",
531
+ " cache: false",
532
+ ].join("\n"),
533
+ );
534
+
535
+ try {
536
+ const result = parseWorkflowFile(workflowFile, { specVersion: 1.7 });
537
+ const setupNodeComp = result.components.find(
538
+ (component) =>
539
+ getProp(component, "cdx:github:action:uses") ===
540
+ "actions/setup-node@v4",
541
+ );
542
+ const setupPythonComp = result.components.find(
543
+ (component) =>
544
+ getProp(component, "cdx:github:action:uses") ===
545
+ "actions/setup-python@v5",
546
+ );
547
+ const setupRustComp = result.components.find(
548
+ (component) =>
549
+ getProp(component, "cdx:github:action:uses") ===
550
+ "moonrepo/setup-rust@v1",
551
+ );
552
+ assert.ok(setupNodeComp, "expected setup-node component");
553
+ assert.ok(setupPythonComp, "expected setup-python component");
554
+ assert.ok(setupRustComp, "expected setup-rust component");
555
+ assert.strictEqual(
556
+ getProp(setupNodeComp, "cdx:github:action:disablesBuildCache"),
557
+ "true",
558
+ );
559
+ assert.strictEqual(
560
+ getProp(setupNodeComp, "cdx:github:action:buildCacheEcosystem"),
561
+ "npm",
562
+ );
563
+ assert.strictEqual(
564
+ getProp(setupNodeComp, "cdx:github:action:buildCacheDisableInput"),
565
+ "package-manager-cache",
566
+ );
567
+ assert.strictEqual(
568
+ getProp(setupPythonComp, "cdx:github:action:disablesBuildCache"),
569
+ "true",
570
+ );
571
+ assert.strictEqual(
572
+ getProp(setupPythonComp, "cdx:github:action:buildCacheEcosystem"),
573
+ "pypi",
574
+ );
575
+ assert.strictEqual(
576
+ getProp(setupPythonComp, "cdx:github:action:buildCacheDisableInput"),
577
+ "cache",
578
+ );
579
+ assert.strictEqual(
580
+ getProp(setupRustComp, "cdx:github:action:disablesBuildCache"),
581
+ "true",
582
+ );
583
+ assert.strictEqual(
584
+ getProp(setupRustComp, "cdx:github:action:buildCacheEcosystem"),
585
+ "cargo",
586
+ );
587
+ assert.strictEqual(
588
+ getProp(setupRustComp, "cdx:github:action:buildCacheDisableInput"),
589
+ "cache",
590
+ );
591
+ } finally {
592
+ rmSync(tmpDir, { force: true, recursive: true });
593
+ }
594
+ });
595
+
596
+ it("does not emit cache disable properties when cache is not explicitly disabled", () => {
597
+ const result = parseWorkflow("simple-build.yml");
598
+ const setupNodeComp = result.components.find(
599
+ (component) =>
600
+ getProp(component, "cdx:github:action:uses") ===
601
+ "actions/setup-node@v4",
602
+ );
603
+ assert.ok(setupNodeComp, "expected setup-node component");
604
+ assert.strictEqual(
605
+ getProp(setupNodeComp, "cdx:github:action:disablesBuildCache"),
606
+ undefined,
607
+ );
608
+ });
609
+ });
610
+
508
611
  describe("script injection interpolation detection", () => {
509
612
  it("detects github.event.pull_request interpolation", () => {
510
613
  const result = parseWorkflow("injection-pull-request-title.yml");
@@ -8,6 +8,7 @@ import {
8
8
  credentialIndicatorsForText,
9
9
  sanitizeMcpRefToken,
10
10
  } from "./mcpDiscovery.js";
11
+ import { sanitizeBomPropertyValue } from "./propertySanitizer.js";
11
12
  import { scanTextForHiddenUnicode } from "./unicodeScan.js";
12
13
 
13
14
  const COMMUNITY_AI_PATTERNS = [
@@ -46,13 +47,22 @@ const COMMUNITY_AI_PATTERNS = [
46
47
  ];
47
48
 
48
49
  function addUniqueProperty(properties, name, value) {
49
- if (value === undefined || value === null || value === "") {
50
+ const sanitizedValue = sanitizeBomPropertyValue(name, value);
51
+ if (
52
+ sanitizedValue === undefined ||
53
+ sanitizedValue === null ||
54
+ sanitizedValue === ""
55
+ ) {
50
56
  return;
51
57
  }
52
- if (properties.some((prop) => prop.name === name && prop.value === value)) {
58
+ if (
59
+ properties.some(
60
+ (prop) => prop.name === name && prop.value === String(sanitizedValue),
61
+ )
62
+ ) {
53
63
  return;
54
64
  }
55
- properties.push({ name, value: String(value) });
65
+ properties.push({ name, value: String(sanitizedValue) });
56
66
  }
57
67
 
58
68
  function normalizeFilePath(filePath) {
@@ -217,7 +227,7 @@ function parseSkillFile(filePath, raw) {
217
227
  addUniqueProperty(
218
228
  component.properties,
219
229
  "cdx:skill:metadata",
220
- JSON.stringify(metadata.metadata),
230
+ metadata.metadata,
221
231
  );
222
232
  }
223
233
  maybeAddFileSignals(component.properties, filePath, raw);
@@ -399,7 +409,7 @@ function parseOpencodeConfig(filePath, raw) {
399
409
  addUniqueProperty(
400
410
  component.properties,
401
411
  "cdx:agent:permission",
402
- JSON.stringify(agentConfig.permission),
412
+ agentConfig.permission,
403
413
  );
404
414
  }
405
415
  components.push(component);
@@ -60,4 +60,75 @@ describe("communityAiConfigParser", () => {
60
60
  ),
61
61
  );
62
62
  });
63
+
64
+ it("sanitizes secret-bearing AI inventory properties before emission", async () => {
65
+ const readFileSync = sinon.stub();
66
+ readFileSync.withArgs("/repo/opencode.json", "utf-8").returns(
67
+ JSON.stringify({
68
+ agent: {
69
+ release: {
70
+ description:
71
+ "Deploy with https://user:pass@example.com/release?access_token=abc#frag and sk_test_super_secret_value",
72
+ permission: {
73
+ endpoints: [
74
+ "https://user:pass@example.com/private?token=abc#frag",
75
+ ],
76
+ __proto__: {
77
+ polluted: true,
78
+ },
79
+ },
80
+ },
81
+ },
82
+ }),
83
+ );
84
+ readFileSync
85
+ .withArgs("/repo/.claude/skills/release/SKILL.md", "utf-8")
86
+ .returns(
87
+ [
88
+ "---",
89
+ "name: release",
90
+ "description: Publish release notes",
91
+ "metadata:",
92
+ " endpoint: https://user:pass@example.com/skill?token=abc#frag",
93
+ " apiKey: sk_test_skill_secret_value",
94
+ "---",
95
+ "Use the release workflow.",
96
+ ].join("\n"),
97
+ );
98
+ const { communityAiConfigParser } = await esmock(
99
+ "./communityAiConfigParser.js",
100
+ {
101
+ "node:fs": { readFileSync },
102
+ },
103
+ );
104
+
105
+ const result = communityAiConfigParser.parse([
106
+ "/repo/opencode.json",
107
+ "/repo/.claude/skills/release/SKILL.md",
108
+ ]);
109
+ const agent = result.components.find(
110
+ (component) => getProp(component, "cdx:file:kind") === "agent-config",
111
+ );
112
+ const skill = result.components.find(
113
+ (component) => getProp(component, "cdx:file:kind") === "skill-file",
114
+ );
115
+
116
+ assert.strictEqual(
117
+ getProp(agent, "cdx:agent:description"),
118
+ "Deploy with https://example.com/release and [redacted]",
119
+ );
120
+ assert.strictEqual(
121
+ getProp(agent, "cdx:agent:permission"),
122
+ JSON.stringify({
123
+ endpoints: ["https://example.com/private"],
124
+ }),
125
+ );
126
+ assert.strictEqual(
127
+ getProp(skill, "cdx:skill:metadata"),
128
+ JSON.stringify({
129
+ endpoint: "https://example.com/skill",
130
+ apiKey: "[redacted]",
131
+ }),
132
+ );
133
+ });
63
134
  });
@@ -272,6 +272,11 @@ export function trimComponents(components) {
272
272
  if (!existIdent.methods) {
273
273
  existIdent.methods = [];
274
274
  }
275
+ if (aident.tools?.length) {
276
+ existIdent.tools = Array.from(
277
+ new Set([...(existIdent.tools || []), ...aident.tools]),
278
+ );
279
+ }
275
280
  let isDup = false;
276
281
  for (const emethod of existIdent.methods) {
277
282
  if (emethod?.value === amethod?.value) {
@@ -208,6 +208,61 @@ describe("trimComponents()", () => {
208
208
  { alg: "SHA-256", content: "def456" },
209
209
  ]);
210
210
  });
211
+
212
+ it("retains identity tool references when merging duplicate components", () => {
213
+ const components = [
214
+ {
215
+ name: "openssl",
216
+ version: "3.0.0",
217
+ purl: "pkg:rpm/redhat/openssl@3.0.0",
218
+ type: "library",
219
+ evidence: {
220
+ identity: [
221
+ {
222
+ field: "purl",
223
+ confidence: 1,
224
+ methods: [
225
+ {
226
+ technique: "binary-analysis",
227
+ confidence: 1,
228
+ value: "openssl",
229
+ },
230
+ ],
231
+ tools: ["pkg:generic/trivy@0.1.0"],
232
+ },
233
+ ],
234
+ },
235
+ },
236
+ {
237
+ name: "openssl",
238
+ version: "3.0.0",
239
+ purl: "pkg:rpm/redhat/openssl@3.0.0",
240
+ type: "library",
241
+ evidence: {
242
+ identity: [
243
+ {
244
+ field: "purl",
245
+ confidence: 1,
246
+ methods: [
247
+ {
248
+ technique: "binary-analysis",
249
+ confidence: 1,
250
+ value: "openssl",
251
+ },
252
+ ],
253
+ tools: ["pkg:generic/blint@1.2.3"],
254
+ },
255
+ ],
256
+ },
257
+ },
258
+ ];
259
+ const result = trimComponents(components);
260
+ assert.strictEqual(result.length, 1);
261
+ assert.deepStrictEqual(result[0].evidence.identity[0].tools, [
262
+ "pkg:generic/trivy@0.1.0",
263
+ "pkg:generic/blint@1.2.3",
264
+ ]);
265
+ });
211
266
  });
212
267
 
213
268
  describe("mergeServices()", () => {