Skip to content

feat: export generateSbom and add sbom CLI command#472

Merged
a-oren merged 2 commits into
guacsec:mainfrom
a-oren:TC-3993
Apr 14, 2026
Merged

feat: export generateSbom and add sbom CLI command#472
a-oren merged 2 commits into
guacsec:mainfrom
a-oren:TC-3993

Conversation

@a-oren

@a-oren a-oren commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add public generateSbom(manifestPath, opts) function that generates a CycloneDX SBOM locally without backend HTTP requests
  • Add sbom CLI command with --output and --workspaceDir options
  • Add comprehensive test suite for SBOM generation

Details

The generateSbom() function validates manifest readability upfront
(consistent with stackAnalysis() and componentAnalysis()), delegates
to the existing provider pipeline via generateOneSbom(), and throws
descriptive errors on failure (unsupported manifest, missing purl, etc.).

The sbom CLI command accepts a manifest path as a positional argument.
--output / -o writes the SBOM JSON to a file instead of stdout.
--workspaceDir / -w sets the workspace root for monorepo lock file
resolution. Error handling follows the same structured JSON pattern as
the license command.

Implements TC-3993

Test plan

  • SBOM generation for pom.xml manifest returns valid CycloneDX JSON
  • SBOM generation for package.json manifest returns valid CycloneDX JSON
  • Unsupported manifest types throw descriptive error
  • Generated SBOM contains metadata.component with expected purl
  • Missing purl in SBOM throws error
  • Existing tests unaffected

🤖 Generated with Claude Code

@a-oren
a-oren requested a review from ruromero April 12, 2026 07:54
@qodo-code-review

Copy link
Copy Markdown

Review Summary by Qodo

Export generateSbom function and add sbom CLI command

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Export new generateSbom() function for local SBOM generation
• Add sbom CLI command with manifest path and options
• Support --output flag to write SBOM to file
• Support --workspaceDir flag for monorepo lock file resolution
• Add comprehensive test suite for SBOM generation
Diagram
flowchart LR
  A["generateSbom Function"] -->|exported from| B["src/index.js"]
  B -->|imported by| C["src/cli.js"]
  C -->|implements| D["sbom CLI Command"]
  D -->|supports| E["--output flag"]
  D -->|supports| F["--workspaceDir flag"]
  G["Test Suite"] -->|validates| A
Loading

Grey Divider

File Changes

1. src/index.js ✨ Enhancement +17/-1

Export generateSbom function for SBOM generation

• Export new generateSbom() public function that generates CycloneDX SBOM locally
• Function accepts manifest path and optional configuration options
• Wraps internal generateOneSbom() and throws error if generation fails
• Add generateSbom to default export object

src/index.js


2. src/cli.js ✨ Enhancement +41/-2

Add sbom CLI command with file output support

• Import fs module and generateSbom function
• Add new sbom command with manifest path positional argument
• Implement --output / -o flag to write SBOM JSON to file
• Implement --workspaceDir / -w flag for monorepo support
• Update CLI usage string to include sbom command
• Register sbom command with yargs parser

src/cli.js


3. test/generate_sbom.test.js 🧪 Tests +101/-0

Add test suite for generateSbom function

• Create new test file with 5 comprehensive test cases
• Test SBOM generation for pom.xml and package.json manifests
• Test error handling for unsupported manifest types
• Validate SBOM structure with metadata and components
• Test error handling when SBOM is missing required purl field
• Use esmock for mocking provider dependencies

test/generate_sbom.test.js


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Apr 12, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3)   📘 Rule violations (0)   📎 Requirement gaps (0)   🎨 UX Issues (0)
🐞\ ≡ Correctness (1) ☼ Reliability (2) ⭐ New (1)

Grey Divider


Action required

1. SBOM CLI errors uncaught 🐞
Description
The new sbom CLI handler does not catch errors from generateSbom() or fs.writeFileSync(), so
failures surface as uncaught exceptions/unhandled rejections with inconsistent output and brittle
exit behavior. This is inconsistent with the license command’s explicit error handling and can
leave users without a clear, structured error message.
Code

src/cli.js[R389-399]

+	handler: async args => {
+		let manifest = args['/path/to/manifest']
+		const opts = args.workspaceDir ? { TRUSTIFY_DA_WORKSPACE_DIR: args.workspaceDir } : {}
+		const result = await generateSbom(manifest, opts)
+		const json = JSON.stringify(result, null, 2)
+		if (args.output) {
+			fs.writeFileSync(args.output, json)
+		} else {
+			console.log(json)
+		}
+	}
Evidence
sbom handler awaits SBOM generation and writes output without any try/catch, so any provider
failure (unsupported manifest, missing lockfile, tool invocation failure) or filesystem write error
will bubble out unhandled. In contrast, the license command demonstrates the project’s preferred
CLI behavior by catching errors, printing a structured error payload, and exiting non-zero.

