Skip to content

Commit

Permalink
feat: add support for encoding to older spec versions (#51)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
nscuro committed Sep 25, 2022
1 parent c4cecac commit 2826fe2
Show file tree
Hide file tree
Showing 21 changed files with 1,763 additions and 77 deletions.
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)
}

0 comments on commit 2826fe2

Please sign in to comment.