diff --git a/pkg/sif/descriptor.go b/pkg/sif/descriptor.go index 86fac669..03ed2b04 100644 --- a/pkg/sif/descriptor.go +++ b/pkg/sif/descriptor.go @@ -56,6 +56,11 @@ type cryptoMessage struct { Messagetype MessageType } +// sbom represents the SIF SBOM data object descriptor. +type sbom struct { + Format SBOMFormat +} + var errNameTooLarge = errors.New("name value too large") // setName encodes name into the name field of d. @@ -230,6 +235,22 @@ func (d Descriptor) CryptoMessageMetadata() (FormatType, MessageType, error) { return m.Formattype, m.Messagetype, nil } +// SBOMMetadata gets metadata for a SBOM data object. +func (d Descriptor) SBOMMetadata() (SBOMFormat, error) { + if got, want := d.raw.DataType, DataSBOM; got != want { + return 0, &unexpectedDataTypeError{got, []DataType{want}} + } + + var s sbom + + b := bytes.NewReader(d.raw.Extra[:]) + if err := binary.Read(b, binary.LittleEndian, &s); err != nil { + return 0, fmt.Errorf("%w", err) + } + + return s.Format, nil +} + // GetData returns the data object associated with descriptor d. func (d Descriptor) GetData() ([]byte, error) { b := make([]byte, d.raw.Size) diff --git a/pkg/sif/descriptor_input.go b/pkg/sif/descriptor_input.go index 1b8dda20..3e81c394 100644 --- a/pkg/sif/descriptor_input.go +++ b/pkg/sif/descriptor_input.go @@ -226,6 +226,24 @@ func OptSignatureMetadata(ht crypto.Hash, fp []byte) DescriptorInputOpt { } } +// OptSBOMMetadata sets metadata for a SBOM data object. The SBOM format is set to f. +// +// If this option is applied to a data object with an incompatible type, an error is returned. +func OptSBOMMetadata(f SBOMFormat) DescriptorInputOpt { + return func(t DataType, opts *descriptorOpts) error { + if got, want := t, DataSBOM; got != want { + return &unexpectedDataTypeError{got, []DataType{want}} + } + + s := sbom{ + Format: f, + } + + opts.extra = s + return nil + } +} + // DescriptorInput describes a new data object. type DescriptorInput struct { dt DataType @@ -241,7 +259,7 @@ const DefaultObjectGroup = 1 // // It is possible (and often necessary) to store additional metadata related to certain types of // data objects. Consider supplying options such as OptCryptoMessageMetadata, OptPartitionMetadata, -// and OptSignatureMetadata for this purpose. +// OptSignatureMetadata, and OptSBOMMetadata for this purpose. // // By default, the data object will be placed in the default data object groupĀ (1). To override // this behavior, use OptNoGroup or OptGroupID. To link this data object, use OptLinkedID or diff --git a/pkg/sif/descriptor_input_test.go b/pkg/sif/descriptor_input_test.go index 84312311..5c34099e 100644 --- a/pkg/sif/descriptor_input_test.go +++ b/pkg/sif/descriptor_input_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021, Sylabs Inc. All rights reserved. +// Copyright (c) 2021-2022, Sylabs Inc. All rights reserved. // This software is licensed under a 3-clause BSD license. Please consult the // LICENSE file distributed with the sources of this project regarding your // rights to use or distribute this software. @@ -163,6 +163,21 @@ func TestNewDescriptorInput(t *testing.T) { OptSignatureMetadata(crypto.SHA256, fp), }, }, + { + name: "OptSBOMMetadataUnexpectedDataType", + t: DataGeneric, + opts: []DescriptorInputOpt{ + OptSBOMMetadata(SBOMFormatCycloneDXJSON), + }, + wantErr: &unexpectedDataTypeError{DataGeneric, []DataType{DataSBOM}}, + }, + { + name: "OptSBOMMetadata", + t: DataSBOM, + opts: []DescriptorInputOpt{ + OptSBOMMetadata(SBOMFormatCycloneDXJSON), + }, + }, } for _, tt := range tests { tt := tt diff --git a/pkg/sif/descriptor_test.go b/pkg/sif/descriptor_test.go index 470d91ab..5febda36 100644 --- a/pkg/sif/descriptor_test.go +++ b/pkg/sif/descriptor_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2021, Sylabs Inc. All rights reserved. +// Copyright (c) 2018-2022, Sylabs Inc. All rights reserved. // This software is licensed under a 3-clause BSD license. Please consult the // LICENSE file distributed with the sources of this project regarding your // rights to use or distribute this software. @@ -287,6 +287,56 @@ func TestDescriptor_CryptoMessageMetadata(t *testing.T) { } } +func TestDescriptor_SBOMMetadata(t *testing.T) { + m := sbom{ + Format: SBOMFormatCycloneDXJSON, + } + + rd := rawDescriptor{ + DataType: DataSBOM, + } + if err := rd.setExtra(m); err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + rd rawDescriptor + wantFormat SBOMFormat + wantErr error + }{ + { + name: "UnexpectedDataType", + rd: rawDescriptor{ + DataType: DataGeneric, + }, + wantErr: &unexpectedDataTypeError{DataGeneric, []DataType{DataSBOM}}, + }, + { + name: "OK", + rd: rd, + wantFormat: SBOMFormatCycloneDXJSON, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := Descriptor{raw: tt.rd} + + f, err := d.SBOMMetadata() + + if got, want := err, tt.wantErr; !errors.Is(got, want) { + t.Fatalf("got error %v, want %v", got, want) + } + + if err == nil { + if got, want := f, tt.wantFormat; got != want { + t.Fatalf("got format %v, want %v", got, want) + } + } + }) + } +} + func TestDescriptor_GetIntegrityReader(t *testing.T) { rd := rawDescriptor{ DataType: DataDeffile, diff --git a/pkg/sif/sif.go b/pkg/sif/sif.go index e0faaedb..2d1c2091 100644 --- a/pkg/sif/sif.go +++ b/pkg/sif/sif.go @@ -132,6 +132,7 @@ const ( DataGenericJSON // generic JSON meta-data DataGeneric // generic / raw data DataCryptoMessage // cryptographic message data object + DataSBOM // software bill of materials ) // String returns a human-readable representation of t. @@ -153,6 +154,8 @@ func (t DataType) String() string { return "Generic/Raw" case DataCryptoMessage: return "Cryptographic Message" + case DataSBOM: + return "SBOM" } return "Unknown" } @@ -267,6 +270,44 @@ func (t MessageType) String() string { return "Unknown" } +// SBOMFormat represents the format used to store an SBOM object. +type SBOMFormat int32 + +// List of supported SBOM formats. +const ( + SBOMFormatCycloneDXJSON SBOMFormat = iota + 1 // CycloneDX (JSON) + SBOMFormatCycloneDXXML // CycloneDX (XML) + SBOMFormatGitHubJSON // GitHub dependency snapshot (JSON) + SBOMFormatSPDXJSON // SPDX (JSON) + SBOMFormatSPDXRDF // SPDX (RDF/xml) + SBOMFormatSPDXTagValue // SPDX (tag/value) + SBOMFormatSPDXYAML // SPDX (YAML) + SBOMFormatSyftJSON // Syft (JSON) +) + +// String returns a human-readable representation of f. +func (f SBOMFormat) String() string { + switch f { + case SBOMFormatCycloneDXJSON: + return "cyclonedx-json" + case SBOMFormatCycloneDXXML: + return "cyclonedx-xml" + case SBOMFormatGitHubJSON: + return "github-json" + case SBOMFormatSPDXJSON: + return "spdx-json" + case SBOMFormatSPDXRDF: + return "spdx-rdf" + case SBOMFormatSPDXTagValue: + return "spdx-tag-value" + case SBOMFormatSPDXYAML: + return "spdx-yaml" + case SBOMFormatSyftJSON: + return "syft-json" + } + return "unknown" +} + // header describes a loaded SIF file. type header struct { LaunchScript [hdrLaunchLen]byte diff --git a/pkg/sif/testdata/TestNewDescriptorInput/OptSBOMMetadata.golden b/pkg/sif/testdata/TestNewDescriptorInput/OptSBOMMetadata.golden new file mode 100644 index 00000000..02516b13 Binary files /dev/null and b/pkg/sif/testdata/TestNewDescriptorInput/OptSBOMMetadata.golden differ