Skip to content

Commit 888c20a

Browse files
author
Marlon Pina Tojal
committed
initial commit
1 parent a291100 commit 888c20a

File tree

7 files changed

+347
-8
lines changed

7 files changed

+347
-8
lines changed

.github/workflows/release.yml

+18-8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ on:
44
release:
55
types: [published]
66

7+
permissions:
8+
contents: write
9+
710
jobs:
811
docker:
912
runs-on: ubuntu-latest
@@ -20,20 +23,27 @@ jobs:
2023
with:
2124
push: true
2225
platforms: linux/arm64/v8,linux/amd64
23-
tags: fnxpt/cyclonedx-merge:latest
26+
tags: fnxpt/cyclonedx-merge:latest,fnxpt/cyclonedx-merge:${{ github.ref_name }}
2427
build:
2528
runs-on: ubuntu-latest
29+
strategy:
30+
matrix:
31+
# build and publish in parallel: linux/386, linux/amd64, linux/arm64, windows/386, windows/amd64, darwin/amd64, darwin/arm64
32+
goos: [linux, darwin]
33+
goarch: [amd64, arm64]
2634
steps:
2735
- uses: actions/checkout@v3
28-
- name: Generate build files
29-
uses: thatisuday/go-cross-build@v1
36+
- uses: actions/setup-go@v4
3037
with:
31-
platforms: 'linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64'
32-
name: 'cyclonedx-merge'
33-
compress: 'true'
34-
dest: 'dist'
38+
go-version: '^1.21.1' # The Go version to download (if necessary) and use.
39+
- run: go build -o dist/cyclonedx-merge-${{ matrix.goos }}-${{ matrix.goarch }} .
40+
env:
41+
GOOS: ${{ matrix.goos }}
42+
GOARCH: ${{ matrix.goarch }}
43+
3544
- name: Release
3645
uses: fnkr/github-action-ghr@v1
3746
env:
38-
GHR_PATH: dist/
47+
GHR_COMPRESS: gz
48+
GHR_PATH: dist/cyclonedx-merge-${{ matrix.goos }}-${{ matrix.goarch }}
3949
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@
1919

2020
# Go workspace file
2121
go.work
22+
23+
#binary
24+
cyclonedx-merge

Dockerfile

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
FROM golang:latest AS builder
2+
WORKDIR /go/src/
3+
ENV CGO_ENABLED=0
4+
5+
ADD . .
6+
7+
RUN go build -ldflags="-s -w" -o release/cyclonedx-merge .
8+
9+
FROM scratch AS runtime
10+
WORKDIR /
11+
12+
COPY --from=builder /go/src/release/cyclonedx-merge .
13+
14+
ENTRYPOINT [ "/cyclonedx-merge" ]

README.md

+8
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,10 @@
11
# cyclonedx-merge
22
Tool to merge cyclonedx files
3+
4+
# Run
5+
6+
To perform a merge on a specific directory
7+
```docker run -v `pwd`/sbom/:/sbom/ fnxpt/sbommerge:latest --dir /sbom/ > output.json```
8+
9+
## Assumptions
10+

go.mod

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module cyclonedx-merge
2+
3+
go 1.21.1
4+
5+
require github.com/CycloneDX/cyclonedx-go v0.7.2

go.sum

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
github.com/CycloneDX/cyclonedx-go v0.7.2 h1:kKQ0t1dPOlugSIYVOMiMtFqeXI2wp/f5DBIdfux8gnQ=
2+
github.com/CycloneDX/cyclonedx-go v0.7.2/go.mod h1:K2bA+324+Og0X84fA8HhN2X066K7Bxz4rpMQ4ZhjtSk=
3+
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
4+
github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
5+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
6+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7+
github.com/fnxpt/cyclonedx-go v0.0.0-20231028181336-dccc3992d8ff h1:Ano0LzN28iaork+wTeuGR+A2okCGJGsBHIb2NnJrID8=
8+
github.com/fnxpt/cyclonedx-go v0.0.0-20231028181336-dccc3992d8ff/go.mod h1:K2bA+324+Og0X84fA8HhN2X066K7Bxz4rpMQ4ZhjtSk=
9+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
10+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
11+
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
12+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
13+
github.com/terminalstatic/go-xsd-validate v0.1.5 h1:RqpJnf6HGE2CB/lZB1A8BYguk8uRtcvYAPLCF15qguo=
14+
github.com/terminalstatic/go-xsd-validate v0.1.5/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw=
15+
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
16+
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
17+
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
18+
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
19+
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
20+
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
21+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
22+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

main.go

