Skip to content

Commit 2826fe2

Browse files
authoredSep 25, 2022
feat: add support for encoding to older spec versions (#51)
* feat: add support for encoding to older spec versions Signed-off-by: nscuro <nscuro@protonmail.com> * fix: ignore generated sources in license check Signed-off-by: nscuro <nscuro@protonmail.com> Signed-off-by: nscuro <nscuro@protonmail.com>
1 parent c4cecac commit 2826fe2

21 files changed

+1763
-77
lines changed
 

‎.licenserc.yml

+6-5
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@ header:
33
spdx-id: Apache-2.0
44
copyright-owner: OWASP Foundation
55
paths-ignore:
6+
- "**/*.md"
7+
- "**/go.mod"
8+
- "**/go.sum"
9+
- "**/testdata/**"
610
- ".github/**"
711
- ".gitignore"
812
- ".gitpod.*"
913
- ".golangci.yml"
1014
- ".goreleaser.yml"
1115
- ".licenserc.yml"
12-
- "**/*.md"
13-
- "**/go.mod"
14-
- "**/go.sum"
15-
- "**/testdata/**"
1616
- "CODEOWNERS"
1717
- "LICENSE"
1818
- "Makefile"
19-
- "NOTICE"
19+
- "NOTICE"
20+
- "cyclonedx_string.go"

‎Makefile

+4
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,9 @@ clean:
1010
go clean
1111
.PHONY: clean
1212

13+
generate:
14+
go generate
15+
.PHONY: generate
16+
1317
all: clean build test
1418
.PHONY: all

‎README.md

+8-5
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,18 @@ Also, checkout the [`examples`](./example_test.go) to get an idea of how this li
2828

2929
| cyclonedx-go versions | Supported Go versions | Supported CycloneDX spec |
3030
|:---------------------:|:---------------------:|:------------------------:|
31-
| < v0.4.0 | 1.14+ | 1.2 |
32-
| == v0.4.0 | 1.14+ | 1.3 |
33-
| >= v0.5.0 | 1.15+ | 1.4 |
31+
| < v0.4.0 | 1.14+ | 1.2 |
32+
| == v0.4.0 | 1.14+ | 1.3 |
33+
| >= v0.5.0 | 1.15+ | 1.4 |
34+
| >= v0.7.0 | 1.15+ | 1.0-1.4 |
3435

3536
We're aiming to support all [officially supported](https://golang.org/doc/devel/release.html#policy) Go versions, plus
3637
an additional older version.
3738

38-
This library will only support the latest version of the CycloneDX specification. While it's generally possible to
39-
*read* BOMs of an older spec, *writing* will exclusively produce BOMs conforming to the latest supported spec.
39+
Prior to v0.7.0, this library only supported the latest version of the CycloneDX specification. While it is generally
40+
possible to *read* BOMs of an older spec, *writing* would exclusively produce BOMs conforming to the latest supported spec.
41+
42+
Starting with v0.7.0, writing BOMs conforming to all previous version of the spec is also possible.
4043

4144
## Copyright & License
4245

‎copy.go

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// This file is part of CycloneDX Go
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the “License”);
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an “AS IS” BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
// SPDX-License-Identifier: Apache-2.0
16+
// Copyright (c) OWASP Foundation. All Rights Reserved.
17+
18+
package cyclonedx
19+
20+
import (
21+
"bytes"
22+
"encoding/gob"
23+
"fmt"
24+
)
25+
26+
// copy creates a deep copy of the BOM in a given destination.
27+
// Copying is currently done be encoding and decoding the BOM struct using the gop.
28+
// In the future we may choose to switch to a more efficient strategy,
29+
// and consider to export this API.
30+
func (b BOM) copy(dst *BOM) error {
31+
buf := bytes.Buffer{}
32+
err := gob.NewEncoder(&buf).Encode(b)
33+
if err != nil {
34+
return fmt.Errorf("failed to encode bom: %w", err)
35+
}
36+
37+
err = gob.NewDecoder(&buf).Decode(dst)
38+
if err != nil {
39+
return fmt.Errorf("failed to decode bom: %w", err)
40+
}
41+
42+
return nil
43+
}

‎cyclonedx.go

+80-9
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,10 @@ import (
2626
"regexp"
2727
)
2828

29+
//go:generate stringer -linecomment -output cyclonedx_string.go -type SpecVersion
30+
2931
const (
30-
BOMFormat = "CycloneDX"
31-
defaultVersion = 1
32-
SpecVersion = "1.4"
33-
XMLNamespace = "http://cyclonedx.org/schema/bom/1.4"
32+
BOMFormat = "CycloneDX"
3433
)
3534

