@cyclonedx/cdxgen 12.3.3 → 12.4.1

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 (175) hide show
  1. package/README.md +69 -25
  2. package/bin/audit.js +21 -7
  3. package/bin/cdxgen.js +270 -127
  4. package/bin/convert.js +34 -15
  5. package/bin/hbom.js +495 -0
  6. package/bin/repl.js +592 -37
  7. package/bin/validate.js +31 -4
  8. package/bin/verify.js +18 -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/cyclonedx-2.0-bundled.schema.json +7182 -0
  13. package/data/predictive-audit-allowlist.json +11 -0
  14. package/data/queries-darwin.json +12 -1
  15. package/data/queries-win.json +7 -1
  16. package/data/queries.json +39 -2
  17. package/data/rules/ai-agent-governance.yaml +16 -0
  18. package/data/rules/asar-archives.yaml +150 -0
  19. package/data/rules/chrome-extensions.yaml +8 -0
  20. package/data/rules/ci-permissions.yaml +42 -18
  21. package/data/rules/container-risk.yaml +14 -7
  22. package/data/rules/dependency-sources.yaml +11 -0
  23. package/data/rules/hbom-compliance.yaml +325 -0
  24. package/data/rules/hbom-performance.yaml +307 -0
  25. package/data/rules/hbom-security.yaml +248 -0
  26. package/data/rules/host-topology.yaml +165 -0
  27. package/data/rules/mcp-servers.yaml +18 -3
  28. package/data/rules/obom-runtime.yaml +907 -22
  29. package/data/rules/package-integrity.yaml +14 -0
  30. package/data/rules/rootfs-hardening.yaml +179 -0
  31. package/data/rules/vscode-extensions.yaml +9 -0
  32. package/lib/audit/index.js +210 -8
  33. package/lib/audit/index.poku.js +332 -0
  34. package/lib/audit/reporters.js +222 -0
  35. package/lib/audit/targets.js +146 -1
  36. package/lib/audit/targets.poku.js +186 -0
  37. package/lib/cli/asar.poku.js +328 -0
  38. package/lib/cli/index.js +527 -99
  39. package/lib/cli/index.poku.js +1469 -212
  40. package/lib/evinser/evinser.js +14 -9
  41. package/lib/helpers/analyzer.js +1406 -29
  42. package/lib/helpers/analyzer.poku.js +342 -0
  43. package/lib/helpers/analyzerScope.js +712 -0
  44. package/lib/helpers/asarutils.js +1556 -0
  45. package/lib/helpers/asarutils.poku.js +443 -0
  46. package/lib/helpers/auditCategories.js +12 -0
  47. package/lib/helpers/auditCategories.poku.js +32 -0
  48. package/lib/helpers/bomUtils.js +155 -1
  49. package/lib/helpers/bomUtils.poku.js +79 -1
  50. package/lib/helpers/cbomutils.js +271 -1
  51. package/lib/helpers/cbomutils.poku.js +248 -5
  52. package/lib/helpers/display.js +291 -1
  53. package/lib/helpers/display.poku.js +149 -0
  54. package/lib/helpers/evidenceUtils.js +58 -0
  55. package/lib/helpers/evidenceUtils.poku.js +54 -0
  56. package/lib/helpers/exportUtils.js +9 -0
  57. package/lib/helpers/gtfobins.js +142 -8
  58. package/lib/helpers/gtfobins.poku.js +24 -1
  59. package/lib/helpers/hbom.js +710 -0
  60. package/lib/helpers/hbom.poku.js +496 -0
  61. package/lib/helpers/hbomAnalysis.js +268 -0
  62. package/lib/helpers/hbomAnalysis.poku.js +249 -0
  63. package/lib/helpers/hbomLoader.js +35 -0
  64. package/lib/helpers/hostTopology.js +803 -0
  65. package/lib/helpers/hostTopology.poku.js +363 -0
  66. package/lib/helpers/inventoryStats.js +69 -0
  67. package/lib/helpers/inventoryStats.poku.js +86 -0
  68. package/lib/helpers/lolbas.js +19 -1
  69. package/lib/helpers/lolbas.poku.js +23 -0
  70. package/lib/helpers/osqueryTransform.js +47 -0
  71. package/lib/helpers/osqueryTransform.poku.js +47 -0
  72. package/lib/helpers/plugins.js +350 -0
  73. package/lib/helpers/plugins.poku.js +57 -0
  74. package/lib/helpers/protobom.js +209 -45
  75. package/lib/helpers/protobom.poku.js +183 -5
  76. package/lib/helpers/protobomLoader.js +43 -0
  77. package/lib/helpers/protobomLoader.poku.js +31 -0
  78. package/lib/helpers/remote/dependency-track.js +36 -3
  79. package/lib/helpers/remote/dependency-track.poku.js +44 -0
  80. package/lib/helpers/source.js +24 -0
  81. package/lib/helpers/source.poku.js +32 -0
  82. package/lib/helpers/utils.js +1438 -93
  83. package/lib/helpers/utils.poku.js +846 -4
  84. package/lib/managers/binary.e2e.poku.js +367 -0
  85. package/lib/managers/binary.js +2293 -353
  86. package/lib/managers/binary.poku.js +1699 -1
  87. package/lib/managers/docker.js +201 -79
  88. package/lib/managers/docker.poku.js +337 -12
  89. package/lib/server/server.js +4 -28
  90. package/lib/stages/postgen/annotator.js +38 -0
  91. package/lib/stages/postgen/annotator.poku.js +107 -1
  92. package/lib/stages/postgen/auditBom.js +121 -18
  93. package/lib/stages/postgen/auditBom.poku.js +1366 -31
  94. package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
  95. package/lib/stages/postgen/postgen.js +406 -8
  96. package/lib/stages/postgen/postgen.poku.js +484 -0
  97. package/lib/stages/postgen/ruleEngine.js +116 -0
  98. package/lib/stages/pregen/envAudit.js +14 -3
  99. package/lib/validator/bomValidator.js +90 -38
  100. package/lib/validator/bomValidator.poku.js +90 -0
  101. package/lib/validator/complianceRules.js +4 -2
  102. package/lib/validator/index.poku.js +14 -0
  103. package/package.json +23 -21
  104. package/types/bin/hbom.d.ts +3 -0
  105. package/types/bin/hbom.d.ts.map +1 -0
  106. package/types/bin/repl.d.ts +1 -1
  107. package/types/bin/repl.d.ts.map +1 -1
  108. package/types/lib/audit/index.d.ts +44 -0
  109. package/types/lib/audit/index.d.ts.map +1 -1
  110. package/types/lib/audit/reporters.d.ts +16 -0
  111. package/types/lib/audit/reporters.d.ts.map +1 -1
  112. package/types/lib/audit/targets.d.ts.map +1 -1
  113. package/types/lib/cli/index.d.ts +16 -0
  114. package/types/lib/cli/index.d.ts.map +1 -1
  115. package/types/lib/evinser/evinser.d.ts +4 -0
  116. package/types/lib/evinser/evinser.d.ts.map +1 -1
  117. package/types/lib/helpers/analyzer.d.ts +33 -0
  118. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  119. package/types/lib/helpers/analyzerScope.d.ts +11 -0
  120. package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
  121. package/types/lib/helpers/asarutils.d.ts +34 -0
  122. package/types/lib/helpers/asarutils.d.ts.map +1 -0
  123. package/types/lib/helpers/auditCategories.d.ts +5 -0
  124. package/types/lib/helpers/auditCategories.d.ts.map +1 -1
  125. package/types/lib/helpers/bomUtils.d.ts +10 -0
  126. package/types/lib/helpers/bomUtils.d.ts.map +1 -1
  127. package/types/lib/helpers/cbomutils.d.ts +3 -2
  128. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  129. package/types/lib/helpers/display.d.ts.map +1 -1
  130. package/types/lib/helpers/evidenceUtils.d.ts +8 -0
  131. package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
  132. package/types/lib/helpers/exportUtils.d.ts.map +1 -1
  133. package/types/lib/helpers/gtfobins.d.ts +8 -0
  134. package/types/lib/helpers/gtfobins.d.ts.map +1 -1
  135. package/types/lib/helpers/hbom.d.ts +49 -0
  136. package/types/lib/helpers/hbom.d.ts.map +1 -0
  137. package/types/lib/helpers/hbomAnalysis.d.ts +76 -0
  138. package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
  139. package/types/lib/helpers/hbomLoader.d.ts +7 -0
  140. package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
  141. package/types/lib/helpers/hostTopology.d.ts +12 -0
  142. package/types/lib/helpers/hostTopology.d.ts.map +1 -0
  143. package/types/lib/helpers/inventoryStats.d.ts +11 -0
  144. package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
  145. package/types/lib/helpers/lolbas.d.ts.map +1 -1
  146. package/types/lib/helpers/osqueryTransform.d.ts +3 -0
  147. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
  148. package/types/lib/helpers/plugins.d.ts +58 -0
  149. package/types/lib/helpers/plugins.d.ts.map +1 -0
  150. package/types/lib/helpers/protobom.d.ts +5 -4
  151. package/types/lib/helpers/protobom.d.ts.map +1 -1
  152. package/types/lib/helpers/protobomLoader.d.ts +17 -0
  153. package/types/lib/helpers/protobomLoader.d.ts.map +1 -0
  154. package/types/lib/helpers/remote/dependency-track.d.ts +10 -3
  155. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  156. package/types/lib/helpers/source.d.ts.map +1 -1
  157. package/types/lib/helpers/utils.d.ts +45 -8
  158. package/types/lib/helpers/utils.d.ts.map +1 -1
  159. package/types/lib/managers/binary.d.ts +5 -0
  160. package/types/lib/managers/binary.d.ts.map +1 -1
  161. package/types/lib/managers/docker.d.ts.map +1 -1
  162. package/types/lib/server/server.d.ts +2 -1
  163. package/types/lib/server/server.d.ts.map +1 -1
  164. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  165. package/types/lib/stages/postgen/auditBom.d.ts +26 -1
  166. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  167. package/types/lib/stages/postgen/postgen.d.ts +2 -1
  168. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  169. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  170. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  171. package/types/lib/third-party/arborist/lib/node.d.ts +23 -0
  172. package/types/lib/third-party/arborist/lib/node.d.ts.map +1 -1
  173. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  174. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  175. package/data/spdx-model-v3.0.1.jsonld +0 -15999
package/bin/convert.js CHANGED
@@ -10,12 +10,18 @@ import { hideBin } from "yargs/helpers";
10
10
  import {
11
11
  getNonCycloneDxErrorMessage,
12
12
  isCycloneDxBom,
13
+ toCycloneDxSpecVersionString,
13
14
  } from "../lib/helpers/bomUtils.js";
14
15
  import { deriveSpdxOutputPath } from "../lib/helpers/exportUtils.js";
16
+ import {
17
+ importProtobomModule,
18
+ isProtoBomPath,
19
+ } from "../lib/helpers/protobomLoader.js";
15
20
  import {
16
21
  retrieveCdxgenVersion,
17
22
  safeExistsSync,
18
23
  safeMkdirSync,
24
+ safeWriteSync,
19
25
  } from "../lib/helpers/utils.js";
20
26
  import { convertCycloneDxToSpdx } from "../lib/stages/postgen/spdxConverter.js";
21
27
  import { validateSpdx } from "../lib/validator/bomValidator.js";
@@ -26,7 +32,7 @@ const args = _yargs
26
32
  .option("input", {
27
33
  alias: "i",
28
34
  default: "bom.json",
29
- description: "Input CycloneDX BOM JSON file.",
35
+ description: "Input CycloneDX BOM JSON or protobuf file.",
30
36
  })