+277
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"io"
7+
"os"
8+
"slices"
9+
"strings"
10+
"time"
11+
12+
"github.com/CycloneDX/cyclonedx-go"
13+
)
14+
15+
var version = "0.0.1"
16+
17+
var nested = false
18+
var sbom *cyclonedx.BOM
19+
var outputFormat = cyclonedx.BOMFileFormatJSON
20+
var output = os.Stdout
21+
22+
func main() {
23+
parseArguments()
24+
}
25+
26+
func showHelpMenu() {
27+
fmt.Println("usage: cyclonedx-merge [options]")
28+
fmt.Println("options:")
29+
os.Exit(0)
30+
}
31+
32+
func parseArguments() {
33+
flag.CommandLine = flag.NewFlagSet("", flag.ExitOnError)
34+
35+
flag.Usage = func() {
36+
showHelpMenu()
37+
flag.PrintDefaults()
38+
}
39+
40+
flag.BoolVar(&nested, "nested", false, "nested merge")
41+
flag.Func("file", "merges file", fileMerge)
42+
flag.Func("dir", "merges files in directory", dirMerge)
43+
flag.Func("format", "output format - json/xml (default: json)", func(value string) error {
44+
switch value {
45+
case "json":
46+
outputFormat = cyclonedx.BOMFileFormatJSON
47+
case "xml":
48+
outputFormat = cyclonedx.BOMFileFormatXML
49+
default:
50+
return fmt.Errorf("invalid output format")
51+
}
52+
return nil
53+
})
54+
flag.Func("output", "output file (default: stdout)", func(value string) error {
55+
file, err := os.Create(value)
56+
57+
if err != nil {
58+
fmt.Printf("unable to create file %s\n", value)
59+
return err
60+
}
61+
output = file
62+
return nil
63+
})
64+
65+
fillSBOM()
66+
flag.Parse()
67+
postSBOM()
68+
writeSBOM()
69+
}
70+
71+
func fileMerge(value string) error {
72+
// fmt.Printf("Processing file %s\n", value)
73+
if _, err := os.Stat(value); os.IsNotExist(err) {
74+
fmt.Printf("file %s doesn't exist\n", value)
75+
return err
76+
}
77+
78+
file, err := os.Open(value)
79+
80+
if err != nil {
81+
fmt.Printf("unable to open file %s\n", value)
82+
return err
83+
}
84+
85+
bom, err := parseSBOM(file)
86+
87+
if err != nil {
88+
fmt.Printf("unable to parse file %s\n", value)
89+
return err
90+
}
91+
92+
mergeSBOM(bom)
93+
94+
return nil
95+
}
96+
97+
var topDependencies = make([]string, 0)
98+
99+
func mergeSBOM(value *cyclonedx.BOM) {
100+
var prefix string
101+
102+
if value.Metadata != nil {
103+
if value.Metadata.Component != nil {
104+
topComponents := []cyclonedx.Component{*value.Metadata.Component}
105+
if nested {
106+
prefix = fmt.Sprintf("%s|", value.Metadata.Component.BOMRef)
107+
}
108+
109+
addIfNew(sbom.Components, &topComponents, "")
110+
topDependencies = append(topDependencies, value.Metadata.Component.BOMRef)
111+
112+
if value.Metadata.Component.Components != nil {
113+
addIfNew(sbom.Components, value.Metadata.Component.Components, prefix)
114+
}
115+
}
116+
}
117+
118+
addIfNew(sbom.Components, value.Components, prefix)
119+
// addIfNew(sbom.Services, value.Services, prefix)
120+
// addIfNew(sbom.ExternalReferences, value.ExternalReferences, prefix)
121+
122+
addIfNewMap(value.Dependencies, prefix)
123+
// addIfNew(sbom.Compositions, value.Compositions)
124+
// addIfNew(sbom.Properties, value.Properties)
125+
// addIfNew(sbom.Vulnerabilities, value.Vulnerabilities, prefix)
126+
// addIfNew(sbom.Annotations, value.Annotations, prefix)
127+
}
128+
129+
func fillSBOM() {
130+
131+
sbom = cyclonedx.NewBOM()
132+
sbom.Metadata = &cyclonedx.Metadata{
133+
Tools: &[]cyclonedx.Tool{{
134+
Vendor: "fnxpt",
135+
Name: "cyclonedx-merge",
136+
Version: version,
137+
}},
138+
Timestamp: time.Now().String(), //TODO: RIGHT FORMAT
139+
Component: &cyclonedx.Component{
140+
BOMRef: "root",
141+
Name: "root",
142+
Type: cyclonedx.ComponentTypeApplication,
143+
},
144+
}
145+
146+
annotations := make([]cyclonedx.Annotation, 0)
147+
components := make([]cyclonedx.Component, 0)
148+
compositions := make([]cyclonedx.Composition, 0)
149+
dependencies := make([]cyclonedx.Dependency, 0)
150+
externalReferences := make([]cyclonedx.ExternalReference, 0)
151+
properties := make([]cyclonedx.Property, 0)
152+
services := make([]cyclonedx.Service, 0)
153+
vulnerabilities := make([]cyclonedx.Vulnerability, 0)
154+
155+
sbom.Annotations = &annotations
156+
sbom.Components = &components
157+
sbom.Compositions = &compositions
158+
sbom.Dependencies = &dependencies
159+
sbom.ExternalReferences = &externalReferences
160+
sbom.Properties = &properties
161+
sbom.Services = &services
162+
sbom.Vulnerabilities = &vulnerabilities
163+
164+
}
165+
166+
func postSBOM() {
167+
168+
for _, item := range tmp {
169+
*sbom.Dependencies = append(*sbom.Dependencies, item)
170+
}
171+
*sbom.Dependencies = append(*sbom.Dependencies, cyclonedx.Dependency{
172+
Ref: "root",
173+
Dependencies: &topDependencies,
174+
})
175+
176+
}
177+
func writeSBOM() {
178+
179+
encoder := cyclonedx.NewBOMEncoder(output, outputFormat)
180+
encoder.Encode(sbom)
181+
}
182+
183+
func parseSBOM(input io.Reader) (*cyclonedx.BOM, error) {
184+
185+
bom := &cyclonedx.BOM{}
186+
decoder := cyclonedx.NewBOMDecoder(input, cyclonedx.BOMFileFormatJSON)
187+
err := decoder.Decode(bom)
188+
189+
if err != nil {
190+
return nil, err
191+
}
192+
193+
return bom, err
194+
}
195+
196+
func dirMerge(value string) error {
197+
if _, err := os.Stat(value); os.IsNotExist(err) {
198+
fmt.Printf("directory %s doesn't exist\n", value)
199+
return err
200+
}
201+
202+
entries, err := os.ReadDir(value)
203+
if err != nil {
204+
fmt.Printf("unable to read directory %s\n", value)
205+
return err
206+
}
207+
208+
for _, e := range entries {
209+
if strings.HasSuffix(e.Name(), ".json") || strings.HasSuffix(e.Name(), ".xml") {
210+
fileMerge(fmt.Sprintf("%s/%s", value, e.Name()))
211+
}
212+
}
213+
214+
return nil
215+
}
216+
217+
var tmp = make(map[string]cyclonedx.Dependency)
218+
219+
func addIfNewMap(input *[]cyclonedx.Dependency, prefix string) {
220+
if input != nil {
221+
222+
for _, item := range *input {
223+
key := item.Ref
224+
225+
if nested && prefix[:len(prefix)-1] != item.Ref {
226+
key = fmt.Sprintf("%s%s", prefix, key)
227+
}
228+
if _, ok := tmp[key]; ok {
229+
for _, dependency := range *item.Dependencies {
230+
dependency = fmt.Sprintf("%s%s", prefix, dependency)
231+
if !slices.Contains(*tmp[key].Dependencies, dependency) {
232+
*tmp[item.Ref].Dependencies = append(*tmp[item.Ref].Dependencies, dependency)
233+
}
234+
}
235+
} else {
236+
item.Ref = key
237+
//TODO: PREFIX DEPENDENCIES
238+
if len(prefix) > 0 {
239+
tmp[key] = cyclonedx.Dependency{
240+
Ref: key,
241+
Dependencies: &[]string{},
242+
}
243+
if item.Dependencies != nil {
244+
for _, dep := range *item.Dependencies {
245+
dependency := fmt.Sprintf("%s%s", prefix, dep)
246+
*tmp[key].Dependencies = append(*tmp[key].Dependencies, dependency)
247+
}
248+
}
249+
} else {
250+
tmp[key] = item
251+
}
252+
}
253+
}
254+
}
255+
}
256+
func addIfNew(items *[]cyclonedx.Component, input *[]cyclonedx.Component, prefix string) {
257+
if items != nil && input != nil {
258+
for _, item := range *input {
259+
item.BOMRef = fmt.Sprintf("%s%s", prefix, item.BOMRef)
260+
if !has(items, &item) {
261+
*items = append(*items, item)
262+
}
263+
}
264+
}
265+
}
266+
267+
func has(items *[]cyclonedx.Component, input *cyclonedx.Component) bool {
268+
if items != nil {
269+
for _, item := range *items {
270+
if item.BOMRef == input.BOMRef {
271+
return true
272+
}
273+
}
274+
}
275+
276+
return false
277+
}

0 commit comments

Comments
 (0)