3635
type Advisory struct {
@@ -61,8 +60,8 @@ type BOM struct {
6160
XMLNS string `json:"-" xml:"xmlns,attr"`
6261

6362
// JSON specific fields
64-
BOMFormat string `json:"bomFormat" xml:"-"`
65-
SpecVersion string `json:"specVersion" xml:"-"`
63+
BOMFormat string `json:"bomFormat" xml:"-"`
64+
SpecVersion SpecVersion `json:"specVersion" xml:"-"`
6665

6766
SerialNumber string `json:"serialNumber,omitempty" xml:"serialNumber,attr,omitempty"`
6867
Version int `json:"version" xml:"version,attr"`
@@ -78,10 +77,10 @@ type BOM struct {
7877

7978
func NewBOM() *BOM {
8079
return &BOM{
81-
XMLNS: XMLNamespace,
80+
XMLNS: xmlNamespaces[SpecVersion1_4],
8281
BOMFormat: BOMFormat,
83-
SpecVersion: SpecVersion,
84-
Version: defaultVersion,
82+
SpecVersion: SpecVersion1_4,
83+
Version: 1,
8584
}
8685
}
8786

@@ -590,6 +589,70 @@ type Source struct {
590589
URL string `json:"url,omitempty" xml:"url,omitempty"`
591590
}
592591

592+
type SpecVersion int
593+
594+
const (
595+
SpecVersion1_0 SpecVersion = iota + 1 // 1.0
596+
SpecVersion1_1 // 1.1
597+
SpecVersion1_2 // 1.2
598+
SpecVersion1_3 // 1.3
599+
SpecVersion1_4 // 1.4
600+
)
601+
602+
func (sv SpecVersion) MarshalJSON() ([]byte, error) {
603+
return json.Marshal(sv.String())
604+
}
605+
606+
func (sv *SpecVersion) UnmarshalJSON(bytes []byte) error {
607+
var v string
608+
err := json.Unmarshal(bytes, &v)
609+
if err != nil {
610+
return err
611+
}
612+
613+
switch v {
614+
case SpecVersion1_0.String():
615+
*sv = SpecVersion1_0
616+
case SpecVersion1_1.String():
617+
*sv = SpecVersion1_1
618+
case SpecVersion1_2.String():
619+
*sv = SpecVersion1_2
620+
case SpecVersion1_3.String():
621+
*sv = SpecVersion1_3
622+
case SpecVersion1_4.String():
623+
*sv = SpecVersion1_4
624+
}
625+
626+
return nil
627+
}
628+
629+
func (sv SpecVersion) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
630+
return e.EncodeElement(sv.String(), start)
631+
}
632+
633+
func (sv *SpecVersion) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
634+
var v string
635+
err := d.DecodeElement(&v, &start)
636+
if err != nil {
637+
return err
638+
}
639+
640+
switch v {
641+
case SpecVersion1_0.String():
642+
*sv = SpecVersion1_0
643+
case SpecVersion1_1.String():
644+
*sv = SpecVersion1_1
645+
case SpecVersion1_2.String():
646+
*sv = SpecVersion1_2
647+
case SpecVersion1_3.String():
648+
*sv = SpecVersion1_3
649+
case SpecVersion1_4.String():
650+
*sv = SpecVersion1_4
651+
}
652+
653+
return nil
654+
}
655+
593656
type SWID struct {
594657
Text *AttachedText `json:"text,omitempty" xml:"text,omitempty"`
595658
URL string `json:"url,omitempty" xml:"url,attr,omitempty"`
@@ -657,3 +720,11 @@ const (
657720
VulnerabilityStatusAffected VulnerabilityStatus = "affected"
658721
VulnerabilityStatusNotAffected VulnerabilityStatus = "unaffected"
659722
)
723+
724+
var xmlNamespaces = map[SpecVersion]string{
725+
SpecVersion1_0: "http://cyclonedx.org/schema/bom/1.0",
726+
SpecVersion1_1: "http://cyclonedx.org/schema/bom/1.1",
727+
SpecVersion1_2: "http://cyclonedx.org/schema/bom/1.2",
728+
SpecVersion1_3: "http://cyclonedx.org/schema/bom/1.3",
729+
SpecVersion1_4: "http://cyclonedx.org/schema/bom/1.4",
730+
}

‎cyclonedx_string.go

+28
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎cyclonedx_test.go

+21
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,19 @@ package cyclonedx
2020
import (
2121
"encoding/json"
2222
"encoding/xml"
23+
"fmt"
24+
"os/exec"
25+
"strings"
2326
"testing"
2427

28+
"github.com/bradleyjkemp/cupaloy/v2"
2529
"github.com/stretchr/testify/assert"
2630
"github.com/stretchr/testify/require"
2731
)
2832

33+
var snapShooter = cupaloy.NewDefaultConfig().
34+
WithOptions(cupaloy.SnapshotSubdirectory("./testdata/snapshots"))
35+
2936
func TestBool(t *testing.T) {
3037
assert.Equal(t, true, *Bool(true))
3138
assert.Equal(t, false, *Bool(false))
@@ -228,3 +235,17 @@ func TestVulnerability_Properties(t *testing.T) {
228235
// EXPECT
229236
assert.Equal(t, 0, len(*vuln.Properties))
230237
}
238+
239+
func assertValidBOM(t *testing.T, bomFilePath string, version SpecVersion) {
240+
inputFormat := "xml"
241+
if strings.HasSuffix(bomFilePath, ".json") {
242+
inputFormat = "json"
243+
}
244+
inputVersion := fmt.Sprintf("v%s", strings.ReplaceAll(version.String(), ".", "_"))
245+
valCmd := exec.Command("cyclonedx", "validate", "--input-file", bomFilePath, "--input-format", inputFormat, "--input-version", inputVersion, "--fail-on-errors")
246+
valOut, err := valCmd.CombinedOutput()
247+
if !assert.NoError(t, err) {
248+
// Provide some context when test is failing
249+
fmt.Printf("validation error: %s\n", string(valOut))
250+
}
251+
}

‎decode.go

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type jsonBOMDecoder struct {
3838
reader io.Reader
3939
}
4040

41+
// Decode implements the BOMDecoder interface.
4142
func (j jsonBOMDecoder) Decode(bom *BOM) error {
4243
return json.NewDecoder(j.reader).Decode(bom)
4344
}
@@ -46,6 +47,7 @@ type xmlBOMDecoder struct {
4647
reader io.Reader
4748
}
4849

50+
// Decode implements the BOMDecoder interface.
4951
func (x xmlBOMDecoder) Decode(bom *BOM) error {
5052
return xml.NewDecoder(x.reader).Decode(bom)
5153
}

‎downgrade.go

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// This file is part of CycloneDX Go
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the “License”);
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an “AS IS” BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
// SPDX-License-Identifier: Apache-2.0
16+
// Copyright (c) OWASP Foundation. All Rights Reserved.
17+
18+
package cyclonedx
19+
20+
import "fmt"
21+
22+
// downgrade "downgrades" the BOM to a given version of the specification.
23+
// Downgrading works by successively removing (or changing) fields introduced in later specification versions.
24+
// This procedure has been adapted from the .NET implementation:
25+
// https://github.com/CycloneDX/cyclonedx-dotnet-library/blob/v5.2.2/src/CycloneDX.Core/BomUtils.cs#L60
26+
func (b *BOM) downgrade(version SpecVersion) error {
27+
if version < SpecVersion1_1 {
28+
b.SerialNumber = ""
29+
b.ExternalReferences = nil
30+
forEachComponent(b.Components, func(c *Component) {
31+
c.BOMRef = ""
32+
c.ExternalReferences = nil
33+
if c.Licenses != nil {
34+
// Keep track of licenses that are still valid
35+
// after removal of unsupported fields.
36+
validLicenses := make(Licenses, 0)
37+
38+
for i := range *c.Licenses {
39+
license := &(*c.Licenses)[i]
40+
if license.License != nil {
41+
license.License.Text = nil
42+
license.License.URL = ""
43+
}
44+
license.Expression = ""
45+
if license.License != nil {
46+
validLicenses = append(validLicenses, *license)
47+
}
48+
}
49+
50+
// Remove the licenses node entirely if no valid licenses
51+
// are left. This avoids empty (thus invalid) <licenses> tags in XML.
52+
if len(validLicenses) == 0 {
53+
c.Licenses = nil
54+
} else {
55+
c.Licenses = &validLicenses
56+
}
57+
}
58+
if c.Modified == nil {
59+
c.Modified = Bool(false)
60+
}
61+
c.Pedigree = nil
62+
})
63+
}
64+
65+
if version < SpecVersion1_2 {
66+
b.Metadata = nil
67+
b.Dependencies = nil
68+
b.Services = nil
69+
forEachComponent(b.Components, func(c *Component) {
70+
c.Author = ""
71+
c.MIMEType = ""
72+
c.Supplier = nil
73+
c.SWID = nil
74+
if c.Pedigree != nil {
75+
c.Pedigree.Patches = nil
76+
}
77+
})
78+
}
79+
80+
if version < SpecVersion1_3 {
81+
b.Compositions = nil
82+
if b.Metadata != nil {
83+
b.Metadata.Licenses = nil
84+
b.Metadata.Properties = nil
85+
}
86+
forEachComponent(b.Components, func(c *Component) {
87+
c.Evidence = nil
88+
c.Properties = nil
89+
if c.ExternalReferences != nil {
90+
for i := range *c.ExternalReferences {
91+
(*c.ExternalReferences)[i].Hashes = nil
92+
}
93+
}
94+
})
95+
forEachService(b.Services, func(s *Service) {
96+
s.Properties = nil
97+
if s.ExternalReferences != nil {
98+
for i := range *s.ExternalReferences {
99+
(*s.ExternalReferences)[i].Hashes = nil
100+
}
101+
}
102+
})
103+
}
104+
105+
if version < SpecVersion1_4 {
106+
if b.Metadata != nil && b.Metadata.Tools != nil {
107+
for i := range *b.Metadata.Tools {
108+
(*b.Metadata.Tools)[i].ExternalReferences = nil
109+
}
110+
}
111+
forEachComponent(b.Components, func(c *Component) {
112+
c.ReleaseNotes = nil
113+
if c.Version == "" {
114+
c.Version = "0.0.0"
115+
}
116+
})
117+
forEachService(b.Services, func(s *Service) {
118+
s.ReleaseNotes = nil
119+
})
120+
b.Vulnerabilities = nil
121+
}
122+
123+
b.SpecVersion = version
124+
b.XMLNS = xmlNamespaces[version]
125+
126+
return nil
127+
}
128+
129+
func (b *BOM) copyAndDowngrade(version SpecVersion) (*BOM, error) {
130+
var bomCopy BOM
131+
err := b.copy(&bomCopy)
132+
if err != nil {
133+
return nil, fmt.Errorf("failed to copy bom: %w", err)
134+
}
135+
136+
err = bomCopy.downgrade(version)
137+
return &bomCopy, err
138+
}
139+
140+
func forEachComponent(components *[]Component, f func(c *Component)) {
141+
if components == nil || len(*components) == 0 {
142+
return
143+
}
144+
145+
for i := range *components {
146+
component := &(*components)[i]
147+
f(component)
148+
forEachComponent(component.Components, f)
149+
if component.Pedigree != nil {
150+
forEachComponent(component.Pedigree.Ancestors, f)
151+
forEachComponent(component.Pedigree.Descendants, f)
152+
forEachComponent(component.Pedigree.Variants, f)
153+
}
154+
}
155+
}
156+
157+
func forEachService(services *[]Service, f func(s *Service)) {
158+
if services == nil || len(*services) == 0 {
159+
return
160+
}
161+
162+
for i := range *services {
163+
f(&(*services)[i])
164+
forEachService((*services)[i].Services, f)
165+
}
166+
}

‎encode.go

+44-4
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,16 @@ import (
2525
)
2626

2727
type BOMEncoder interface {
28-
Encode(*BOM) error
29-
SetPretty(bool)
28+
// Encode encodes a given BOM.
29+
Encode(bom *BOM) error
30+
31+
// EncodeVersion encodes a given BOM in a specific version of the specification.
32+
// Choosing a lower spec version than what the BOM was constructed for will result
33+
// in loss of information. The original BOM struct is guaranteed to not be modified.
34+
EncodeVersion(bom *BOM, version SpecVersion) error
35+
36+
// SetPretty toggles prettified output.
37+
SetPretty(pretty bool) BOMEncoder
3038
}
3139

3240
func NewBOMEncoder(writer io.Writer, format BOMFileFormat) BOMEncoder {
@@ -41,23 +49,42 @@ type jsonBOMEncoder struct {
4149
pretty bool
4250
}
4351

52+
// Encode implements the BOMEncoder interface.
4453
func (j jsonBOMEncoder) Encode(bom *BOM) error {
54+
if bom.SpecVersion < SpecVersion1_2 {
55+
return fmt.Errorf("json format is not supported for specification versions lower than 1.2")
56+
}
57+
4558
encoder := json.NewEncoder(j.writer)
4659
if j.pretty {
4760
encoder.SetIndent("", " ")
4861
}
62+
4963
return encoder.Encode(bom)
5064
}
5165

52-
func (j *jsonBOMEncoder) SetPretty(pretty bool) {
66+
// EncodeVersion implements the BOMEncoder interface.
67+
func (j jsonBOMEncoder) EncodeVersion(bom *BOM, version SpecVersion) (err error) {
68+
bom, err = bom.copyAndDowngrade(version)
69+
if err != nil {
70+
return
71+
}
72+
73+
return j.Encode(bom)
74+
}
75+
76+
// SetPretty implements the BOMEncoder interface.
77+
func (j *jsonBOMEncoder) SetPretty(pretty bool) BOMEncoder {
5378
j.pretty = pretty
79+
return j
5480
}
5581

5682
type xmlBOMEncoder struct {
5783
writer io.Writer
5884
pretty bool
5985
}
6086

87+
// Encode implements the BOMEncoder interface.
6188
func (x xmlBOMEncoder) Encode(bom *BOM) error {
6289
if _, err := fmt.Fprintf(x.writer, xml.Header); err != nil {
6390
return err
@@ -67,9 +94,22 @@ func (x xmlBOMEncoder) Encode(bom *BOM) error {
6794
if x.pretty {
6895
encoder.Indent("", " ")
6996
}
97+
7098
return encoder.Encode(bom)
7199
}
72100

73-
func (x *xmlBOMEncoder) SetPretty(pretty bool) {
101+
// EncodeVersion implements the BOMEncoder interface.
102+
func (x xmlBOMEncoder) EncodeVersion(bom *BOM, version SpecVersion) (err error) {
103+
bom, err = bom.copyAndDowngrade(version)
104+
if err != nil {
105+
return
106+
}
107+
108+
return x.Encode(bom)
109+
}
110+
111+
// SetPretty implements the BOMEncoder interface.
112+
func (x *xmlBOMEncoder) SetPretty(pretty bool) BOMEncoder {
74113
x.pretty = pretty
114+
return x
75115
}

‎encode_test.go

+85
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ package cyclonedx
1919

2020
import (
2121
"bytes"
22+
"fmt"
23+
"io"
24+
"io/ioutil"
25+
"os"
26+
"path/filepath"
2227
"testing"
2328

2429
"github.com/stretchr/testify/assert"
@@ -102,3 +107,83 @@ func TestXmlBOMEncoder_SetPretty(t *testing.T) {
102107
</metadata>
103108
</bom>`, buf.String())
104109
}
110+
111+
func TestJsonBOMEncoder_EncodeVersion(t *testing.T) {
112+
t.Run(SpecVersion1_0.String(), func(t *testing.T) {
113+
err := NewBOMEncoder(ioutil.Discard, BOMFileFormatJSON).EncodeVersion(NewBOM(), SpecVersion1_0)
114+
require.Error(t, err)
115+
require.ErrorContains(t, err, "not supported")
116+
})
117+
118+
t.Run(SpecVersion1_1.String(), func(t *testing.T) {
119+
err := NewBOMEncoder(ioutil.Discard, BOMFileFormatJSON).EncodeVersion(NewBOM(), SpecVersion1_1)
120+
require.Error(t, err)
121+
require.ErrorContains(t, err, "not supported")
122+
})
123+
124+
for _, version := range []SpecVersion{SpecVersion1_2, SpecVersion1_3, SpecVersion1_4} {
125+
t.Run(version.String(), func(t *testing.T) {
126+
// Read original BOM JSON
127+
inputFile, err := os.Open("./testdata/valid-bom.json")
128+
require.NoError(t, err)
129+
130+
// Decode BOM
131+
var bom BOM
132+
require.NoError(t, NewBOMDecoder(inputFile, BOMFileFormatJSON).Decode(&bom))
133+
inputFile.Close()
134+
135+
// Prepare encoding destinations
136+
buf := bytes.Buffer{}
137+
outputFilePath := filepath.Join(t.TempDir(), "bom.json")
138+
outputFile, err := os.Create(outputFilePath)
139+
require.NoError(t, err)
140+
141+
// Encode BOM again, for a specific version
142+
err = NewBOMEncoder(io.MultiWriter(&buf, outputFile), BOMFileFormatJSON).
143+
SetPretty(true).
144+
EncodeVersion(&bom, version)
145+
require.NoError(t, err)
146+
outputFile.Close() // Required for CLI to be able to access the file
147+
148+
// Sanity checks: BOM has to be valid
149+
assertValidBOM(t, outputFilePath, version)
150+
151+
// Compare with snapshot
152+
require.NoError(t, snapShooter.SnapshotMulti(fmt.Sprintf("%s.bom.json", version), buf.String()))
153+
})
154+
}
155+
}
156+
157+
func TestXmlBOMEncoder_EncodeVersion(t *testing.T) {
158+
for _, version := range []SpecVersion{SpecVersion1_0, SpecVersion1_1, SpecVersion1_2, SpecVersion1_3, SpecVersion1_4} {
159+
t.Run(version.String(), func(t *testing.T) {
160+
// Read original BOM JSON
161+
inputFile, err := os.Open("./testdata/valid-bom.xml")
162+
require.NoError(t, err)
163+
164+
// Decode BOM
165+
var bom BOM
166+
require.NoError(t, NewBOMDecoder(inputFile, BOMFileFormatXML).Decode(&bom))
167+
inputFile.Close()
168+
169+
// Prepare encoding destinations
170+
buf := bytes.Buffer{}
171+
outputFilePath := filepath.Join(t.TempDir(), "bom.xml")
172+
outputFile, err := os.Create(outputFilePath)
173+
require.NoError(t, err)
174+
175+
// Encode BOM again
176+
err = NewBOMEncoder(io.MultiWriter(&buf, outputFile), BOMFileFormatXML).
177+
SetPretty(true).
178+
EncodeVersion(&bom, version)
179+
require.NoError(t, err)
180+
outputFile.Close() // Required for CLI to be able to access the file
181+
182+
// Sanity checks: BOM has to be valid
183+
require.NoError(t, snapShooter.SnapshotMulti(fmt.Sprintf("%s.bom.xml", version), buf.String()))
184+
185+
// Compare with snapshot
186+
assertValidBOM(t, outputFilePath, version)
187+
})
188+
}
189+
}

‎example_test.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,10 @@ func Example_encode() {
8080
bom.Dependencies = &dependencies
8181

8282
// Encode the BOM
83-
encoder := cdx.NewBOMEncoder(os.Stdout, cdx.BOMFileFormatXML)
84-
encoder.SetPretty(true)
85-
if err := encoder.Encode(bom); err != nil {
83+
err := cdx.NewBOMEncoder(os.Stdout, cdx.BOMFileFormatXML).
84+
SetPretty(true).
85+
Encode(bom)
86+
if err != nil {
8687
panic(err)
8788
}
8889

‎roundtrip_test.go

+32-51
Original file line numberDiff line numberDiff line change
@@ -19,56 +19,48 @@ package cyclonedx
1919

2020
import (
2121
"bytes"
22-
"fmt"
2322
"io"
24-
"io/ioutil"
2523
"os"
26-
"os/exec"
2724
"path/filepath"
28-
"strings"
2925
"testing"
3026

31-
"github.com/bradleyjkemp/cupaloy/v2"
3227
"github.com/stretchr/testify/assert"
3328
"github.com/stretchr/testify/require"
3429
)
3530

36-
var roundTripSnapshotter = cupaloy.NewDefaultConfig().
37-
WithOptions(cupaloy.SnapshotSubdirectory("./testdata/snapshots"))
38-
39-
var subTestNameSlashReplacer = strings.NewReplacer("/", "_")
40-
4131
func TestRoundTripJSON(t *testing.T) {
4232
bomFilePaths, err := filepath.Glob("./testdata/*.json")
4333
require.NoError(t, err)
4434

4535
for _, bomFilePath := range bomFilePaths {
4636
t.Run(filepath.Base(bomFilePath), func(t *testing.T) {
4737
// Read original BOM JSON
48-
bomFile, err := os.Open(bomFilePath)
38+
inputFile, err := os.Open(bomFilePath)
4939
require.NoError(t, err)
5040

5141
// Decode BOM
52-
bom := new(BOM)
53-
require.NoError(t, NewBOMDecoder(bomFile, BOMFileFormatJSON).Decode(bom))
54-
bomFile.Close()
42+
var bom BOM
43+
require.NoError(t, NewBOMDecoder(inputFile, BOMFileFormatJSON).Decode(&bom))
44+
inputFile.Close()
45+
46+
// Prepare encoding destinations
47+
buf := bytes.Buffer{}
48+
outputFilePath := filepath.Join(t.TempDir(), "bom.json")
49+
outputFile, err := os.Create(outputFilePath)
50+
require.NoError(t, err)
5551

5652
// Encode BOM again
57-
buf := new(bytes.Buffer)
58-
tempFile, err := ioutil.TempFile("", "*_"+subTestNameSlashReplacer.Replace(t.Name()))
53+
err = NewBOMEncoder(io.MultiWriter(&buf, outputFile), BOMFileFormatJSON).
54+
SetPretty(true).
55+
Encode(&bom)
5956
require.NoError(t, err)
60-
61-
encoder := NewBOMEncoder(io.MultiWriter(buf, tempFile), BOMFileFormatJSON)
62-
encoder.SetPretty(true)
63-
require.NoError(t, encoder.Encode(bom))
64-
tempFile.Close() // Required for CLI to be able to access the file
57+
outputFile.Close() // Required for CLI to be able to access the file
6558

6659
// Sanity checks: BOM has to be valid
67-
assertValidBOM(t, tempFile.Name())
68-
os.Remove(tempFile.Name())
60+
assertValidBOM(t, outputFilePath, SpecVersion1_4)
6961

7062
// Compare with snapshot
71-
assert.NoError(t, roundTripSnapshotter.SnapshotMulti(filepath.Base(bomFilePath), buf.String()))
63+
assert.NoError(t, snapShooter.SnapshotMulti(filepath.Base(bomFilePath), buf.String()))
7264
})
7365
}
7466
}
@@ -80,43 +72,32 @@ func TestRoundTripXML(t *testing.T) {
8072
for _, bomFilePath := range bomFilePaths {
8173
t.Run(filepath.Base(bomFilePath), func(t *testing.T) {
8274
// Read original BOM XML
83-
bomFile, err := os.Open(bomFilePath)
75+
inputFile, err := os.Open(bomFilePath)
8476
require.NoError(t, err)
8577

8678
// Decode BOM
87-
bom := new(BOM)
88-
require.NoError(t, NewBOMDecoder(bomFile, BOMFileFormatXML).Decode(bom))
89-
bomFile.Close()
79+
var bom BOM
80+
require.NoError(t, NewBOMDecoder(inputFile, BOMFileFormatXML).Decode(&bom))
81+
inputFile.Close()
82+
83+
// Prepare encoding destinations
84+
buf := bytes.Buffer{}
85+
outputFilePath := filepath.Join(t.TempDir(), "bom.xml")
86+
outputFile, err := os.Create(outputFilePath)
87+
require.NoError(t, err)
9088

9189
// Encode BOM again
92-
buf := new(bytes.Buffer)
93-
tempFile, err := ioutil.TempFile("", "*_"+subTestNameSlashReplacer.Replace(t.Name()))
90+
err = NewBOMEncoder(io.MultiWriter(&buf, outputFile), BOMFileFormatXML).
91+
SetPretty(true).
92+
Encode(&bom)
9493
require.NoError(t, err)
95-
96-
encoder := NewBOMEncoder(io.MultiWriter(buf, tempFile), BOMFileFormatXML)
97-
encoder.SetPretty(true)
98-
require.NoError(t, encoder.Encode(bom))
99-
tempFile.Close() // Required for CLI to be able to access the file
94+
outputFile.Close() // Required for CLI to be able to access the file
10095

10196
// Sanity check: BOM has to be valid
102-
assertValidBOM(t, tempFile.Name())
103-
os.Remove(tempFile.Name())
97+
assertValidBOM(t, outputFilePath, SpecVersion1_4)
10498

10599
// Compare with snapshot
106-
assert.NoError(t, roundTripSnapshotter.SnapshotMulti(filepath.Base(bomFilePath), buf.String()))
100+
assert.NoError(t, snapShooter.SnapshotMulti(filepath.Base(bomFilePath), buf.String()))
107101
})
108102
}
109103
}
110-
111-
func assertValidBOM(t *testing.T, bomFilePath string) {
112-
inputFormat := "xml"
113-
if strings.HasSuffix(bomFilePath, ".json") {
114-
inputFormat = "json"
115-
}
116-
valCmd := exec.Command("cyclonedx", "validate", "--input-file", bomFilePath, "--input-format", inputFormat, "--input-version", "v1_4", "--fail-on-errors")
117-
valOut, err := valCmd.CombinedOutput()
118-
if !assert.NoError(t, err) {
119-
// Provide some context when test is failing
120-
fmt.Printf("validation error: %s\n", string(valOut))
121-
}
122-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
{
2+
"bomFormat": "CycloneDX",
3+
"specVersion": "1.2",
4+
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
5+
"version": 1,
6+
"metadata": {
7+
"timestamp": "2020-04-13T20:20:39+00:00",
8+
"tools": [
9+
{
10+
"vendor": "Awesome Vendor",
11+
"name": "Awesome Tool",
12+
"version": "9.1.2",
13+
"hashes": [
14+
{
15+
"alg": "SHA-1",
16+
"content": "25ed8e31b995bb927966616df2a42b979a2717f0"
17+
},
18+
{
19+
"alg": "SHA-256",
20+
"content": "a74f733635a19aefb1f73e5947cef59cd7440c6952ef0f03d09d974274cbd6df"
21+
}
22+
]
23+
}
24+
],
25+
"authors": [
26+
{
27+
"name": "Samantha Wright",
28+
"email": "samantha.wright@example.com",
29+
"phone": "800-555-1212"
30+
}
31+
],
32+
"component": {
33+
"type": "application",
34+
"author": "Acme Super Heros",
35+
"name": "Acme Application",
36+
"version": "9.1.1",
37+
"swid": {
38+
"text": {
39+
"content": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+CjxTb2Z0d2FyZUlkZW50aXR5IHhtbDpsYW5nPSJFTiIgbmFtZT0iQWNtZSBBcHBsaWNhdGlvbiIgdmVyc2lvbj0iOS4xLjEiIAogdmVyc2lvblNjaGVtZT0ibXVsdGlwYXJ0bnVtZXJpYyIgCiB0YWdJZD0ic3dpZGdlbi1iNTk1MWFjOS00MmMwLWYzODItM2YxZS1iYzdhMmE0NDk3Y2JfOS4xLjEiIAogeG1sbnM9Imh0dHA6Ly9zdGFuZGFyZHMuaXNvLm9yZy9pc28vMTk3NzAvLTIvMjAxNS9zY2hlbWEueHNkIj4gCiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiAKIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3N0YW5kYXJkcy5pc28ub3JnL2lzby8xOTc3MC8tMi8yMDE1LWN1cnJlbnQvc2NoZW1hLnhzZCBzY2hlbWEueHNkIiA+CiAgPE1ldGEgZ2VuZXJhdG9yPSJTV0lEIFRhZyBPbmxpbmUgR2VuZXJhdG9yIHYwLjEiIC8+IAogIDxFbnRpdHkgbmFtZT0iQWNtZSwgSW5jLiIgcmVnaWQ9ImV4YW1wbGUuY29tIiByb2xlPSJ0YWdDcmVhdG9yIiAvPiAKPC9Tb2Z0d2FyZUlkZW50aXR5Pg==",
40+
"contentType": "text/xml",
41+
"encoding": "base64"
42+
},
43+
"tagId": "swidgen-242eb18a-503e-ca37-393b-cf156ef09691_9.1.1",
44+
"name": "Acme Application",
45+
"version": "9.1.1"
46+
}
47+
},
48+
"manufacture": {
49+
"name": "Acme, Inc.",
50+
"url": [
51+
"https://example.com"
52+
],
53+
"contact": [
54+
{
55+
"name": "Acme Professional Services",
56+
"email": "professional.services@example.com"
57+
}
58+
]
59+
},
60+
"supplier": {
61+
"name": "Acme, Inc.",
62+
"url": [
63+
"https://example.com"
64+
],
65+
"contact": [
66+
{
67+
"name": "Acme Distribution",
68+
"email": "distribution@example.com"
69+
}
70+
]
71+
}
72+
},
73+
"components": [
74+
{
75+
"bom-ref": "pkg:npm/acme/component@1.0.0",
76+
"type": "library",
77+
"publisher": "Acme Inc",
78+
"group": "com.acme",
79+
"name": "tomcat-catalina",
80+
"version": "9.0.14",
81+
"hashes": [
82+
{
83+
"alg": "MD5",
84+
"content": "3942447fac867ae5cdb3229b658f4d48"
85+
},
86+
{
87+
"alg": "SHA-1",
88+
"content": "e6b1000b94e835ffd37f4c6dcbdad43f4b48a02a"
89+
},
90+
{
91+
"alg": "SHA-256",
92+
"content": "f498a8ff2dd007e29c2074f5e4b01a9a01775c3ff3aeaf6906ea503bc5791b7b"
93+
},
94+
{
95+
"alg": "SHA-512",
96+
"content": "e8f33e424f3f4ed6db76a482fde1a5298970e442c531729119e37991884bdffab4f9426b7ee11fccd074eeda0634d71697d6f88a460dce0ac8d627a29f7d1282"
97+
}
98+
],
99+
"licenses": [
100+
{
101+
"license": {
102+
"id": "Apache-2.0",
103+
"text": {
104+
"content": "License text here",
105+
"contentType": "text/plain",
106+
"encoding": "base64"
107+
},
108+
"url": "https://www.apache.org/licenses/LICENSE-2.0.txt"
109+
}
110+
}
111+
],
112+
"purl": "pkg:npm/acme/component@1.0.0",
113+
"pedigree": {
114+
"ancestors": [
115+
{
116+
"type": "library",
117+
"publisher": "Acme Inc",
118+
"group": "com.acme",
119+
"name": "tomcat-catalina",
120+
"version": "9.0.14"
121+
},
122+
{
123+
"type": "library",
124+
"publisher": "Acme Inc",
125+
"group": "com.acme",
126+
"name": "tomcat-catalina",
127+
"version": "9.0.14"
128+
}
129+
],
130+
"commits": [
131+
{
132+
"uid": "123",
133+
"author": {
134+
"timestamp": "2018-11-13T20:20:39+00:00"
135+
}
136+
}
137+
]
138+
}
139+
},
140+
{
141+
"type": "library",
142+
"supplier": {
143+
"name": "Example, Inc.",
144+
"url": [
145+
"https://example.com",
146+
"https://example.net"
147+
],
148+
"contact": [
149+
{
150+
"name": "Example Support AMER Distribution",
151+
"email": "support@example.com",
152+
"phone": "800-555-1212"
153+
},
154+
{
155+
"name": "Example Support APAC",
156+
"email": "support@apac.example.com"
157+
}
158+
]
159+
},
160+
"author": "Example Super Heros",
161+
"group": "org.example",
162+
"name": "mylibrary",
163+
"version": "1.0.0"
164+
}
165+
],
166+
"dependencies": [
167+
{
168+
"ref": "pkg:npm/acme/component@1.0.0",
169+
"dependsOn": [
170+
"pkg:npm/acme/component@1.0.0"
171+
]
172+
}
173+
]
174+
}
175+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
{
2+
"bomFormat": "CycloneDX",
3+
"specVersion": "1.3",
4+
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
5+
"version": 1,
6+
"metadata": {
7+
"timestamp": "2020-04-13T20:20:39+00:00",
8+
"tools": [
9+
{
10+
"vendor": "Awesome Vendor",
11+
"name": "Awesome Tool",
12+
"version": "9.1.2",
13+
"hashes": [
14+
{
15+
"alg": "SHA-1",
16+
"content": "25ed8e31b995bb927966616df2a42b979a2717f0"
17+
},
18+
{
19+
"alg": "SHA-256",
20+
"content": "a74f733635a19aefb1f73e5947cef59cd7440c6952ef0f03d09d974274cbd6df"
21+
}
22+
]
23+
}
24+
],
25+
"authors": [
26+
{
27+
"name": "Samantha Wright",
28+
"email": "samantha.wright@example.com",
29+
"phone": "800-555-1212"
30+
}
31+
],
32+
"component": {
33+
"type": "application",
34+
"author": "Acme Super Heros",
35+
"name": "Acme Application",
36+
"version": "9.1.1",
37+
"swid": {
38+
"text": {
39+
"content": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+CjxTb2Z0d2FyZUlkZW50aXR5IHhtbDpsYW5nPSJFTiIgbmFtZT0iQWNtZSBBcHBsaWNhdGlvbiIgdmVyc2lvbj0iOS4xLjEiIAogdmVyc2lvblNjaGVtZT0ibXVsdGlwYXJ0bnVtZXJpYyIgCiB0YWdJZD0ic3dpZGdlbi1iNTk1MWFjOS00MmMwLWYzODItM2YxZS1iYzdhMmE0NDk3Y2JfOS4xLjEiIAogeG1sbnM9Imh0dHA6Ly9zdGFuZGFyZHMuaXNvLm9yZy9pc28vMTk3NzAvLTIvMjAxNS9zY2hlbWEueHNkIj4gCiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiAKIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3N0YW5kYXJkcy5pc28ub3JnL2lzby8xOTc3MC8tMi8yMDE1LWN1cnJlbnQvc2NoZW1hLnhzZCBzY2hlbWEueHNkIiA+CiAgPE1ldGEgZ2VuZXJhdG9yPSJTV0lEIFRhZyBPbmxpbmUgR2VuZXJhdG9yIHYwLjEiIC8+IAogIDxFbnRpdHkgbmFtZT0iQWNtZSwgSW5jLiIgcmVnaWQ9ImV4YW1wbGUuY29tIiByb2xlPSJ0YWdDcmVhdG9yIiAvPiAKPC9Tb2Z0d2FyZUlkZW50aXR5Pg==",
40+
"contentType": "text/xml",
41+
"encoding": "base64"
42+
},
43+
"tagId": "swidgen-242eb18a-503e-ca37-393b-cf156ef09691_9.1.1",
44+
"name": "Acme Application",
45+
"version": "9.1.1"
46+
}
47+
},
48+
"manufacture": {
49+
"name": "Acme, Inc.",
50+
"url": [
51+
"https://example.com"
52+
],
53+
"contact": [
54+
{
55+
"name": "Acme Professional Services",
56+
"email": "professional.services@example.com"
57+
}
58+
]
59+
},
60+
"supplier": {
61+
"name": "Acme, Inc.",
62+
"url": [
63+
"https://example.com"
64+
],
65+
"contact": [
66+
{
67+
"name": "Acme Distribution",
68+
"email": "distribution@example.com"
69+
}
70+
]
71+
}
72+
},
73+
"components": [
74+
{
75+
"bom-ref": "pkg:npm/acme/component@1.0.0",
76+
"type": "library",
77+
"publisher": "Acme Inc",
78+
"group": "com.acme",
79+
"name": "tomcat-catalina",
80+
"version": "9.0.14",
81+
"hashes": [
82+
{
83+
"alg": "MD5",
84+
"content": "3942447fac867ae5cdb3229b658f4d48"
85+
},
86+
{
87+
"alg": "SHA-1",
88+
"content": "e6b1000b94e835ffd37f4c6dcbdad43f4b48a02a"
89+
},
90+
{
91+
"alg": "SHA-256",
92+
"content": "f498a8ff2dd007e29c2074f5e4b01a9a01775c3ff3aeaf6906ea503bc5791b7b"
93+
},
94+
{
95+
"alg": "SHA-512",
96+
"content": "e8f33e424f3f4ed6db76a482fde1a5298970e442c531729119e37991884bdffab4f9426b7ee11fccd074eeda0634d71697d6f88a460dce0ac8d627a29f7d1282"
97+
}
98+
],
99+
"licenses": [
100+
{
101+
"license": {
102+
"id": "Apache-2.0",
103+
"text": {
104+
"content": "License text here",
105+
"contentType": "text/plain",
106+
"encoding": "base64"
107+
},
108+
"url": "https://www.apache.org/licenses/LICENSE-2.0.txt"
109+
}
110+
}
111+
],
112+
"purl": "pkg:npm/acme/component@1.0.0",
113+
"pedigree": {
114+
"ancestors": [
115+
{
116+
"type": "library",
117+
"publisher": "Acme Inc",
118+
"group": "com.acme",
119+
"name": "tomcat-catalina",
120+
"version": "9.0.14"
121+
},
122+
{
123+
"type": "library",
124+
"publisher": "Acme Inc",
125+
"group": "com.acme",
126+
"name": "tomcat-catalina",
127+
"version": "9.0.14"
128+
}
129+
],
130+
"commits": [
131+
{
132+
"uid": "123",
133+
"author": {
134+
"timestamp": "2018-11-13T20:20:39+00:00"
135+
}
136+
}
137+
]
138+
}
139+
},
140+
{
141+
"type": "library",
142+
"supplier": {
143+
"name": "Example, Inc.",
144+
"url": [
145+
"https://example.com",
146+
"https://example.net"
147+
],
148+
"contact": [
149+
{
150+
"name": "Example Support AMER Distribution",
151+
"email": "support@example.com",
152+
"phone": "800-555-1212"
153+
},
154+
{
155+
"name": "Example Support APAC",
156+
"email": "support@apac.example.com"
157+
}
158+
]
159+
},
160+
"author": "Example Super Heros",
161+
"group": "org.example",
162+
"name": "mylibrary",
163+
"version": "1.0.0"
164+
}
165+
],
166+
"dependencies": [
167+
{
168+
"ref": "pkg:npm/acme/component@1.0.0",
169+
"dependsOn": [
170+
"pkg:npm/acme/component@1.0.0"
171+
]
172+
}
173+
]
174+
}
175+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
{
2+
"bomFormat": "CycloneDX",
3+
"specVersion": "1.4",
4+
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
5+
"version": 1,
6+
"metadata": {
7+
"timestamp": "2020-04-13T20:20:39+00:00",
8+
"tools": [
9+
{
10+
"vendor": "Awesome Vendor",
11+
"name": "Awesome Tool",
12+
"version": "9.1.2",
13+
"hashes": [
14+
{
15+
"alg": "SHA-1",
16+
"content": "25ed8e31b995bb927966616df2a42b979a2717f0"
17+
},
18+
{
19+
"alg": "SHA-256",
20+
"content": "a74f733635a19aefb1f73e5947cef59cd7440c6952ef0f03d09d974274cbd6df"
21+
}
22+
]
23+
}
24+
],
25+
"authors": [
26+
{
27+
"name": "Samantha Wright",
28+
"email": "samantha.wright@example.com",
29+
"phone": "800-555-1212"
30+
}
31+
],
32+
"component": {
33+
"type": "application",
34+
"author": "Acme Super Heros",
35+
"name": "Acme Application",
36+
"version": "9.1.1",
37+
"swid": {
38+
"text": {
39+
"content": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+CjxTb2Z0d2FyZUlkZW50aXR5IHhtbDpsYW5nPSJFTiIgbmFtZT0iQWNtZSBBcHBsaWNhdGlvbiIgdmVyc2lvbj0iOS4xLjEiIAogdmVyc2lvblNjaGVtZT0ibXVsdGlwYXJ0bnVtZXJpYyIgCiB0YWdJZD0ic3dpZGdlbi1iNTk1MWFjOS00MmMwLWYzODItM2YxZS1iYzdhMmE0NDk3Y2JfOS4xLjEiIAogeG1sbnM9Imh0dHA6Ly9zdGFuZGFyZHMuaXNvLm9yZy9pc28vMTk3NzAvLTIvMjAxNS9zY2hlbWEueHNkIj4gCiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiAKIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3N0YW5kYXJkcy5pc28ub3JnL2lzby8xOTc3MC8tMi8yMDE1LWN1cnJlbnQvc2NoZW1hLnhzZCBzY2hlbWEueHNkIiA+CiAgPE1ldGEgZ2VuZXJhdG9yPSJTV0lEIFRhZyBPbmxpbmUgR2VuZXJhdG9yIHYwLjEiIC8+IAogIDxFbnRpdHkgbmFtZT0iQWNtZSwgSW5jLiIgcmVnaWQ9ImV4YW1wbGUuY29tIiByb2xlPSJ0YWdDcmVhdG9yIiAvPiAKPC9Tb2Z0d2FyZUlkZW50aXR5Pg==",
40+
"contentType": "text/xml",
41+
"encoding": "base64"
42+
},
43+
"tagId": "swidgen-242eb18a-503e-ca37-393b-cf156ef09691_9.1.1",
44+
"name": "Acme Application",
45+
"version": "9.1.1"
46+
}
47+
},
48+
"manufacture": {
49+
"name": "Acme, Inc.",
50+
"url": [
51+
"https://example.com"
52+
],
53+
"contact": [
54+
{
55+
"name": "Acme Professional Services",
56+
"email": "professional.services@example.com"
57+
}
58+
]
59+
},
60+
"supplier": {
61+
"name": "Acme, Inc.",
62+
"url": [
63+
"https://example.com"
64+
],
65+
"contact": [
66+
{
67+
"name": "Acme Distribution",
68+
"email": "distribution@example.com"
69+
}
70+
]
71+
}
72+
},
73+
"components": [
74+
{
75+
"bom-ref": "pkg:npm/acme/component@1.0.0",
76+
"type": "library",
77+
"publisher": "Acme Inc",
78+
"group": "com.acme",
79+
"name": "tomcat-catalina",
80+
"version": "9.0.14",
81+
"hashes": [
82+
{
83+
"alg": "MD5",
84+
"content": "3942447fac867ae5cdb3229b658f4d48"
85+
},
86+
{
87+
"alg": "SHA-1",
88+
"content": "e6b1000b94e835ffd37f4c6dcbdad43f4b48a02a"
89+
},
90+
{
91+
"alg": "SHA-256",
92+
"content": "f498a8ff2dd007e29c2074f5e4b01a9a01775c3ff3aeaf6906ea503bc5791b7b"
93+
},
94+
{
95+
"alg": "SHA-512",
96+
"content": "e8f33e424f3f4ed6db76a482fde1a5298970e442c531729119e37991884bdffab4f9426b7ee11fccd074eeda0634d71697d6f88a460dce0ac8d627a29f7d1282"
97+
}
98+
],
99+
"licenses": [
100+
{
101+
"license": {
102+
"id": "Apache-2.0",
103+
"text": {
104+
"content": "License text here",
105+
"contentType": "text/plain",
106+
"encoding": "base64"
107+
},
108+
"url": "https://www.apache.org/licenses/LICENSE-2.0.txt"
109+
}
110+
}
111+
],
112+
"purl": "pkg:npm/acme/component@1.0.0",
113+
"pedigree": {
114+
"ancestors": [
115+
{
116+
"type": "library",
117+
"publisher": "Acme Inc",
118+
"group": "com.acme",
119+
"name": "tomcat-catalina",
120+
"version": "9.0.14"
121+
},
122+
{
123+
"type": "library",
124+
"publisher": "Acme Inc",
125+
"group": "com.acme",
126+
"name": "tomcat-catalina",
127+
"version": "9.0.14"
128+
}
129+
],
130+
"commits": [
131+
{
132+
"uid": "123",
133+
"author": {
134+
"timestamp": "2018-11-13T20:20:39+00:00"
135+
}
136+
}
137+
]
138+
}
139+
},
140+
{
141+
"type": "library",
142+
"supplier": {
143+
"name": "Example, Inc.",
144+
"url": [
145+
"https://example.com",
146+
"https://example.net"
147+
],
148+
"contact": [
149+
{
150+
"name": "Example Support AMER Distribution",
151+
"email": "support@example.com",
152+
"phone": "800-555-1212"
153+
},
154+
{
155+
"name": "Example Support APAC",
156+
"email": "support@apac.example.com"
157+
}
158+
]
159+
},
160+
"author": "Example Super Heros",
161+
"group": "org.example",
162+
"name": "mylibrary",
163+
"version": "1.0.0"
164+
}
165+
],
166+
"dependencies": [
167+
{
168+
"ref": "pkg:npm/acme/component@1.0.0",
169+
"dependsOn": [
170+
"pkg:npm/acme/component@1.0.0"
171+
]
172+
}
173+
]
174+
}
175+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.0" version="1">
3+
<components>
4+
<component type="application">
5+
<publisher>Acme Inc</publisher>
6+
<group>com.acme</group>
7+
<name>tomcat-catalina</name>
8+
<version>9.0.14</version>
9+
<description>Modified version of Apache Catalina</description>
10+
<scope>required</scope>
11+
<hashes>
12+
<hash alg="MD5">3942447fac867ae5cdb3229b658f4d48</hash>
13+
<hash alg="SHA-1">e6b1000b94e835ffd37f4c6dcbdad43f4b48a02a</hash>
14+
<hash alg="SHA-256">f498a8ff2dd007e29c2074f5e4b01a9a01775c3ff3aeaf6906ea503bc5791b7b</hash>
15+
<hash alg="SHA-512">e8f33e424f3f4ed6db76a482fde1a5298970e442c531729119e37991884bdffab4f9426b7ee11fccd074eeda0634d71697d6f88a460dce0ac8d627a29f7d1282</hash>
16+
</hashes>
17+
<licenses>
18+
<license>
19+
<id>Apache-2.0</id>
20+
</license>
21+
</licenses>
22+
<purl>pkg:maven/com.acme/tomcat-catalina@9.0.14?packaging=jar</purl>
23+
<modified>false</modified>
24+
</component>
25+
<component type="library">
26+
<group>org.example</group>
27+
<name>mylibrary</name>
28+
<version>1.0.0</version>
29+
<scope>required</scope>
30+
<hashes>
31+
<hash alg="MD5">2342c2eaf1feb9a80195dbaddf2ebaa3</hash>
32+
<hash alg="SHA-1">68b78babe00a053f9e35ec6a2d9080f5b90122b0</hash>
33+
<hash alg="SHA-256">708f1f53b41f11f02d12a11b1a38d2905d47b099afc71a0f1124ef8582ec7313</hash>
34+
<hash alg="SHA-512">387b7ae16b9cae45f830671541539bf544202faae5aac544a93b7b0a04f5f846fa2f4e81ef3f1677e13aed7496408a441f5657ab6d54423e56bf6f38da124aef</hash>
35+
</hashes>
36+
<copyright>Copyright Example Inc. All rights reserved.</copyright>
37+
<cpe>cpe:/a:example:myapplication:1.0.0</cpe>
38+
<purl>pkg:maven/com.example/myapplication@1.0.0?packaging=war</purl>
39+
<modified>false</modified>
40+
</component>
41+
<component type="framework">
42+
<group>com.example</group>
43+
<name>myframework</name>
44+
<version>1.0.0</version>
45+
<description>Example Inc, enterprise framework</description>
46+
<scope>required</scope>
47+
<hashes>
48+
<hash alg="MD5">cfcb0b64aacd2f81c1cd546543de965a</hash>
49+
<hash alg="SHA-1">7fbeef2346c45d565c3341f037bce4e088af8a52</hash>
50+
<hash alg="SHA-256">0384db3cec55d86a6898c489fdb75a8e75fe66b26639634983d2f3c3558493d1</hash>
51+
<hash alg="SHA-512">854909cdb9e3ca183056837144aab6d8069b377bd66445087cc7157bf0c3f620418705dd0b83bdc2f73a508c2bdb316ca1809d75ee6972d02023a3e7dd655c79</hash>
52+
</hashes>
53+
<licenses>
54+
<license>
55+
<name>Some random license</name>
56+
</license>
57+
</licenses>
58+
<purl>pkg:maven/com.example/myframework@1.0.0?packaging=war</purl>
59+
<modified>false</modified>
60+
</component>
61+
</components>
62+
</bom>

‎testdata/snapshots/cyclonedx-go-TestXmlBOMEncoder_EncodeVersion-func1-1.1.bom.xml

+116
Large diffs are not rendered by default.

‎testdata/snapshots/cyclonedx-go-TestXmlBOMEncoder_EncodeVersion-func1-1.2.bom.xml

+179
Large diffs are not rendered by default.

‎testdata/snapshots/cyclonedx-go-TestXmlBOMEncoder_EncodeVersion-func1-1.3.bom.xml

+179
Large diffs are not rendered by default.

‎testdata/snapshots/cyclonedx-go-TestXmlBOMEncoder_EncodeVersion-func1-1.4.bom.xml

+179
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.