diff --git a/README.md b/README.md index 194c340..9fe48ef 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ for input and output formats We currently support two algorithm - Hierarchical: This merge algo tries to maintain, the order of the dependent components to its primary component. For spdx this is done via relationships and for cyclonedx via nested components & dependencies. - Flat: As the name states, are just consolidated lists of components, dependencies, etc. +- Assembly: Merge is very similar to Hierarchical, except that it does not create dependency relationships among the merged sboms. For `spdx hierarchical merge`, all packages, dependencies, externalrefs, files are consolidates into a individual lists, no duplicates are removed. The hierarchy is maintained via dependencies. A new primary package is created, which the generated SBOM describes. This primary package also adds contains relationship between itself and the primary components of the individual SBOMs. diff --git a/cmd/assemble.go b/cmd/assemble.go index 4510cb5..1428413 100644 --- a/cmd/assemble.go +++ b/cmd/assemble.go @@ -72,10 +72,13 @@ func init() { assembleCmd.MarkFlagsRequiredTogether("name", "version", "type") assembleCmd.Flags().BoolP("flatMerge", "f", false, "flat merge") - assembleCmd.Flags().BoolP("hierMerge", "m", true, "hierarchical merge") + assembleCmd.Flags().BoolP("hierMerge", "m", false, "hierarchical merge") + assembleCmd.Flags().BoolP("assemblyMerge", "a", false, "assembly merge") + assembleCmd.MarkFlagsMutuallyExclusive("flatMerge", "hierMerge", "assemblyMerge") assembleCmd.Flags().BoolP("xml", "x", false, "output in xml format") assembleCmd.Flags().BoolP("json", "j", true, "output in json format") + assembleCmd.MarkFlagsMutuallyExclusive("xml", "json") assembleCmd.PersistentFlags().BoolP("debug", "d", false, "debug output") } @@ -125,13 +128,11 @@ func extractArgs(cmd *cobra.Command, args []string) (*assemble.Params, error) { flatMerge, _ := cmd.Flags().GetBool("flatMerge") hierMerge, _ := cmd.Flags().GetBool("hierMerge") - - if flatMerge { - hierMerge = false - } + assemblyMerge, _ := cmd.Flags().GetBool("assemblyMerge") aParams.FlatMerge = flatMerge aParams.HierMerge = hierMerge + aParams.AssemblyMerge = assemblyMerge xml, _ := cmd.Flags().GetBool("xml") json, _ := cmd.Flags().GetBool("json") diff --git a/pkg/assemble/cdx/interface.go b/pkg/assemble/cdx/interface.go index 4642649..2ee5d21 100644 --- a/pkg/assemble/cdx/interface.go +++ b/pkg/assemble/cdx/interface.go @@ -109,6 +109,7 @@ type assemble struct { IncludeDuplicateComponents bool FlatMerge bool HierarchicalMerge bool + AssemblyMerge bool } type MergeSettings struct { @@ -129,7 +130,9 @@ func Merge(ms *MergeSettings) error { return merger.flatMerge() } else if ms.Assemble.HierarchicalMerge { return merger.hierarchicalMerge() + } else if ms.Assemble.AssemblyMerge { + return merger.assemblyMerge() } - return nil + return merger.hierarchicalMerge() } diff --git a/pkg/assemble/cdx/merge.go b/pkg/assemble/cdx/merge.go index 0563096..6afa77d 100644 --- a/pkg/assemble/cdx/merge.go +++ b/pkg/assemble/cdx/merge.go @@ -21,7 +21,6 @@ import ( cydx "github.com/CycloneDX/cyclonedx-go" "github.com/interlynk-io/sbomasm/pkg/logger" "github.com/samber/lo" - "sigs.k8s.io/release-utils/version" ) type merge struct { @@ -52,9 +51,6 @@ func (m *merge) initOutBom() { m.out.SerialNumber = newSerialNumber() m.out.Metadata = &cydx.Metadata{} m.out.Metadata.Timestamp = utcNowTime() - m.out.Metadata.Tools.Tools = &[]cydx.Tool{ - {Vendor: "Interlynk.io", Name: "sbomasm", Version: version.GetVersionInfo().GitVersion}, - } if m.settings.App.Supplier.Name != "" || m.settings.App.Supplier.Email != "" { m.out.Metadata.Supplier = &cydx.OrganizationalEntity{} @@ -146,12 +142,6 @@ func (m *merge) flatMerge() error { cs := newComponentService(*m.settings.Ctx) log.Debug("Merging BOMs into a flat list") - tools := lo.Flatten(lo.Map(m.in, func(bom *cydx.BOM, _ int) []cydx.Tool { - if bom.Metadata.Tools != nil { - return *bom.Metadata.Tools.Tools - } - return []cydx.Tool{} - })) priComps := lo.Map(m.in, func(bom *cydx.BOM, _ int) *cydx.Component { if bom.Metadata != nil && bom.Metadata.Component != nil { @@ -188,7 +178,12 @@ func (m *merge) flatMerge() error { })) m.out.Metadata.Component = m.setupPrimaryComp() - m.out.Metadata.Tools.Tools = &tools + + tools := getAllTools(m.in) + m.out.Metadata.Tools = &cydx.ToolsChoice{ + Components: &[]cydx.Component{}, + } + *m.out.Metadata.Tools.Components = append(*m.out.Metadata.Tools.Components, tools...) //Add the primary component to the list of components for _, c := range priComps { @@ -217,19 +212,80 @@ func (m *merge) flatMerge() error { } -func (m *merge) hierarchicalMerge() error { +func (m *merge) assemblyMerge() error { log := logger.FromContext(*m.settings.Ctx) cs := newComponentService(*m.settings.Ctx) - log.Debug("Merging BOMs hierarchically") + log.Debug("Merging BOMs as an assembly") + + priComps := lo.Map(m.in, func(bom *cydx.BOM, _ int) *cydx.Component { + if bom.Metadata != nil && bom.Metadata.Component != nil { + pc := cs.StoreAndCloneWithNewID(bom.Metadata.Component) + + if pc.Components == nil { + pc.Components = &[]cydx.Component{} + } + + for _, c := range lo.FromPtr(bom.Components) { + *pc.Components = append(*pc.Components, *cs.StoreAndCloneWithNewID(&c)) + } + return pc + } + return &cydx.Component{} + }) + + deps := lo.Flatten(lo.Map(m.in, func(bom *cydx.BOM, _ int) []cydx.Dependency { + newDeps := []cydx.Dependency{} + for _, dep := range lo.FromPtr(bom.Dependencies) { + nd := cydx.Dependency{} + ref, found := cs.ResolveDepID(dep.Ref) + if !found { + log.Warnf("dependency %s not found", dep.Ref) + continue + } + + deps := cs.ResolveDepIDs(lo.FromPtr(dep.Dependencies)) - tools := lo.Flatten(lo.Map(m.in, func(bom *cydx.BOM, _ int) []cydx.Tool { - if bom.Metadata.Tools != nil { - return *bom.Metadata.Tools.Tools + nd.Ref = ref + nd.Dependencies = &deps + newDeps = append(newDeps, nd) } - return []cydx.Tool{} + return newDeps })) + m.out.Metadata.Component = m.setupPrimaryComp() + + m.out.Metadata.Component.Components = &[]cydx.Component{} + for _, c := range priComps { + *m.out.Metadata.Component.Components = append(*m.out.Metadata.Component.Components, *c) + } + + tools := getAllTools(m.in) + m.out.Metadata.Tools = &cydx.ToolsChoice{ + Components: &[]cydx.Component{}, + } + *m.out.Metadata.Tools.Components = append(*m.out.Metadata.Tools.Components, tools...) + + if m.settings.Assemble.IncludeComponents { + m.out.Components = &[]cydx.Component{} + for _, c := range priComps { + *m.out.Components = append(*m.out.Components, *c) + } + } + + if m.settings.Assemble.IncludeDependencyGraph { + m.out.Dependencies = &deps + } + + return m.writeSBOM() +} + +func (m *merge) hierarchicalMerge() error { + log := logger.FromContext(*m.settings.Ctx) + cs := newComponentService(*m.settings.Ctx) + + log.Debug("Merging BOMs hierarchically") + priComps := lo.Map(m.in, func(bom *cydx.BOM, _ int) *cydx.Component { if bom.Metadata != nil && bom.Metadata.Component != nil { pc := cs.StoreAndCloneWithNewID(bom.Metadata.Component) @@ -266,7 +322,12 @@ func (m *merge) hierarchicalMerge() error { })) m.out.Metadata.Component = m.setupPrimaryComp() - m.out.Metadata.Tools.Tools = &tools + + tools := getAllTools(m.in) + m.out.Metadata.Tools = &cydx.ToolsChoice{ + Components: &[]cydx.Component{}, + } + *m.out.Metadata.Tools.Components = append(*m.out.Metadata.Tools.Components, tools...) //Add depedencies between new primary component and old primary components priIds := lo.Map(priComps, func(c *cydx.Component, _ int) string { diff --git a/pkg/assemble/cdx/util.go b/pkg/assemble/cdx/util.go index bcdb50a..5d9b0f5 100644 --- a/pkg/assemble/cdx/util.go +++ b/pkg/assemble/cdx/util.go @@ -26,6 +26,7 @@ import ( "github.com/interlynk-io/sbomasm/pkg/logger" "github.com/mitchellh/copystructure" "github.com/mitchellh/hashstructure/v2" + "sigs.k8s.io/release-utils/version" ) func newSerialNumber() string { @@ -96,3 +97,45 @@ func utcNowTime() string { locationTime := time.Now().In(location) return locationTime.Format(time.RFC3339) } + +func getAllTools(boms []*cydx.BOM) []cydx.Component { + tools := []cydx.Component{} + + tools = append(tools, *toolInfo("sbomasm", version.GetVersionInfo().GitVersion, "Assembler for your sboms", "Interlynk", "https://interlynk.io", "support@interlynk.io", "Apache-2.0")) + + for _, bom := range boms { + if bom.Metadata != nil && bom.Metadata.Tools != nil { + for _, tool := range *bom.Metadata.Tools.Tools { + tools = append(tools, *toolInfo(tool.Name, tool.Version, "", tool.Vendor, "", "", "")) + } + } + + if bom.Metadata != nil && bom.Metadata.Tools != nil && bom.Metadata.Tools.Components != nil { + for _, tool := range *bom.Metadata.Tools.Components { + tools = append(tools, *toolInfo(tool.Name, tool.Version, "", "", "", "", "")) + } + } + } + return tools +} + +func toolInfo(name, version, desc, sName, sUrl, sEmail, sLicense string) *cydx.Component { + return &cydx.Component{ + Type: cydx.ComponentTypeApplication, + Name: name, + Version: version, + Description: desc, + Supplier: &cydx.OrganizationalEntity{ + Name: sName, + URL: &[]string{sUrl}, + Contact: &[]cydx.OrganizationalContact{{Email: sEmail}}, + }, + Licenses: &cydx.Licenses{ + { + License: &cydx.License{ + ID: sLicense, + }, + }, + }, + } +} diff --git a/pkg/assemble/combiner.go b/pkg/assemble/combiner.go index ff9059e..aa9d89d 100644 --- a/pkg/assemble/combiner.go +++ b/pkg/assemble/combiner.go @@ -88,6 +88,7 @@ func toCDXMergerSettings(c *config) *cdx.MergeSettings { ms.Assemble.FlatMerge = c.Assemble.FlatMerge ms.Assemble.HierarchicalMerge = c.Assemble.HierarchicalMerge + ms.Assemble.AssemblyMerge = c.Assemble.AssemblyMerge ms.Assemble.IncludeComponents = c.Assemble.IncludeComponents ms.Assemble.IncludeDuplicateComponents = c.Assemble.includeDuplicateComponents ms.Assemble.IncludeDependencyGraph = c.Assemble.IncludeDependencyGraph diff --git a/pkg/assemble/config.go b/pkg/assemble/config.go index 62433dc..9108e56 100644 --- a/pkg/assemble/config.go +++ b/pkg/assemble/config.go @@ -19,7 +19,6 @@ import ( "crypto/sha256" "fmt" "io" - "io/ioutil" "log" "os" "strings" @@ -85,6 +84,7 @@ type assemble struct { includeDuplicateComponents bool FlatMerge bool `yaml:"flat_merge"` HierarchicalMerge bool `yaml:"hierarchical_merge"` + AssemblyMerge bool `yaml:"assembly_merge"` } type config struct { @@ -125,6 +125,7 @@ var defaultConfig = config{ Assemble: assemble{ FlatMerge: false, HierarchicalMerge: true, + AssemblyMerge: false, IncludeComponents: true, IncludeDependencyGraph: true, includeDuplicateComponents: true, @@ -158,7 +159,7 @@ func newConfig() *config { func (c *config) readAndMerge(p *Params) error { if p.ConfigPath != "" { - yF, err := ioutil.ReadFile(p.ConfigPath) + yF, err := os.ReadFile(p.ConfigPath) if err != nil { return err } @@ -171,6 +172,7 @@ func (c *config) readAndMerge(p *Params) error { c.Assemble.FlatMerge = p.FlatMerge c.Assemble.HierarchicalMerge = p.HierMerge + c.Assemble.AssemblyMerge = p.AssemblyMerge } c.input.files = p.Input @@ -276,14 +278,6 @@ func (c *config) validate() error { return fmt.Errorf("input files are not set") } - if !c.Assemble.FlatMerge && !c.Assemble.HierarchicalMerge { - return fmt.Errorf("flat merge or hierarchical merge must be set") - } - - if c.Assemble.FlatMerge && c.Assemble.HierarchicalMerge { - return fmt.Errorf("flat merge or hierarchical merger can be set, not both") - } - err := c.validateInputContent() if err != nil { return err diff --git a/pkg/assemble/interface.go b/pkg/assemble/interface.go index 8983ab6..7859dd6 100644 --- a/pkg/assemble/interface.go +++ b/pkg/assemble/interface.go @@ -28,8 +28,9 @@ type Params struct { Version string Type string - FlatMerge bool - HierMerge bool + FlatMerge bool + HierMerge bool + AssemblyMerge bool Xml bool Json bool diff --git a/pkg/assemble/spdx/interface.go b/pkg/assemble/spdx/interface.go index 901edf9..d51de55 100644 --- a/pkg/assemble/spdx/interface.go +++ b/pkg/assemble/spdx/interface.go @@ -85,6 +85,7 @@ type assemble struct { IncludeDuplicateComponents bool FlatMerge bool HierarchicalMerge bool + AssemblyMerge bool } type MergeSettings struct { @@ -105,6 +106,10 @@ func Merge(ms *MergeSettings) error { return merger.flatMerge() } else if ms.Assemble.HierarchicalMerge { return merger.hierarchicalMerge() + } else if ms.Assemble.AssemblyMerge { + return merger.assemblyMerge() + } else { + return merger.hierarchicalMerge() } return nil diff --git a/pkg/assemble/spdx/merge.go b/pkg/assemble/spdx/merge.go index e238777..efc4a37 100644 --- a/pkg/assemble/spdx/merge.go +++ b/pkg/assemble/spdx/merge.go @@ -349,7 +349,11 @@ func (m *merge) hierarchicalMerge() error { } func (m *merge) flatMerge() error { - return fmt.Errorf("spdx flat merge not implemented") + return fmt.Errorf("not implemented") +} + +func (m *merge) assemblyMerge() error { + return fmt.Errorf("not implemented") } func (m *merge) writeSBOM() error {