Skip to content
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

feat: add support for encoding to older spec versions #51

Merged
merged 2 commits into from Sep 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 6 additions & 5 deletions .licenserc.yml
Expand Up @@ -3,17 +3,18 @@ header:
spdx-id: Apache-2.0
copyright-owner: OWASP Foundation
paths-ignore:
- "**/*.md"
- "**/go.mod"
- "**/go.sum"
- "**/testdata/**"
- ".github/**"
- ".gitignore"
- ".gitpod.*"
- ".golangci.yml"
- ".goreleaser.yml"
- ".licenserc.yml"
- "**/*.md"
- "**/go.mod"
- "**/go.sum"
- "**/testdata/**"
- "CODEOWNERS"
- "LICENSE"
- "Makefile"
- "NOTICE"
- "NOTICE"
- "cyclonedx_string.go"
4 changes: 4 additions & 0 deletions Makefile
Expand Up @@ -10,5 +10,9 @@ clean:
go clean
.PHONY: clean

generate:
go generate
.PHONY: generate

all: clean build test
.PHONY: all
13 changes: 8 additions & 5 deletions README.md
Expand Up @@ -28,15 +28,18 @@ Also, checkout the [`examples`](./example_test.go) to get an idea of how this li

| cyclonedx-go versions | Supported Go versions | Supported CycloneDX spec |
|:---------------------:|:---------------------:|:------------------------:|
| < v0.4.0 | 1.14+ | 1.2 |
| == v0.4.0 | 1.14+ | 1.3 |
| >= v0.5.0 | 1.15+ | 1.4 |
| < v0.4.0 | 1.14+ | 1.2 |
| == v0.4.0 | 1.14+ | 1.3 |
| >= v0.5.0 | 1.15+ | 1.4 |
| >= v0.7.0 | 1.15+ | 1.0-1.4 |

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

This library will only support the latest version of the CycloneDX specification. While it's generally possible to
*read* BOMs of an older spec, *writing* will exclusively produce BOMs conforming to the latest supported spec.
Prior to v0.7.0, this library only supported the latest version of the CycloneDX specification. While it is generally
possible to *read* BOMs of an older spec, *writing* would exclusively produce BOMs conforming to the latest supported spec.

Starting with v0.7.0, writing BOMs conforming to all previous version of the spec is also possible.

## Copyright & License

Expand Down
43 changes: 43 additions & 0 deletions copy.go
@@ -0,0 +1,43 @@
// This file is part of CycloneDX Go
//
// Licensed under the Apache License, Version 2.0 (the “License”);
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an “AS IS” BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) OWASP Foundation. All Rights Reserved.

package cyclonedx

import (
"bytes"
"encoding/gob"
"fmt"
)

// copy creates a deep copy of the BOM in a given destination.
// Copying is currently done be encoding and decoding the BOM struct using the gop.
// In the future we may choose to switch to a more efficient strategy,
// and consider to export this API.
func (b BOM) copy(dst *BOM) error {
buf := bytes.Buffer{}
err := gob.NewEncoder(&buf).Encode(b)
if err != nil {
return fmt.Errorf("failed to encode bom: %w", err)
}

err = gob.NewDecoder(&buf).Decode(dst)
if err != nil {
return fmt.Errorf("failed to decode bom: %w", err)
}

return nil
}
89 changes: 80 additions & 9 deletions cyclonedx.go
Expand Up @@ -26,11 +26,10 @@ import (
"regexp"
)

//go:generate stringer -linecomment -output cyclonedx_string.go -type SpecVersion

const (
BOMFormat = "CycloneDX"
defaultVersion = 1
SpecVersion = "1.4"
XMLNamespace = "http://cyclonedx.org/schema/bom/1.4"
BOMFormat = "CycloneDX"
)

