-
-
Notifications
You must be signed in to change notification settings - Fork 0
feat(sbom): consistent primary component, document identity, and CRA metadata #364
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,10 +8,12 @@ import ( | |
|
|
||
| "github.com/bomly-dev/bomly-cli/internal/cli/exit" | ||
| "github.com/bomly-dev/bomly-cli/internal/cli/render" | ||
| "github.com/bomly-dev/bomly-cli/internal/config" | ||
| "github.com/bomly-dev/bomly-cli/internal/engine" | ||
| scanengine "github.com/bomly-dev/bomly-cli/internal/engine/scan" | ||
| "github.com/bomly-dev/bomly-cli/internal/output" | ||
| "github.com/bomly-dev/bomly-cli/internal/sbom" | ||
| "github.com/bomly-dev/bomly-cli/internal/system" | ||
| "github.com/bomly-dev/bomly-cli/internal/tui" | ||
| "github.com/bomly-dev/bomly-sdk" | ||
| "github.com/spf13/cobra" | ||
|
|
@@ -126,10 +128,11 @@ func newScanCmd() *cobra.Command { | |
| return output.WriteSARIF(w, findings, pipeResult.Registry, "bomly", cmd.Root().Version, output.SARIFOptions{IncludeReachability: commandCtx.ResolvedConfig.Analyze, LocationGraphs: []*sdk.Graph{pipeResult.Graph}}) | ||
| } | ||
|
|
||
| sbomBuildOpts := scanSBOMBuildOptions(payload.Project, commandCtx.ResolvedConfig, cmd.Root().Version, resolved, pipeResult.Registry, selectedScope, len(pipeResult.DetectorWarnings) > 0) | ||
|
|
||
| if len(outputSpecs) > 0 { | ||
| prog.Advance("Writing additional output") | ||
| stdout := streams.reportWriter() | ||
| sbomBuildOpts := sbom.BuildOptions{ToolNames: sbomToolNames(resolved), Registry: pipeResult.Registry} | ||
| for _, spec := range outputSpecs { | ||
| switch { | ||
| case spec.IsSBOM(): | ||
|
|
@@ -157,7 +160,7 @@ func newScanCmd() *cobra.Command { | |
| if !ok { | ||
| return exit.InvalidInputError("output format %q is not supported by scan", graphOutputFormat) | ||
| } | ||
| rawDocument, err := sbom.MarshalDepGraphJSON(selectedGraph, target, sbom.BuildOptions{ToolNames: sbomToolNames(resolved), Registry: pipeResult.Registry}, sbom.EncodeOptions{Pretty: true}) | ||
| rawDocument, err := sbom.MarshalDepGraphJSON(selectedGraph, target, sbomBuildOpts, sbom.EncodeOptions{Pretty: true}) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal %s sbom: %w", graphOutputFormat, err) | ||
| } | ||
|
|
@@ -211,6 +214,82 @@ func scanPolicyExit(auditEnabled bool, findings []sdk.Finding) error { | |
| return nil | ||
| } | ||
|
|
||
| // scanSBOMBuildOptions assembles the SBOM projection options for a scan: the | ||
| // document is named after the scanned project, the primary component mirrors | ||
| // it, and optional provenance metadata comes from configuration. | ||
| func scanSBOMBuildOptions(project output.ProjectDescriptor, current config.Resolved, version string, resolved []sdk.DetectionResult, registry *sdk.PackageRegistry, selectedScope sdk.Scope, degraded bool) sbom.BuildOptions { | ||
| opts := sbom.BuildOptions{ | ||
| ToolNames: sbomToolNames(resolved), | ||
| ToolVersion: strings.TrimSpace(version), | ||
| Registry: registry, | ||
| Lifecycle: sbomLifecyclePhase(project.TargetType), | ||
| Aggregate: sbomCompositionAggregate(selectedScope, degraded), | ||
| Provenance: sbom.Provenance{ | ||
| Manufacturer: strings.TrimSpace(current.SBOMManufacturer), | ||
| SecurityContact: strings.TrimSpace(current.SBOMSecurityContact), | ||
| VulnerabilityDisclosureURL: strings.TrimSpace(current.SBOMVulnerabilityDisclosureURL), | ||
| SupportEnd: strings.TrimSpace(current.SBOMSupportEnd), | ||
| }, | ||
| } | ||
| if name := strings.TrimSpace(project.Name); name != "" { | ||
| projectVersion := strings.TrimSpace(project.TargetRef) | ||
| if projectVersion == "" { | ||
| projectVersion = gitDescribeVersion(project.Path) | ||
| } | ||
| opts.DocumentName = name | ||
| opts.ProjectRoot = &sbom.ProjectRoot{Name: name, Version: projectVersion} | ||
| } | ||
| return opts | ||
| } | ||
|
|
||
| // sbomLifecyclePhase maps the execution target type onto a CycloneDX | ||
| // lifecycle phase: source trees are pre-build inventories, container images | ||
| // describe a built artifact. Other targets (for example re-exported SBOMs) | ||
| // carry no phase claim. | ||
| func sbomLifecyclePhase(targetType string) string { | ||
| switch targetType { | ||
| case "filesystem", "git repository": | ||
| return "pre-build" | ||
| case "container image": | ||
| return "post-build" | ||
| default: | ||
| return "" | ||
| } | ||
| } | ||
|
|
||
| // sbomCompositionAggregate declares dependency-graph completeness. A scope | ||
| // filter deliberately drops part of the graph, and degraded resolution means | ||
| // completeness is unknown; only an unfiltered, warning-free scan may claim | ||
| // "complete". | ||
| func sbomCompositionAggregate(selectedScope sdk.Scope, degraded bool) string { | ||
| if degraded { | ||
| return "unknown" | ||
| } | ||
| if selectedScope != sdk.ScopeUnknown && selectedScope != "" { | ||
| return "incomplete" | ||
| } | ||
| return "complete" | ||
| } | ||
|
|
||
| // gitDescribeVersion derives a project version from Git history when the scan | ||
| // target is a checkout with no explicit ref (local path scans). Returns "" | ||
| // when Git or history is unavailable — the version is then simply omitted. | ||
| func gitDescribeVersion(path string) string { | ||
| if strings.TrimSpace(path) == "" { | ||
| return "" | ||
| } | ||
| gitPath, err := system.LookPath("git") | ||
| if err != nil { | ||
| return "" | ||
| } | ||
| cmd := system.Command(gitPath, "-C", path, "describe", "--tags", "--always", "--dirty") | ||
| out, err := cmd.Output() | ||
| if err != nil { | ||
| return "" | ||
| } | ||
| return strings.TrimSpace(string(out)) | ||
|
Comment on lines
+274
to
+290
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Log the Git command before execution. Line 285 starts a subprocess. It does not log the executable path, arguments, and working directory at DEBUG level. Emit the required DEBUG log before As per coding guidelines, “When invoking subprocesses, DEBUG logs must include the binary path, arguments, and working directory so the command can be reproduced.” 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| } | ||
|
|
||
| func sbomToolNames(results []sdk.DetectionResult) []string { | ||
| tools := make([]string, 0, len(results)) | ||
| seen := make(map[string]struct{}, len(results)) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Correct the supplier fallback statement.
The exporter does not default a package supplier to the producing tool. SPDX sets
PackageSupplieronly whenmanufactureris configured. State that supplier metadata remains absent until a data source provides it, or implement the documented fallback.🤖 Prompt for AI Agents