31
37
  .option("output", {
32
38
  alias: "o",
@@ -50,25 +56,38 @@ const args = _yargs
50
56
  .help()
51
57
  .wrap(Math.min(120, yargs().terminalWidth())).argv;
52
58
 
53
- if (!safeExistsSync(args.input)) {
54
- console.error(`Input file '${args.input}' not found.`);
55
- process.exit(1);
56
- }
59
+ const loadCycloneDxBom = async (inputPath) => {
60
+ if (!safeExistsSync(inputPath)) {
61
+ console.error(`Input file '${inputPath}' not found.`);
62
+ process.exit(1);
63
+ }
64
+ const isProtoInput = isProtoBomPath(inputPath);
65
+ try {
66
+ if (isProtoInput) {
67
+ const { readBinary } = await importProtobomModule(
68
+ "cdx-convert",
69
+ "protobuf BOM input",
70
+ );
71
+ return readBinary(inputPath, true);
72
+ }
73
+ return JSON.parse(fs.readFileSync(inputPath, "utf8"));
74
+ } catch (error) {
75
+ const inputType = isProtoInput ? "protobuf" : "JSON";
76
+ console.error(
77
+ `Failed to parse '${inputPath}' as CycloneDX ${inputType}: ${error.message}`,
78
+ );
79
+ process.exit(1);
80
+ }
81
+ };
57
82
 
58
- let bomJson;
59
- try {
60
- bomJson = JSON.parse(fs.readFileSync(args.input, "utf8"));
61
- } catch (error) {
62
- console.error(`Failed to parse '${args.input}' as JSON: ${error.message}`);
63
- process.exit(1);
64
- }
83
+ const bomJson = await loadCycloneDxBom(args.input);
65
84
 
66
85
  if (!isCycloneDxBom(bomJson)) {
67
86
  console.error(getNonCycloneDxErrorMessage(bomJson, "cdx-convert"));
68
87
  process.exit(1);
69
88
  }
70
- const cdxSpecVersion = Number.parseFloat(`${bomJson?.specVersion || ""}`);
71
- if (![1.6, 1.7].includes(cdxSpecVersion)) {
89
+ const cdxSpecVersion = toCycloneDxSpecVersionString(bomJson?.specVersion);
90
+ if (!["1.6", "1.7"].includes(cdxSpecVersion)) {
72
91
  console.error(
73
92
  `Unsupported CycloneDX specVersion '${bomJson?.specVersion}'. cdx-convert currently supports CycloneDX 1.6 or 1.7 input and exports SPDX 3.0.1.`,
74
93
  );
@@ -92,7 +111,7 @@ if (outputParent && outputParent !== "." && !safeExistsSync(outputParent)) {
92
111
  safeMkdirSync(outputParent, { recursive: true });
93
112
  }
94
113
 
95
- fs.writeFileSync(
114
+ safeWriteSync(
96
115
  outputPath,
97
116
  JSON.stringify(spdxJson, null, args.jsonPretty ? 2 : null),
98
117
  );
package/bin/hbom.js ADDED
@@ -0,0 +1,495 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { basename, resolve } from "node:path";
4
+ import process from "node:process";
5
+
6
+ import yargs from "yargs";
7
+ import { hideBin } from "yargs/helpers";
8
+
9
+ import { createHBom } from "../lib/cli/index.js";
10
+ import { printActivitySummary } from "../lib/helpers/display.js";
11
+ import { getOutputDirectory } from "../lib/helpers/exportUtils.js";
12
+ import {
13
+ ensureNoMixedHbomProjectTypes,
14
+ ensureSupportedHbomSpecVersion,
15
+ hasHbomProjectType,
16
+ } from "../lib/helpers/hbom.js";
17
+ import { getHbomSummary } from "../lib/helpers/hbomAnalysis.js";
18
+ import { thoughtLog } from "../lib/helpers/logger.js";
19
+ import {
20
+ importProtobomModule,
21
+ isProtoBomPath,
22
+ } from "../lib/helpers/protobomLoader.js";
23
+ import {
24
+ DEBUG_MODE,
25
+ isDryRun,
26
+ retrieveCdxgenVersion,
27
+ safeExistsSync,
28
+ safeMkdirSync,
29
+ safeWriteSync,
30
+ setActivityContext,
31
+ setDryRunMode,
32
+ } from "../lib/helpers/utils.js";
33
+ import { validateBom } from "../lib/validator/bomValidator.js";
34
+
35
+ function determineHbomCommandName() {
36
+ const invokedScriptName = basename(process.argv[1] || "hbom").replace(
37
+ /\.(?:[cm]?js|exe)$/u,
38
+ "",
39
+ );
40
+ return invokedScriptName || "hbom";
41
+ }
42
+
43
+ const hbomCommandName = determineHbomCommandName();
44
+
45
+ const _yargs = yargs(hideBin(process.argv));
46
+
47
+ const args = _yargs
48
+ .parserConfiguration({
49
+ "boolean-negation": true,
50
+ "greedy-arrays": false,
51
+ "parse-numbers": true,
52
+ "short-option-groups": false,
53
+ })
54
+ .usage("$0 [command] [options]")
55
+ .command(
56
+ "diagnostics",
57
+ "Identify HBOM collector missing-command and permission-denied issues from a live run or an existing HBOM JSON file.",
58
+ )
59
+ .option("output", {
60
+ alias: "o",
61
+ default: "hbom.json",
62
+ description: "Output file. Default hbom.json",
63
+ type: "string",
64
+ })
65
+ .option("print", {
66
+ alias: "p",
67
+ description:
68
+ "Print the generated HBOM to stdout instead of writing a file.",
69
+ type: "boolean",
70
+ default: false,
71
+ })
72
+ .option("pretty", {
73
+ description: "Pretty-print the generated HBOM JSON.",
74
+ type: "boolean",
75
+ default: true,
76
+ })
77
+ .option("validate", {
78
+ description: "Validate the generated HBOM using the CycloneDX schema.",
79
+ type: "boolean",
80
+ default: true,
81
+ })
82
+ .option("export-proto", {
83
+ description:
84
+ "Serialize and export the generated HBOM as a protobuf binary.",
85
+ type: "boolean",
86
+ default: false,
87
+ })
88
+ .option("proto-bin-file", {
89
+ description: "Path for the serialized protobuf HBOM binary.",
90
+ type: "string",
91
+ default: "hbom.cdx",
92
+ })
93
+ .option("dry-run", {
94
+ description:
95
+ "Read-only mode. Report the requested HBOM collection and block host probing plus filesystem writes.",
96
+ type: "boolean",
97
+ default: isDryRun,
98
+ })
99
+ .option("spec-version", {
100
+ choices: [1.7],
101
+ default: 1.7,
102
+ description:
103
+ "CycloneDX specification version to use. HBOM currently supports 1.7 only.",
104
+ type: "number",
105
+ })
106
+ .option("platform", {
107
+ description: "Override platform selection.",
108
+ type: "string",
109
+ })
110
+ .option("arch", {
111
+ description: "Override architecture selection.",
112
+ type: "string",
113
+ })
114
+ .option("sensitive", {
115
+ description: "Include raw identifiers instead of redacted defaults.",
116
+ type: "boolean",
117
+ default: false,
118
+ })
119
+ .option("no-command-enrichment", {
120
+ description: "Disable optional command-based enrichment.",
121
+ type: "boolean",
122
+ default: false,
123
+ })
124
+ .option("include-runtime", {
125
+ description:
126
+ "Collect OBOM runtime inventory alongside the HBOM and emit a merged host view with strict topology links.",
127
+ type: "boolean",
128
+ default: false,
129
+ })
130
+ .option("privileged", {
131
+ description:
132
+ "Enable privileged Linux enrichment and non-interactive sudo retries for documented permission-sensitive commands.",
133
+ type: "boolean",
134
+ default: false,
135
+ })
136
+ .option("plist-enrichment", {
137
+ description: "Enable additional Darwin plist-based enrichment.",
138
+ type: "boolean",
139
+ default: false,
140
+ })
141
+ .option("strict", {
142
+ description:
143
+ "Fail instead of returning partial results when enrichment fails.",
144
+ type: "boolean",
145
+ default: false,
146
+ })
147
+ .option("timeout", {
148
+ description:
149
+ "Per-command timeout in milliseconds. Increase this on slower hosts such as Raspberry Pi systems.",
150
+ type: "number",
151
+ })
152
+ .option("type", {
153
+ description:
154
+ "Compatibility project type flag. Only 'hbom' or 'hardware' are accepted.",
155
+ hidden: true,
156
+ })
157
+ .option("input", {
158
+ alias: "i",
159
+ description:
160
+ "Read an existing HBOM JSON or protobuf file instead of collecting a fresh live inventory. Primarily useful with the diagnostics command.",
161
+ type: "string",
162
+ })
163
+ .option("json", {
164
+ description:
165
+ "Print the diagnostics summary as JSON instead of human-readable text. Only applies to the diagnostics command.",
166
+ type: "boolean",
167
+ default: false,
168
+ })
169
+ .array("type")
170
+ .example([
171
+ ["$0", "Generate an HBOM file for the current host"],
172
+ ["$0 -p", "Print the generated HBOM to stdout"],
173
+ ["$0 --platform linux --arch amd64", "Override target selection"],
174
+ ["$0 --privileged --pretty", "Enable privileged Linux enrichment"],
175
+ [
176
+ "$0 diagnostics",
177
+ "Run a live HBOM diagnostic pass and summarize missing commands or permission-sensitive enrichments",
178
+ ],
179
+ [
180
+ "$0 diagnostics --input hbom.json",
181
+ "Summarize missing commands or permission-denied enrichments from an existing HBOM file",
182
+ ],
183
+ ])
184
+ .scriptName(hbomCommandName)
185
+ .version(retrieveCdxgenVersion())
186
+ .alias("v", "version")
187
+ .help(false)
188
+ .option("help", {
189
+ alias: "h",
190
+ description: "Show help",
191
+ type: "boolean",
192
+ })
193
+ .wrap(Math.min(120, yargs().terminalWidth())).argv;
194
+
195
+ if (args.help) {
196
+ console.log(`${retrieveCdxgenVersion()}\n`);
197
+ _yargs.showHelp();
198
+ process.exit(0);
199
+ }
200
+
201
+ const requestedTypes = args.type?.length ? args.type : ["hbom"];
202
+ const selectedCommand = `${args._?.[0] ?? "generate"}`;
203
+
204
+ try {
205
+ ensureNoMixedHbomProjectTypes(requestedTypes);
206
+ ensureSupportedHbomSpecVersion(args.specVersion);
207
+ } catch (error) {
208
+ console.error(error.message);
209
+ process.exit(1);
210
+ }
211
+ if (!hasHbomProjectType(requestedTypes)) {
212
+ console.error(
213
+ `The '${hbomCommandName}' command only supports the 'hbom' or 'hardware' project type.`,
214
+ );
215
+ process.exit(1);
216
+ }
217
+
218
+ const options = {
219
+ arch: args.arch,
220
+ command: selectedCommand,
221
+ commandName: hbomCommandName,
222
+ dryRun: args.dryRun,
223
+ input: args.input ? resolve(args.input) : undefined,
224
+ noCommandEnrichment: args.noCommandEnrichment,
225
+ includeRuntime: args.includeRuntime,
226
+ json: args.json,
227
+ output: resolve(args.output),
228
+ platform: args.platform,
229
+ plistEnrichment: args.plistEnrichment,
230
+ pretty: args.pretty,
231
+ print: args.print,
232
+ privileged: args.privileged,
233
+ exportProto: args.exportProto,
234
+ protoBinFile: resolve(args.protoBinFile),
235
+ projectType: [requestedTypes[0]],
236
+ sensitive: args.sensitive,
237
+ specVersion: args.specVersion,
238
+ strict: args.strict,
239
+ timeout: args.timeout,
240
+ validate: args.validate,
241
+ };
242
+
243
+ setDryRunMode(options.dryRun);
244
+ setActivityContext({
245
+ projectType: requestedTypes[0],
246
+ sourcePath: process.cwd(),
247
+ });
248
+
249
+ if (options.dryRun) {
250
+ thoughtLog(
251
+ "HBOM dry-run mode is enabled. I must keep collection read-only, block command enrichment, and avoid filesystem writes.",
252
+ );
253
+ }
254
+
255
+ function groupDiagnosticsByIssue(issue, commandDiagnostics = []) {
256
+ const groupedDiagnostics = new Map();
257
+
258
+ for (const entry of commandDiagnostics) {
259
+ if (entry?.issue !== issue) {
260
+ continue;
261
+ }
262
+ const commandName = `${entry.command ?? entry.id ?? "command"}`;
263
+ const hint = `${entry.installHint ?? entry.privilegeHint ?? ""}`.trim();
264
+ const message = `${entry.message ?? ""}`.trim();
265
+ const groupingKey = [commandName, issue, hint, message].join("\u0000");
266
+ const currentEntry = groupedDiagnostics.get(groupingKey) ?? {
267
+ command: commandName,
268
+ count: 0,
269
+ hint: hint || undefined,
270
+ message: message || undefined,
271
+ };
272
+ currentEntry.count += 1;
273
+ groupedDiagnostics.set(groupingKey, currentEntry);
274
+ }
275
+
276
+ return [...groupedDiagnostics.values()].sort((firstEntry, secondEntry) =>
277
+ firstEntry.command.localeCompare(secondEntry.command),
278
+ );
279
+ }
280
+
281
+ function buildFormattedDiagnosticLines(groupedEntries = []) {
282
+ return groupedEntries.flatMap((entry) => {
283
+ const headline = `- ${entry.command}${entry.count > 1 ? ` (${entry.count} invocations)` : ""}`;
284
+ const detailParts = [];
285
+ if (entry.message) {
286
+ detailParts.push(entry.message);
287
+ }
288
+ if (entry.hint) {
289
+ detailParts.push(`Hint: ${entry.hint}`);
290
+ }
291
+ if (!detailParts.length) {
292
+ return [headline];
293
+ }
294
+ return [headline, ...detailParts.map((value) => ` ${value}`)];
295
+ });
296
+ }
297
+
298
+ function printHbomDiagnosticNotice(bomJson) {
299
+ const hbomSummary = getHbomSummary(bomJson);
300
+ if (!hbomSummary.actionableDiagnosticCount) {
301
+ return;
302
+ }
303
+ const detailParts = [];
304
+ if (hbomSummary.missingCommandCount) {
305
+ detailParts.push(`${hbomSummary.missingCommandCount} missing command`);
306
+ }
307
+ if (hbomSummary.permissionDeniedCount) {
308
+ detailParts.push(
309
+ `${hbomSummary.permissionDeniedCount} permission-denied enrichment`,
310
+ );
311
+ }
312
+ const followUpCommand = options.print
313
+ ? `${hbomCommandName} diagnostics`
314
+ : `${hbomCommandName} diagnostics --input ${options.output}`;
315
+ console.error(
316
+ `HBOM collector reported ${hbomSummary.actionableDiagnosticCount} actionable diagnostic(s) (${detailParts.join(", ")}). Run '${followUpCommand}' for detailed install and privilege guidance.`,
317
+ );
318
+ }
319
+
320
+ async function loadBomFromInputFile(inputFile) {
321
+ if (!inputFile || !safeExistsSync(inputFile)) {
322
+ throw new Error(`HBOM input file not found: ${inputFile}`);
323
+ }
324
+ if (isProtoBomPath(inputFile)) {
325
+ const { readBinary } = await importProtobomModule(
326
+ hbomCommandName,
327
+ "protobuf BOM input",
328
+ );
329
+ return readBinary(inputFile, true);
330
+ }
331
+ return JSON.parse(readFileSync(inputFile, { encoding: "utf8" }));
332
+ }
333
+
334
+ function printHbomDiagnosticsReport(bomJson) {
335
+ const hbomSummary = getHbomSummary(bomJson);
336
+ if (options.json) {
337
+ console.log(
338
+ JSON.stringify(
339
+ {
340
+ actionableDiagnosticCount: hbomSummary.actionableDiagnosticCount,
341
+ architecture: hbomSummary.architecture,
342
+ collectorProfile: hbomSummary.collectorProfile,
343
+ commandDiagnosticCount: hbomSummary.commandDiagnosticCount,
344
+ commandDiagnostics: hbomSummary.commandDiagnostics,
345
+ commandErrorCount: hbomSummary.commandErrorCount,
346
+ diagnosticIssues: hbomSummary.diagnosticIssues,
347
+ installHints: hbomSummary.installHints,
348
+ metadataName: hbomSummary.metadataName,
349
+ missingCommandCount: hbomSummary.missingCommandCount,
350
+ missingCommands: hbomSummary.missingCommands,
351
+ partialSupportCount: hbomSummary.partialSupportCount,
352
+ permissionDeniedCommands: hbomSummary.permissionDeniedCommands,
353
+ permissionDeniedCount: hbomSummary.permissionDeniedCount,
354
+ platform: hbomSummary.platform,
355
+ privilegeHints: hbomSummary.privilegeHints,
356
+ requiresPrivilegedEnrichment:
357
+ hbomSummary.requiresPrivilegedEnrichment,
358
+ timeoutCount: hbomSummary.timeoutCount,
359
+ },
360
+ null,
361
+ 2,
362
+ ),
363
+ );
364
+ return;
365
+ }
366
+
367
+ const missingCommands = groupDiagnosticsByIssue(
368
+ "missing-command",
369
+ hbomSummary.commandDiagnostics,
370
+ );
371
+ const permissionDeniedCommands = groupDiagnosticsByIssue(
372
+ "permission-denied",
373
+ hbomSummary.commandDiagnostics,
374
+ );
375
+
376
+ console.log("HBOM diagnostics summary");
377
+ console.log(
378
+ `Target: ${hbomSummary.platform ?? "unknown"}/${hbomSummary.architecture ?? "unknown"}`,
379
+ );
380
+ if (hbomSummary.collectorProfile) {
381
+ console.log(`Collector profile: ${hbomSummary.collectorProfile}`);
382
+ }
383
+ if (hbomSummary.metadataName) {
384
+ console.log(`Host: ${hbomSummary.metadataName}`);
385
+ }
386
+ console.log(`Total diagnostics: ${hbomSummary.commandDiagnosticCount}`);
387
+ console.log(`Missing commands: ${hbomSummary.missingCommandCount}`);
388
+ console.log(`Permission denied: ${hbomSummary.permissionDeniedCount}`);
389
+ console.log(`Partial support: ${hbomSummary.partialSupportCount}`);
390
+ console.log(`Timeouts: ${hbomSummary.timeoutCount}`);
391
+ console.log(`Other command errors: ${hbomSummary.commandErrorCount}`);
392
+
393
+ if (!hbomSummary.commandDiagnosticCount) {
394
+ console.log("No HBOM collector diagnostics were found.");
395
+ return;
396
+ }
397
+
398
+ if (missingCommands.length) {
399
+ console.log("\nMissing commands:");
400
+ for (const line of buildFormattedDiagnosticLines(missingCommands)) {
401
+ console.log(line);
402
+ }
403
+ }
404
+
405
+ if (permissionDeniedCommands.length) {
406
+ console.log("\nPermission-sensitive enrichments:");
407
+ for (const line of buildFormattedDiagnosticLines(
408
+ permissionDeniedCommands,
409
+ )) {
410
+ console.log(line);
411
+ }
412
+ }
413
+
414
+ if (hbomSummary.requiresPrivilegedEnrichment) {
415
+ console.log(
416
+ "\nSome Linux enrichments can likely succeed only with --privileged and a target environment that allows non-interactive sudo.",
417
+ );
418
+ }
419
+ }
420
+
421
+ async function runDiagnosticsCommand() {
422
+ if (options.includeRuntime) {
423
+ thoughtLog(
424
+ "The diagnostics subcommand focuses on HBOM collector gaps only, so I will skip the merged runtime host view.",
425
+ );
426
+ }
427
+ const bomJson = options.input
428
+ ? await loadBomFromInputFile(options.input)
429
+ : (
430
+ await createHBom(process.cwd(), {
431
+ ...options,
432
+ includeRuntime: false,
433
+ print: false,
434
+ validate: false,
435
+ })
436
+ ).bomJson;
437
+ printHbomDiagnosticsReport(bomJson);
438
+ }
439
+
440
+ (async () => {
441
+ if (selectedCommand === "diagnostics") {
442
+ await runDiagnosticsCommand();
443
+ if (options.dryRun || DEBUG_MODE) {
444
+ printActivitySummary();
445
+ }
446
+ return;
447
+ }
448
+ thoughtLog(
449
+ "Let's generate a Hardware Bill-of-Materials (HBOM) for this host.",
450
+ );
451
+ if (options.includeRuntime) {
452
+ thoughtLog(
453
+ "Let's also collect the runtime inventory so I can build a merged HBOM+OBOM host view without guessing relationships.",
454
+ );
455
+ }
456
+ const { bomJson } = await createHBom(process.cwd(), options);
457
+ if (options.validate && !validateBom(bomJson)) {
458
+ process.exit(1);
459
+ }
460
+ const output = JSON.stringify(bomJson, null, options.pretty ? 2 : null);
461
+ if (options.print) {
462
+ console.log(output);
463
+ } else {
464
+ const outputDirectory = getOutputDirectory(options.output);
465
+ if (outputDirectory && !safeExistsSync(outputDirectory)) {
466
+ safeMkdirSync(outputDirectory, { recursive: true });
467
+ }
468
+ safeWriteSync(options.output, output);
469
+ thoughtLog(`Let's save the HBOM file to '${options.output}'.`);
470
+ }
471
+ if (options.exportProto) {
472
+ const protoOutputDirectory = getOutputDirectory(options.protoBinFile);
473
+ if (protoOutputDirectory && !safeExistsSync(protoOutputDirectory)) {
474
+ safeMkdirSync(protoOutputDirectory, { recursive: true });
475
+ }
476
+ const { writeBinary } = await importProtobomModule(
477
+ hbomCommandName,
478
+ "protobuf export",
479
+ );
480
+ writeBinary(bomJson, options.protoBinFile);
481
+ thoughtLog(
482
+ `Let's also save the HBOM protobuf binary to '${options.protoBinFile}'.`,
483
+ );
484
+ }
485
+ printHbomDiagnosticNotice(bomJson);
486
+ if (options.dryRun || DEBUG_MODE) {
487
+ printActivitySummary();
488
+ }
489
+ })().catch((error) => {
490
+ if (options.dryRun || DEBUG_MODE) {
491
+ printActivitySummary();
492
+ }
493
+ console.error(error.message || error);
494
+ process.exit(1);
495
+ });