Skip to content
54 changes: 52 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Common options:
- `--arch <value>` override architecture detection
- `--sensitive` include raw identifiers instead of redacted defaults
- `--no-command-enrichment` disable optional command-based enrichment on Linux
- `--privileged` enable privileged Linux SMBIOS enrichment via `dmidecode`
- `--privileged` enable privileged Linux enrichment and non-interactive `sudo -n` retries for documented permission-sensitive commands
- `--plist-enrichment` enable additional Darwin plist-based enrichment
- `--strict` fail instead of returning partial results when enrichment fails
- `--timeout <ms>` set per-command timeout
Expand Down Expand Up @@ -75,6 +75,9 @@ console.log(collectedTrace.activities);

const plan = getCommandPlan({ platform: "linux", architecture: "amd64" });

// Some plan entries are templates; their final arguments are resolved per
// device or interface at runtime when live collection expands the plan.

const rebuilt = buildHardwareFromSources({
platform: "linux",
architecture: "amd64",
Expand All @@ -85,11 +88,57 @@ const rebuilt = buildHardwareFromSources({
});
```

### Reading command diagnostics from the library

`cdx-hbom` keeps JSON output on the BOM itself and records optional command issues in the attached collector trace and BOM evidence properties.

```js
import {
collectHardware,
createCollectorTrace,
getCollectorTrace,
} from "@cdxgen/cdx-hbom";

const trace = createCollectorTrace();

const bom = await collectHardware({
platform: "linux",
architecture: "amd64",
includeCommandEnrichment: true,
includePrivilegedEnrichment: true,
allowPartial: true,
trace,
});

const collectedTrace = getCollectorTrace(bom) ?? trace;
const commandDiagnostics = collectedTrace.activities.filter(
(entry) =>
entry.kind === "command-diagnostic" || entry.kind === "command-warning",
);

for (const entry of commandDiagnostics) {
if (entry.issue === "missing-command") {
console.error(
`Install hint for ${entry.command}: ${entry.hint ?? "see package docs"}`,
);
}

if (entry.issue === "permission-denied") {
console.error(
`Privilege hint for ${entry.command}: ${entry.hint ?? "retry with --privileged"}`,
);
}
}
```

You can also read serialized command diagnostics from the BOM root by inspecting `cdx:hbom:evidence:commandDiagnostic*` properties.

## Native enrichment currently covered

### Dry-run and trace support

- `dryRun: true` blocks command execution inside `cdx-hbom` itself instead of relying on a caller-side fallback.
- `getCommandPlan()` exposes the static command registry, including template entries whose concrete arguments are resolved per device or interface at runtime.
- Successful file reads and directory discovery, plus completed/blocked/failed command attempts, are recorded in the collector trace.
- Pass `trace: createCollectorTrace()` to collect activity into a caller-owned ledger, or read it later via `getCollectorTrace(bom)`.
- The attached trace is non-enumerable, so `JSON.stringify(bom)` still emits a normal CycloneDX document.
Expand All @@ -99,7 +148,8 @@ const rebuilt = buildHardwareFromSources({
- `/proc` and `/sys` baseline discovery
- CPU, memory, storage, PCI, USB, DRM display, audio, MMC/SDIO, and network inventory
- `hwmon`, thermal zones, TPM, and NVMe controller enrichment
- optional command enrichment via `lscpu`, `lsblk`, `ip`, `lsmem`, `hostnamectl`, `lspci`, `lsusb`, and `ethtool`
- optional command enrichment via `lscpu`, `lsblk`, `ip`, `lsmem`, `hostnamectl`, `lspci`, `lsusb`, `ethtool`, `cpupower`, `drm_info`, `upower`, `fwupdmgr`, `boltctl`, `mmcli`, and `edid-decode`
- command diagnostics for missing utilities, partial support, and permission-sensitive enrichments

### Darwin arm64

Expand Down
72 changes: 36 additions & 36 deletions SECURITY.md

Large diffs are not rendered by default.

55 changes: 53 additions & 2 deletions bin/cdx-hbom.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { readFileSync } from "node:fs";
import process from "node:process";

import { collectHardware } from "../index.js";
import { collectHardware, getCollectorTrace } from "../index.js";

const packageJson = JSON.parse(
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
Expand All @@ -21,7 +21,7 @@ Options:
--arch <value> Override architecture selection
--sensitive Include raw identifiers
--no-command-enrichment Disable optional command-based enrichment
--privileged Enable privileged enrichment (Linux dmidecode)
--privileged Enable privileged enrichment and sudo -n retries for permission-sensitive Linux commands
--plist-enrichment Enable plist enrichment (Darwin)
--strict Fail on partial collection errors
--timeout <ms> Command timeout in milliseconds
Expand Down Expand Up @@ -58,6 +58,11 @@ async function main(argv) {
timeoutMs: options.timeout,
});

const diagnostics = collectCliDiagnostics(bom);
if (diagnostics.length) {
process.stderr.write(`${diagnostics.join("\n")}\n`);
}

process.stdout.write(
`${JSON.stringify(bom, null, options.pretty ? 2 : 0)}\n`,
);
Expand Down Expand Up @@ -169,6 +174,52 @@ function requireValue(argv, index, option) {
return value;
}

function collectCliDiagnostics(bom) {
const trace = getCollectorTrace(bom);
const activities = Array.isArray(trace?.activities) ? trace.activities : [];
const grouped = activities
.filter(
(entry) =>
entry?.kind === "command-diagnostic" ||
entry?.kind === "command-warning",
)
.reduce((result, entry) => {
const key = [
entry.command ?? entry.id ?? "command",
entry.issue ?? "warning",
entry.reason ?? "",
entry.hint ?? "",
].join("\u0000");
const current = result.get(key) ?? { ...entry, count: 0 };
current.count += 1;
result.set(key, current);
return result;
}, new Map());

return [...grouped.values()].map((entry) => formatCliDiagnostic(entry));
}

function formatCliDiagnostic(entry) {
const id = entry.command
? entry.count > 1
? `${entry.command} (${entry.count} invocations)`
: `${entry.command}`
: entry.id
? `${entry.id}`
: "command";
const issue = entry.issue ? `${entry.issue}` : "warning";
const reason = entry.reason ? `${entry.reason}` : undefined;
const hint = entry.hint ? `${entry.hint}` : undefined;
return [
`Warning: ${id}`,
`[${issue}]`,
reason,
hint ? `Hint: ${hint}` : undefined,
]
.filter(Boolean)
.join(" ");
}

main(process.argv.slice(2)).catch((error) => {
process.stderr.write(`${error.message}\n`);
process.exitCode = 1;
Expand Down
Loading
Loading