Skip to content

Commit fa47af5

Browse files
committed
Generates cdx 1.6 compliannt sboms
1 parent 21ad0ce commit fa47af5

File tree

3 files changed

+215
-35
lines changed

3 files changed

+215
-35
lines changed

pkg/assemble/cdx/merge.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ func (m *merge) combinedMerge() error {
5959
m.loadBoms()
6060

6161
log.Debugf("initialize component service")
62-
cs := newComponentService(*m.settings.Ctx)
62+
//cs := newComponentService(*m.settings.Ctx)
63+
cs := newUniqueComponentService(*m.settings.Ctx)
6364

6465
// Build primary component list from each sbom
6566
priCompList := buildPrimaryComponentList(m.in, cs)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2023 Interlynk.io
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
//
17+
18+
package cdx
19+
20+
import (
21+
"context"
22+
"fmt"
23+
"strings"
24+
25+
cydx "github.com/CycloneDX/cyclonedx-go"
26+
)
27+
28+
type uniqueComponentService struct {
29+
ctx context.Context
30+
//unique list of new components
31+
compMap map[string]*cydx.Component
32+
33+
//mapping from old component id to new component id
34+
idMap map[string]string
35+
}
36+
37+
func newUniqueComponentService(ctx context.Context) *uniqueComponentService {
38+
return &uniqueComponentService{
39+
ctx: ctx,
40+
compMap: make(map[string]*cydx.Component),
41+
idMap: make(map[string]string),
42+
}
43+
}
44+
45+
func (s *uniqueComponentService) StoreAndCloneWithNewID(c *cydx.Component) (*cydx.Component, bool) {
46+
if c == nil {
47+
return nil, false
48+
}
49+
50+
lookupKey := fmt.Sprintf("%s-%s-%s",
51+
strings.ToLower(string(c.Type)),
52+
strings.ToLower(c.Name),
53+
strings.ToLower(c.Version))
54+
55+
if foundComp, ok := s.compMap[lookupKey]; ok {
56+
if c.BOMRef != foundComp.BOMRef {
57+
s.idMap[c.BOMRef] = foundComp.BOMRef
58+
}
59+
return foundComp, true
60+
}
61+
62+
nc, err := cloneComp(c)
63+
if err != nil {
64+
panic(err)
65+
}
66+
67+
newID := newBomRef()
68+
nc.BOMRef = newID
69+
70+
s.compMap[lookupKey] = nc
71+
s.idMap[c.BOMRef] = newID
72+
return nc, false
73+
}
74+
75+
func (s *uniqueComponentService) ResolveDepID(depID string) (string, bool) {
76+
if newID, ok := s.idMap[depID]; ok {
77+
return newID, true
78+
}
79+
return "", false
80+
}
81+
82+
func (s *uniqueComponentService) ResolveDepIDs(depIDs []string) []string {
83+
ids := make([]string, 0, len(depIDs))
84+
for _, depID := range depIDs {
85+
if newID, ok := s.idMap[depID]; ok {
86+
ids = append(ids, newID)
87+
}
88+
}
89+
return ids
90+
}

pkg/assemble/cdx/util.go

Lines changed: 123 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,16 @@ package cdx
1818

1919
import (
2020
"context"
21+
"encoding/json"
2122
"fmt"
2223
"os"
24+
"reflect"
2325
"time"
2426

2527
cydx "github.com/CycloneDX/cyclonedx-go"
2628
"github.com/google/uuid"
2729
"github.com/interlynk-io/sbomasm/pkg/detect"
2830
"github.com/interlynk-io/sbomasm/pkg/logger"
29-
"github.com/mitchellh/copystructure"
3031
"github.com/samber/lo"
3132
"sigs.k8s.io/release-utils/version"
3233
)
@@ -55,21 +56,88 @@ func newBomRef() string {
5556
}
5657

5758
func cloneComp(c *cydx.Component) (*cydx.Component, error) {
58-
compCopy, err := copystructure.Copy(c)
59+
var newComp cydx.Component
60+
61+
// Marshal the original component to JSON
62+
b, err := json.Marshal(c)
5963
if err != nil {
6064
return nil, err
6165
}
6266

63-
return compCopy.(*cydx.Component), nil
67+
// Unmarshal into a map[string]interface{} to perform cleanup
68+
var tempMap map[string]interface{}
69+
if err := json.Unmarshal(b, &tempMap); err != nil {
70+
return nil, err
71+
}
72+
73+
// Remove empty fields recursively
74+
cleanedUpMap := removeEmptyFields(tempMap)
75+
76+
// Marshal the cleaned-up map back to JSON
77+
cleanedUpBytes, err := json.Marshal(cleanedUpMap)
78+
if err != nil {
79+
return nil, err
80+
}
81+
82+
// Unmarshal the cleaned-up JSON back into a cydx.Component struct
83+
if err := json.Unmarshal(cleanedUpBytes, &newComp); err != nil {
84+
return nil, err
85+
}
86+
87+
return &newComp, nil
6488
}
6589

6690
func cloneService(s *cydx.Service) (*cydx.Service, error) {
67-
serviceCopy, err := copystructure.Copy(s)
91+
var newService cydx.Service
92+
b, err := json.Marshal(s)
6893
if err != nil {
6994
return nil, err
7095
}
96+
json.Unmarshal(b, &newService)
97+
return &newService, nil
98+
}
7199

72-
return serviceCopy.(*cydx.Service), nil
100+
// Recursive function to remove empty fields, including empty objects and arrays
101+
func removeEmptyFields(data interface{}) interface{} {
102+
switch v := data.(type) {
103+
case map[string]interface{}:
104+
// Loop through map and remove empty fields
105+
for key, value := range v {
106+
v[key] = removeEmptyFields(value)
107+
// Remove empty maps and slices
108+
if isEmptyValue(v[key]) {
109+
delete(v, key)
110+
}
111+
}
112+
case []interface{}:
113+
// Process arrays
114+
var newArray []interface{}
115+
for _, item := range v {
116+
item = removeEmptyFields(item)
117+
if !isEmptyValue(item) {
118+
newArray = append(newArray, item)
119+
}
120+
}
121+
return newArray
122+
}
123+
return data
124+
}
125+
126+
// Helper function to determine if a value is considered "empty"
127+
func isEmptyValue(v interface{}) bool {
128+
if v == nil {
129+
return true
130+
}
131+
val := reflect.ValueOf(v)
132+
switch val.Kind() {
133+
case reflect.Array, reflect.Slice, reflect.Map:
134+
return val.Len() == 0
135+
case reflect.Struct:
136+
return reflect.DeepEqual(v, reflect.Zero(val.Type()).Interface())
137+
case reflect.String:
138+
return v == ""
139+
}
140+
return false
73141
}
74142

75143
func loadBom(ctx context.Context, path string) (*cydx.BOM, error) {
@@ -117,39 +185,42 @@ func utcNowTime() string {
117185
return locationTime.Format(time.RFC3339)
118186
}
119187

120-
func toolInfo(name, version, desc, sName, sUrl, sEmail, sLicense string) *cydx.Component {
121-
return &cydx.Component{
188+
func buildToolList(in []*cydx.BOM) *cydx.ToolsChoice {
189+
tools := cydx.ToolsChoice{}
190+
191+
tools.Services = &[]cydx.Service{}
192+
tools.Components = &[]cydx.Component{}
193+
194+
*tools.Components = append(*tools.Components, cydx.Component{
122195
Type: cydx.ComponentTypeApplication,
123-
Name: name,
124-
Version: version,
125-
Description: desc,
196+
Name: "sbomasm",
197+
Version: version.GetVersionInfo().GitVersion,
198+
Description: "Assembler & Editor for your sboms",
126199
Supplier: &cydx.OrganizationalEntity{
127-
Name: sName,
128-
URL: &[]string{sUrl},
129-
Contact: &[]cydx.OrganizationalContact{{Email: sEmail}},
200+
Name: "Interlynk",
201+
URL: &[]string{"https://interlynk.io"},
202+
Contact: &[]cydx.OrganizationalContact{{Email: "support@interlynk.io"}},
130203
},
131204
Licenses: &cydx.Licenses{
132205
{
133206
License: &cydx.License{
134-
ID: sLicense,
207+
ID: "Apache-2.0",
135208
},
136209
},
137210
},
138-
}
139-
}
140-
141-
func buildToolList(in []*cydx.BOM) *cydx.ToolsChoice {
142-
tools := cydx.ToolsChoice{}
143-
144-
tools.Services = &[]cydx.Service{}
145-
tools.Components = &[]cydx.Component{}
146-
147-
*tools.Components = append(*tools.Components, *toolInfo("sbomasm", version.GetVersionInfo().GitVersion, "Assembler & Editor for your sboms", "Interlynk", "https://interlynk.io", "support@interlynk.io", "Apache-2.0"))
211+
})
148212

149213
for _, bom := range in {
150214
if bom.Metadata != nil && bom.Metadata.Tools != nil {
151215
for _, tool := range *bom.Metadata.Tools.Tools {
152-
*tools.Components = append(*tools.Components, *toolInfo(tool.Name, tool.Version, "", tool.Vendor, "", "", ""))
216+
*tools.Components = append(*tools.Components, cydx.Component{
217+
Type: cydx.ComponentTypeApplication,
218+
Name: tool.Name,
219+
Version: tool.Version,
220+
Supplier: &cydx.OrganizationalEntity{
221+
Name: tool.Vendor,
222+
},
223+
})
153224
}
154225
}
155226

@@ -168,29 +239,47 @@ func buildToolList(in []*cydx.BOM) *cydx.ToolsChoice {
168239
}
169240
}
170241

242+
uniqTools := lo.UniqBy(*tools.Components, func(c cydx.Component) string {
243+
return fmt.Sprintf("%s-%s", c.Name, c.Version)
244+
})
245+
246+
uniqServices := lo.UniqBy(*tools.Services, func(s cydx.Service) string {
247+
return fmt.Sprintf("%s-%s", s.Name, s.Version)
248+
})
249+
250+
tools.Components = &uniqTools
251+
tools.Services = &uniqServices
252+
171253
return &tools
172254
}
173255

174-
func buildComponentList(in []*cydx.BOM, cs *ComponentService) []cydx.Component {
175-
return lo.Flatten(lo.Map(in, func(bom *cydx.BOM, _ int) []cydx.Component {
176-
newComps := []cydx.Component{}
256+
func buildComponentList(in []*cydx.BOM, cs *uniqueComponentService) []cydx.Component {
257+
finalList := []cydx.Component{}
258+
259+
for _, bom := range in {
177260
for _, comp := range lo.FromPtr(bom.Components) {
178-
newComps = append(newComps, *cs.StoreAndCloneWithNewID(&comp))
261+
newComp, duplicate := cs.StoreAndCloneWithNewID(&comp)
262+
if !duplicate {
263+
finalList = append(finalList, *newComp)
264+
}
179265
}
180-
return newComps
181-
}))
266+
}
267+
return finalList
182268
}
183269

184-
func buildPrimaryComponentList(in []*cydx.BOM, cs *ComponentService) []cydx.Component {
270+
func buildPrimaryComponentList(in []*cydx.BOM, cs *uniqueComponentService) []cydx.Component {
185271
return lo.Map(in, func(bom *cydx.BOM, _ int) cydx.Component {
186272
if bom.Metadata != nil && bom.Metadata.Component != nil {
187-
return *cs.StoreAndCloneWithNewID(bom.Metadata.Component)
273+
newComp, duplicate := cs.StoreAndCloneWithNewID(bom.Metadata.Component)
274+
if !duplicate {
275+
return *newComp
276+
}
188277
}
189278
return cydx.Component{}
190279
})
191280
}
192281

193-
func buildDependencyList(in []*cydx.BOM, cs *ComponentService) []cydx.Dependency {
282+
func buildDependencyList(in []*cydx.BOM, cs *uniqueComponentService) []cydx.Dependency {
194283
return lo.Flatten(lo.Map(in, func(bom *cydx.BOM, _ int) []cydx.Dependency {
195284
newDeps := []cydx.Dependency{}
196285
for _, dep := range lo.FromPtr(bom.Dependencies) {

0 commit comments

Comments
 (0)