src/cli.js[365-399]
src/cli.js[293-322]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `sbom` CLI command handler does not handle failures from SBOM generation or output file writing. This leads to unstructured crashes and inconsistent CLI behavior.
### Issue Context
Other CLI commands (notably `license`) wrap failure points in try/catch, emit a structured error, and exit with code 1.
### Fix Focus Areas
- Add try/catch around SBOM generation + writing, print a clear error to stderr (optionally JSON like `license`), and set `process.exitCode = 1` or `process.exit(1)`.
- If writing to a file, consider writing atomically (temp file + rename) to avoid partially-written output on failure.
- src/cli.js[365-399]
- src/cli.js[293-322]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Purl validation inconsistent 🐞
Description
generateOneSbom() treats metadata.component['bom-ref'] as a purl fallback, so generateSbom() can
succeed and return an SBOM whose root component has no metadata.component.purl. This contradicts the
stated failure reason ('missing purl in SBOM') and the new test expectation that missing purl should
throw.
Code

src/index.js[R283-288]

+export async function generateSbom(manifestPath, opts = {}) {
+	fs.accessSync(manifestPath, fs.constants.R_OK)
+	const result = await generateOneSbom(manifestPath, opts)
+	if (!result.ok) {
+		throw new Error(`Failed to generate SBOM for ${result.manifestPath}: ${result.reason}`)
+	}
Evidence
generateSbom() relies on generateOneSbom()'s ok/failed result; generateOneSbom() considers either
metadata.component.purl OR metadata.component['bom-ref'] sufficient, but the failure reason and
tests are written specifically in terms of a missing purl.

src/index.js[283-290]
src/index.js[304-313]
test/generate_sbom.test.js[90-106]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`generateOneSbom()` currently accepts `metadata.component['bom-ref']` as a fallback identifier, but `generateSbom()` is documented/tested as requiring a root `metadata.component.purl`. As a result, `generateSbom()` may return an SBOM without `metadata.component.purl`, even though errors/tests describe this as invalid.

## Issue Context
CycloneDX allows `bom-ref`, but callers of this new API (and the added tests) treat `metadata.component.purl` as required. The code should either (a) enforce `purl` strictly, or (b) if `bom-ref` is a valid purl, normalize it into `metadata.component.purl` before returning.

## Fix Focus Areas
- src/index.js[304-313]
- test/generate_sbom.test.js[90-106]

## Suggested change
- If `sbom.metadata.component.purl` is missing but `bom-ref` exists and looks like a purl (e.g., starts with `pkg:`), set `sbom.metadata.component.purl = bomRef` before returning success.
- Otherwise, return `{ ok: false, reason: 'missing purl in SBOM' }`.
- Optionally add a unit test covering the `bom-ref`-only case to lock in the intended behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. No manifest access check 🐞
Description
generateSbom() does not validate that manifestPath is readable before provider matching and SBOM
generation, unlike stackAnalysis()/componentAnalysis() which call fs.accessSync(). This makes
error behavior inconsistent across public APIs and can produce less actionable failures for
missing/permission-denied manifests.
Code

src/index.js[R283-289]

+export async function generateSbom(manifestPath, opts = {}) {
+	const result = await generateOneSbom(manifestPath, opts)
+	if (!result.ok) {
+		throw new Error(`Failed to generate SBOM for ${result.manifestPath}: ${result.reason}`)
+	}
+	return result.sbom
+}
Evidence
stackAnalysis() and componentAnalysis() explicitly validate the manifest is readable via
fs.accessSync(..., R_OK) before selecting providers and proceeding. generateSbom() delegates to
generateOneSbom(), which immediately calls match() and provider.provideStack() without any
up-front readability check, so missing/permission issues will surface later and inconsistently
depending on provider internals.

src/index.js[184-204]
src/index.js[283-312]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`generateSbom(manifestPath, opts)` should consistently fail fast when the manifest file is missing/unreadable, similar to other exported analysis functions.
### Issue Context
`stackAnalysis()` and `componentAnalysis()` call `fs.accessSync(manifest, fs.constants.R_OK)` before provider selection. `generateSbom()`/`generateOneSbom()` currently do not.
### Fix Focus Areas
- Add `fs.accessSync(manifestPath, fs.constants.R_OK)` (or async equivalent) at the start of `generateSbom()` (or inside `generateOneSbom()`) to standardize error behavior.
- Ensure the thrown error message remains actionable (includes the manifest path).
- src/index.js[184-204]
- src/index.js[283-312]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Grey Divider

Previous review results

Review updated until commit 8c46ca3

Results up to commit 39363a2


🐞 Bugs (2)  
📘 Rule violations (0)  
📎 Requirement gaps (0)  
🎨 UX Issues (0)

🐞\ ☼ Reliability (2)

Grey Divider


Action required

1. SBOM CLI errors uncaught 🐞
Description
The new sbom CLI handler does not catch errors from generateSbom() or fs.writeFileSync(), so
failures surface as uncaught exceptions/unhandled rejections with inconsistent output and brittle
exit behavior. This is inconsistent with the license command’s explicit error handling and can
leave users without a clear, structured error message.
Code

src/cli.js[R389-399]

+	handler: async args => {
+		let manifest = args['/path/to/manifest']
+		const opts = args.workspaceDir ? { TRUSTIFY_DA_WORKSPACE_DIR: args.workspaceDir } : {}
+		const result = await generateSbom(manifest, opts)
+		const json = JSON.stringify(result, null, 2)
+		if (args.output) {
+			fs.writeFileSync(args.output, json)
+		} else {
+			console.log(json)
+		}
+	}
Evidence
sbom handler awaits SBOM generation and writes output without any try/catch, so any provider
failure (unsupported manifest, missing lockfile, tool invocation failure) or filesystem write error
will bubble out unhandled. In contrast, the license command demonstrates the project’s preferred
CLI behavior by catching errors, printing a structured error payload, and exiting non-zero.

src/cli.js[365-399]
src/cli.js[293-322]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The `sbom` CLI command handler does not handle failures from SBOM generation or output file writing. This leads to unstructured crashes and inconsistent CLI behavior.

### Issue Context
Other CLI commands (notably `license`) wrap failure points in try/catch, emit a structured error, and exit with code 1.

### Fix Focus Areas
- Add try/catch around SBOM generation + writing, print a clear error to stderr (optionally JSON like `license`), and set `process.exitCode = 1` or `process.exit(1)`.
- If writing to a file, consider writing atomically (temp file + rename) to avoid partially-written output on failure.

- src/cli.js[365-399]
- src/cli.js[293-322]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. No manifest access check 🐞
Description
generateSbom() does not validate that manifestPath is readable before provider matching and SBOM
generation, unlike stackAnalysis()/componentAnalysis() which call fs.accessSync(). This makes
error behavior inconsistent across public APIs and can produce less actionable failures for
missing/permission-denied manifests.
Code

src/index.js[R283-289]

+export async function generateSbom(manifestPath, opts = {}) {
+	const result = await generateOneSbom(manifestPath, opts)
+	if (!result.ok) {
+		throw new Error(`Failed to generate SBOM for ${result.manifestPath}: ${result.reason}`)
+	}
+	return result.sbom
+}
Evidence
stackAnalysis() and componentAnalysis() explicitly validate the manifest is readable via
fs.accessSync(..., R_OK) before selecting providers and proceeding. generateSbom() delegates to
generateOneSbom(), which immediately calls match() and provider.provideStack() without any
up-front readability check, so missing/permission issues will surface later and inconsistently
depending on provider internals.

src/index.js[184-204]
src/index.js[283-312]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`generateSbom(manifestPath, opts)` should consistently fail fast when the manifest file is missing/unreadable, similar to other exported analysis functions.

### Issue Context
`stackAnalysis()` and `componentAnalysis()` call `fs.accessSync(manifest, fs.constants.R_OK)` before provider selection. `generateSbom()`/`generateOneSbom()` currently do not.

### Fix Focus Areas
- Add `fs.accessSync(manifestPath, fs.constants.R_OK)` (or async equivalent) at the start of `generateSbom()` (or inside `generateOneSbom()`) to standardize error behavior.
- Ensure the thrown error message remains actionable (includes the manifest path).

- src/index.js[184-204]
- src/index.js[283-312]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Grey Divider

Qodo Logo

Comment thread src/cli.js
@a-oren
a-oren force-pushed the TC-3993 branch 2 times, most recently from 43efad7 to 8c46ca3 Compare April 12, 2026 08:13
@a-oren

a-oren commented Apr 12, 2026

Copy link
Copy Markdown
Contributor Author

/review

@qodo-code-review

qodo-code-review Bot commented Apr 12, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit 8c46ca3

Add public generateSbom(manifestPath, opts) function that generates a
CycloneDX SBOM locally without backend HTTP requests. The function
validates manifest readability upfront (consistent with stackAnalysis
and componentAnalysis), delegates to the existing provider pipeline,
and throws descriptive errors on failure.

Add 'sbom' CLI command with --output flag to write SBOM JSON to file
and --workspaceDir flag for monorepo lock file resolution. CLI handler
includes structured error handling consistent with other commands.

Comprehensive test suite covers pom.xml and package.json manifests,
unsupported manifest error paths, SBOM structure validation, and
missing purl detection.

Implements TC-3993

Assisted-by: Claude Code
@a-oren
a-oren merged commit c2f6c64 into guacsec:main Apr 14, 2026
4 checks passed
@a-oren
a-oren deleted the TC-3993 branch April 28, 2026 07:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants