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(sbom): add support for scanning a sbom attestation #2652

Merged
merged 24 commits into from
Aug 8, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ae0862b
feat(sbom): add support for scanning a sbom attestation
otms61 Aug 2, 2022
6a9cfb6
chore: excecute go mod tidy
otms61 Aug 2, 2022
8f38112
fix: fix goimports lint errors
otms61 Aug 2, 2022
c8e2a6b
feat: support for detecting attest xml
otms61 Aug 2, 2022
50727bb
refactor: move the predicate data access logic to attestation package
otms61 Aug 2, 2022
87b2d21
refacotr: rename cosign predicate data field
otms61 Aug 3, 2022
173b381
refactor: define our own Statement structure and remove the support f…
otms61 Aug 3, 2022
8e3d190
Merge branch 'main' into scan_sbom_attest
otms61 Aug 3, 2022
1be389e
refactor: fix the comment for Decode function
otms61 Aug 3, 2022
f461112
refactor: wrap the error by xerrors
otms61 Aug 3, 2022
b6cf89f
test: add a test for Decode attestaions
otms61 Aug 3, 2022
0921717
test: add a test for Inspect an SBOM attestation
otms61 Aug 3, 2022
031dd8f
refactor(cyclonedx): implement json.Unmarshaler
knqyf263 Aug 4, 2022
3e3438e
Merge branch 'refactor_sbom' into scan_sbom_attest
knqyf263 Aug 4, 2022
5838440
test(attestation): fix expected
knqyf263 Aug 4, 2022
e2b9200
Merge branch 'main' into scan_sbom_attest
knqyf263 Aug 4, 2022
5db9da3
refactor: remove cruft
knqyf263 Aug 4, 2022
03ae38e
test: add an integration test for scanning a sbom
otms61 Aug 4, 2022
bf6f0aa
docs: add a descrition for scanning sbom attestation
otms61 Aug 5, 2022
7219892
docs: update the cosign --type option
otms61 Aug 5, 2022
26cce23
refactor: use .intoto.jsonl extension
otms61 Aug 8, 2022
a0a8cf4
docs: use .intoto.jsonl extension
otms61 Aug 8, 2022
2393a0e
docs: respond to PR feedback
otms61 Aug 8, 2022
661b0eb
refactor: rename TestDecode to TestStatement_UnmarshalJSON
otms61 Aug 8, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ require (
github.com/google/uuid v1.3.0
github.com/google/wire v0.5.0
github.com/hashicorp/go-getter v1.6.2
github.com/in-toto/in-toto-golang v0.3.4-0.20220709202702-fa494aaa0add
github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f
github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d
github.com/knqyf263/go-rpm-version v0.0.0-20220614171824-631e686d1075
Expand All @@ -46,6 +47,7 @@ require (
github.com/owenrumney/go-sarif/v2 v2.1.2
github.com/package-url/packageurl-go v0.1.1-0.20220203205134-d70459300c8a
github.com/samber/lo v1.24.0
github.com/secure-systems-lab/go-securesystemslib v0.4.0
github.com/sosedoff/gitkit v0.3.0
github.com/spf13/cobra v1.5.0
github.com/spf13/pflag v1.0.5
Expand All @@ -64,7 +66,10 @@ require (
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9
)

require github.com/emicklei/go-restful/v3 v3.8.0 // indirect
require (
github.com/emicklei/go-restful/v3 v3.8.0 // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
)

require (
cloud.google.com/go v0.100.2 // indirect
Expand Down
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:z
github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo=
github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA=
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE=
github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU=
github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=
Expand Down Expand Up @@ -922,6 +923,8 @@ github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/in-toto/in-toto-golang v0.3.4-0.20220709202702-fa494aaa0add h1:DAh7mHiRT7wc6kKepYdCpH16ElPciMPQWJaJ7H3l/ng=
github.com/in-toto/in-toto-golang v0.3.4-0.20220709202702-fa494aaa0add/go.mod h1:DQI8vlV6h6qSY/tCOoYKtxjWrkyiNpJ3WTV/WoBllmQ=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/intel/goresctrl v0.2.0/go.mod h1:+CZdzouYFn5EsxgqAQTEzMfwKwuc0fVdMrT9FCCAVRQ=
Expand Down Expand Up @@ -1355,9 +1358,13 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
github.com/secure-systems-lab/go-securesystemslib v0.4.0 h1:b23VGrQhTA8cN2CbBw7/FulN9fTtqYUdS5+Oxzt+DUE=
github.com/secure-systems-lab/go-securesystemslib v0.4.0/go.mod h1:FGBZgq2tXWICsxWQW1msNf49F0Pf2Op5Htayx335Qbs=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE=
github.com/shogo82148/go-shuffle v0.0.0-20170808115208-59829097ff3b h1:VI1u+o2KZPZ5AhuPpXY0JBdpQPnkTx6Dd5XJhK/9MYE=
github.com/shogo82148/go-shuffle v0.0.0-20170808115208-59829097ff3b/go.mod h1:2htx6lmL0NGLHlO8ZCf+lQBGBHIbEujyywxJArf+2Yc=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
Expand Down
49 changes: 49 additions & 0 deletions pkg/attestation/attestation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package attestation

import (
"bytes"
"encoding/base64"
"encoding/json"
"io"

"github.com/in-toto/in-toto-golang/in_toto"
"github.com/secure-systems-lab/go-securesystemslib/dsse"
"golang.org/x/xerrors"
)

type Statement struct {
in_toto.Statement
CosignPredicateData interface{} `json:"-"`
}

// Decode returns the in-toto statement from the in-toto attestation.
func Decode(r io.Reader) (Statement, error) {

var envelope dsse.Envelope
err := json.NewDecoder(r).Decode(&envelope)
if err != nil {
return Statement{}, xerrors.Errorf("failed to decode as a dsse envelope: %w", err)
}
if envelope.PayloadType != in_toto.PayloadType {
return Statement{}, xerrors.Errorf("invalid attestation payload type: %s", envelope.PayloadType)
}

decoded, err := base64.StdEncoding.DecodeString(envelope.Payload)
if err != nil {
return Statement{}, xerrors.Errorf("failed to decode attestation payload: %w", err)
}

var st Statement
err = json.NewDecoder(bytes.NewReader(decoded)).Decode(&st)
if err != nil {
return Statement{}, xerrors.Errorf("failed to decode attestation payload as in-toto statement: %w", err)
}

// When cosign creates an SBOM attestation, it stores the predicate under a "Data" key.
// https://github.com/sigstore/cosign/blob/938ad43f84aa183850014c8cc6d999f4b7ec5e8d/pkg/cosign/attestation/attestation.go#L39-L43
if _, found := st.Predicate.(map[string]interface{})["Data"]; found {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It causes panic if the type assertion fails. Can we make sure the assertion works beforehand?

Suggested change
if _, found := st.Predicate.(map[string]interface{})["Data"]; found {
if cosignPredicate, ok := st.Predicate.(map[string]interface{}); ok {
data, found := cosignPredicate["Data"]
if !found {
return Statement{}, xerrors.Errorf("unsupported predicate format")
}
st.CosignPredicateData = data

Also, we can define our own struct only with needed fields so that we will not have to go back and forth for marshaling/unmarshaling. We can pass json.RawMessage to the SBOM unmarshaler.

predicateByte, err = json.Marshal(attest.CosignPredicateData)
if err != nil {
return sbom.SBOM{}, xerrors.Errorf("failed to marshal predicate: %w", err)
}

// StatementHeader defines the common fields for all statements
type StatementHeader struct {
	PredicateType string    `json:"predicateType"`
}

// CosignPredicate specifies the format of the Custom Predicate.
type CosignPredicate struct {
	Data      json.RawMessage
}

/*
Statement binds the attestation to a particular subject and identifies the
of the predicate. This struct represents a generic statement.
*/
type Statement struct {
	StatementHeader
	// Predicate contains type speficic metadata.
	Predicate CosignPredicate `json:"predicate"`
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea! It will simplify the code!

st.CosignPredicateData = st.Predicate.(map[string]interface{})["Data"]
}

return st, nil
}
3 changes: 3 additions & 0 deletions pkg/fanal/artifact/sbom/sbom.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/sbom"
"github.com/aquasecurity/trivy/pkg/sbom/attestation"
"github.com/aquasecurity/trivy/pkg/sbom/cyclonedx"
)

Expand Down Expand Up @@ -63,6 +64,8 @@ func (a Artifact) Inspect(_ context.Context) (types.ArtifactReference, error) {
switch format {
case sbom.FormatCycloneDXJSON:
unmarshaler = cyclonedx.NewJSONUnmarshaler()
case sbom.FormatAttestCycloneDXJSON:
unmarshaler = attestation.NewUnmarshaler(cyclonedx.NewJSONUnmarshaler())
default:
return types.ArtifactReference{}, xerrors.Errorf("%s scanning is not yet supported", format)

Expand Down
43 changes: 43 additions & 0 deletions pkg/sbom/attestation/attestation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package attestation

import (
"bytes"
"encoding/json"
"io"

"golang.org/x/xerrors"

"github.com/aquasecurity/trivy/pkg/attestation"
"github.com/aquasecurity/trivy/pkg/sbom"
)

type Unmarshaler struct {
predicateUnmarshaler sbom.Unmarshaler
}

func (u Unmarshaler) Unmarshal(r io.Reader) (sbom.SBOM, error) {
attest, err := attestation.Decode(r)
if err != nil {
return sbom.SBOM{}, xerrors.Errorf("failed to decode attestation: %w", err)
}

var predicateByte []byte

switch attest.CosignPredicateData.(type) {
case map[string]interface{}:
predicateByte, err = json.Marshal(attest.CosignPredicateData)
if err != nil {
return sbom.SBOM{}, xerrors.Errorf("failed to marshal predicate: %w", err)
}
case string:
predicateByte = []byte(attest.CosignPredicateData.(string))
}

return u.predicateUnmarshaler.Unmarshal(bytes.NewReader(predicateByte))
}

func NewUnmarshaler(predicateUnmarshaler sbom.Unmarshaler) sbom.Unmarshaler {
return &Unmarshaler{
predicateUnmarshaler: predicateUnmarshaler,
}
}
30 changes: 25 additions & 5 deletions pkg/sbom/sbom.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import (
"io"
"strings"

"github.com/in-toto/in-toto-golang/in_toto"
"golang.org/x/xerrors"

"github.com/aquasecurity/trivy/pkg/attestation"
"github.com/aquasecurity/trivy/pkg/fanal/types"
)

Expand All @@ -26,11 +28,13 @@ type Unmarshaler interface {
type Format string

const (
FormatCycloneDXJSON = "cyclonedx-json"
FormatCycloneDXXML = "cyclonedx-xml"
FormatSPDXJSON = "spdx-json"
FormatSPDXXML = "spdx-xml"
FormatUnknown = "unknown"
FormatCycloneDXJSON = "cyclonedx-json"
FormatCycloneDXXML = "cyclonedx-xml"
FormatSPDXJSON = "spdx-json"
FormatSPDXXML = "spdx-xml"
FormatAttestCycloneDXJSON = "attest-cyclonedx-json"
FormatAttestCycloneDXXML = "attest-cyclonedx-xml"
FormatUnknown = "unknown"
)

func DetectFormat(r io.ReadSeeker) (Format, error) {
Expand Down Expand Up @@ -61,7 +65,23 @@ func DetectFormat(r io.ReadSeeker) (Format, error) {
}
}

if _, err := r.Seek(0, io.SeekStart); err != nil {
return FormatUnknown, xerrors.Errorf("seek error: %w", err)
}

// TODO: implement SPDX

// Try Attestation
if attest, err := attestation.Decode(r); err == nil {
if attest.PredicateType == in_toto.PredicateCycloneDX {
switch attest.CosignPredicateData.(type) {
case map[string]interface{}:
return FormatAttestCycloneDXJSON, nil
case string:
return FormatAttestCycloneDXXML, nil
}
}
}

return FormatUnknown, nil
}