type Advisory struct {
Expand Down Expand Up @@ -61,8 +60,8 @@ type BOM struct {
XMLNS string `json:"-" xml:"xmlns,attr"`

// JSON specific fields
BOMFormat string `json:"bomFormat" xml:"-"`
SpecVersion string `json:"specVersion" xml:"-"`
BOMFormat string `json:"bomFormat" xml:"-"`
SpecVersion SpecVersion `json:"specVersion" xml:"-"`

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

func NewBOM() *BOM {
return &BOM{
XMLNS: XMLNamespace,
XMLNS: xmlNamespaces[SpecVersion1_4],
BOMFormat: BOMFormat,
SpecVersion: SpecVersion,
Version: defaultVersion,
SpecVersion: SpecVersion1_4,
Version: 1,
}
}

Expand Down Expand Up @@ -590,6 +589,70 @@ type Source struct {
URL string `json:"url,omitempty" xml:"url,omitempty"`
}

type SpecVersion int

const (
SpecVersion1_0 SpecVersion = iota + 1 // 1.0
SpecVersion1_1 // 1.1
SpecVersion1_2 // 1.2
SpecVersion1_3 // 1.3
SpecVersion1_4 // 1.4
)

func (sv SpecVersion) MarshalJSON() ([]byte, error) {
return json.Marshal(sv.String())
}

func (sv *SpecVersion) UnmarshalJSON(bytes []byte) error {
var v string
err := json.Unmarshal(bytes, &v)
if err != nil {
return err
}

switch v {
case SpecVersion1_0.String():
*sv = SpecVersion1_0
case SpecVersion1_1.String():
*sv = SpecVersion1_1
case SpecVersion1_2.String():
*sv = SpecVersion1_2
case SpecVersion1_3.String():
*sv = SpecVersion1_3
case SpecVersion1_4.String():
*sv = SpecVersion1_4
}

return nil
}

func (sv SpecVersion) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.EncodeElement(sv.String(), start)
}

func (sv *SpecVersion) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var v string
err := d.DecodeElement(&v, &start)
if err != nil {
return err
}

switch v {
case SpecVersion1_0.String():
*sv = SpecVersion1_0
case SpecVersion1_1.String():
*sv = SpecVersion1_1
case SpecVersion1_2.String():
*sv = SpecVersion1_2
case SpecVersion1_3.String():
*sv = SpecVersion1_3
case SpecVersion1_4.String():
*sv = SpecVersion1_4
}

return nil
}

type SWID struct {
Text *AttachedText `json:"text,omitempty" xml:"text,omitempty"`
URL string `json:"url,omitempty" xml:"url,attr,omitempty"`
Expand Down Expand Up @@ -657,3 +720,11 @@ const (
VulnerabilityStatusAffected VulnerabilityStatus = "affected"
VulnerabilityStatusNotAffected VulnerabilityStatus = "unaffected"
)

var xmlNamespaces = map[SpecVersion]string{
SpecVersion1_0: "http://cyclonedx.org/schema/bom/1.0",
SpecVersion1_1: "http://cyclonedx.org/schema/bom/1.1",
SpecVersion1_2: "http://cyclonedx.org/schema/bom/1.2",
SpecVersion1_3: "http://cyclonedx.org/schema/bom/1.3",
SpecVersion1_4: "http://cyclonedx.org/schema/bom/1.4",
}
28 changes: 28 additions & 0 deletions cyclonedx_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions cyclonedx_test.go
Expand Up @@ -20,12 +20,19 @@ package cyclonedx
import (
"encoding/json"
"encoding/xml"
"fmt"
"os/exec"
"strings"
"testing"

"github.com/bradleyjkemp/cupaloy/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

var snapShooter = cupaloy.NewDefaultConfig().
WithOptions(cupaloy.SnapshotSubdirectory("./testdata/snapshots"))

func TestBool(t *testing.T) {
assert.Equal(t, true, *Bool(true))
assert.Equal(t, false, *Bool(false))
Expand Down Expand Up @@ -228,3 +235,17 @@ func TestVulnerability_Properties(t *testing.T) {
// EXPECT
assert.Equal(t, 0, len(*vuln.Properties))
}

func assertValidBOM(t *testing.T, bomFilePath string, version SpecVersion) {
inputFormat := "xml"
if strings.HasSuffix(bomFilePath, ".json") {
inputFormat = "json"
}
inputVersion := fmt.Sprintf("v%s", strings.ReplaceAll(version.String(), ".", "_"))
valCmd := exec.Command("cyclonedx", "validate", "--input-file", bomFilePath, "--input-format", inputFormat, "--input-version", inputVersion, "--fail-on-errors")
valOut, err := valCmd.CombinedOutput()
if !assert.NoError(t, err) {
// Provide some context when test is failing
fmt.Printf("validation error: %s\n", string(valOut))
}
}
2 changes: 2 additions & 0 deletions decode.go
Expand Up @@ -38,6 +38,7 @@ type jsonBOMDecoder struct {
reader io.Reader
}

// Decode implements the BOMDecoder interface.
func (j jsonBOMDecoder) Decode(bom *BOM) error {
return json.NewDecoder(j.reader).Decode(bom)
}
Expand All @@ -46,6 +47,7 @@ type xmlBOMDecoder struct {
reader io.Reader
}

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