diff --git a/docs/docs/attestation/rekor.md b/docs/docs/attestation/rekor.md index 1b934a3801..303cb55370 100644 --- a/docs/docs/attestation/rekor.md +++ b/docs/docs/attestation/rekor.md @@ -3,14 +3,15 @@ !!! warning "EXPERIMENTAL" This feature might change without preserving backwards compatibility. +## Container images Trivy can retrieve SBOM attestation of the specified container image in the [Rekor][rekor] instance and scan it for vulnerabilities. -## Prerequisites +### Prerequisites 1. SBOM attestation stored in Rekor - See [the "Keyless signing" section][sbom-attest] if you want to upload your SBOM attestation to Rekor. -## Scanning +### Scanning You need to pass `--sbom-sources rekor` so that Trivy will look for SBOM attestation in Rekor. !!! note @@ -54,5 +55,88 @@ If you have your own Rekor instance, you can specify the URL via `--rekor-url`. $ trivy image --sbom-sources rekor --rekor-url https://my-rekor.dev otms61/alpine:3.7.3 ``` +## Non-packaged binaries +Trivy can retrieve SBOM attestation of non-packaged binaries in the [Rekor][rekor] instance and scan it for vulnerabilities. + +### Prerequisites +1. SBOM attestation stored in Rekor + - See [the "Keyless signing" section][sbom-attest] if you want to upload your SBOM attestation to Rekor. + +Cosign currently does not support keyless signing for blob attestation, so use our plugin at the moment. +This example uses a cat clone [bat][bat] written in Rust. +You need to generate SBOM from lock files like `Cargo.lock` at first. + +```bash +$ git clone -b v0.20.0 https://github.com/sharkdp/bat +$ trivy fs --format cyclonedx --output bat.cdx ./bat/Cargo.lock +``` + +Then [our attestation plugin][plugin-attest] allows you to store the SBOM attestation linking to a `bat` binary in the Rekor instance. + +```bash +$ wget https://github.com/sharkdp/bat/releases/download/v0.20.0/bat-v0.20.0-x86_64-apple-darwin.tar.gz +$ tar xvf bat-v0.20.0-x86_64-apple-darwin.tar.gz +$ trivy plugin install github.com/aquasecurity/trivy-plugin-attest +$ trivy attest --predicate ./bat.cdx --type cyclonedx ./bat-v0.20.0-x86_64-apple-darwin/bat +``` + +### Scan a non-packaged binary +Trivy calculates the digest of the `bat` binary and searches for the SBOM attestation by the digest in Rekor. +If it is found, Trivy uses that for vulnerability scanning. + +```bash +$ trivy fs --sbom-sources rekor ./bat-v0.20.0-x86_64-apple-darwin/bat +2022-10-25T13:27:25.950+0300 INFO Found SBOM attestation in Rekor: bat +2022-10-25T13:27:25.993+0300 INFO Number of language-specific files: 1 +2022-10-25T13:27:25.993+0300 INFO Detecting cargo vulnerabilities... + +bat (cargo) +=========== +Total: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 1, CRITICAL: 0) + +┌───────────┬───────────────────┬──────────┬───────────────────┬───────────────┬────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │ +├───────────┼───────────────────┼──────────┼───────────────────┼───────────────┼────────────────────────────────────────────────────────────┤ +│ regex │ CVE-2022-24713 │ HIGH │ 1.5.4 │ 1.5.5 │ Mozilla: Denial of Service via complex regular expressions │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-24713 │ +└───────────┴───────────────────┴──────────┴───────────────────┴───────────────┴────────────────────────────────────────────────────────────┘ +``` + +Also, it is applied to non-packaged binaries even in container images. + +```bash +$ trivy image --sbom-sources rekor --security-checks vuln alpine-with-bat +2022-10-25T13:40:14.920+0300 INFO Vulnerability scanning is enabled +2022-10-25T13:40:18.047+0300 INFO Found SBOM attestation in Rekor: bat +2022-10-25T13:40:18.186+0300 INFO Detected OS: alpine +2022-10-25T13:40:18.186+0300 INFO Detecting Alpine vulnerabilities... +2022-10-25T13:40:18.199+0300 INFO Number of language-specific files: 1 +2022-10-25T13:40:18.199+0300 INFO Detecting cargo vulnerabilities... + +alpine-with-bat (alpine 3.15.6) +=============================== +Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0) + + +bat (cargo) +=========== +Total: 4 (UNKNOWN: 3, LOW: 0, MEDIUM: 0, HIGH: 1, CRITICAL: 0) + +┌───────────┬───────────────────┬──────────┬───────────────────┬───────────────┬────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │ +├───────────┼───────────────────┼──────────┼───────────────────┼───────────────┼────────────────────────────────────────────────────────────┤ +│ regex │ CVE-2022-24713 │ HIGH │ 1.5.4 │ 1.5.5 │ Mozilla: Denial of Service via complex regular expressions │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-24713 │ +└───────────┴───────────────────┴──────────┴───────────────────┴───────────────┴────────────────────────────────────────────────────────────┘ +``` + + +!!! note + The `--sbom-sources rekor` flag slows down the scanning as it queries Rekor on the Internet for all non-packaged binaries. + [rekor]: https://github.com/sigstore/rekor -[sbom-attest]: sbom.md#keyless-signing \ No newline at end of file +[sbom-attest]: sbom.md#keyless-signing + +[plugin-attest]: https://github.com/aquasecurity/trivy-plugin-attest + +[bat]: https://github.com/sharkdp/bat \ No newline at end of file diff --git a/docs/docs/attestation/sbom.md b/docs/docs/attestation/sbom.md index 58bbdc2cf3..5d2667d0e1 100644 --- a/docs/docs/attestation/sbom.md +++ b/docs/docs/attestation/sbom.md @@ -61,7 +61,9 @@ $ COSIGN_EXPERIMENTAL=1 cosign verify-attestation --type cyclonedx Trivy can take an SBOM attestation as input and scan for vulnerabilities. Currently, Trivy supports CycloneDX-type attestation. -In the following example, Cosign can get an CycloneDX-type attestation and trivy scan it. You must create CycloneDX-type attestation before trying the example. To learn more about how to create an CycloneDX-Type attestation and attach it to an image, see the [Sign with a local key pair](#sign-with-a-local-key-pair) section. +In the following example, Cosign can get an CycloneDX-type attestation and trivy scan it. +You must create CycloneDX-type attestation before trying the example. +To learn more about how to create an CycloneDX-Type attestation and attach it to an image, see the [Sign with a local key pair](#sign-with-a-local-key-pair) section. ```bash $ cosign verify-attestation --key /path/to/cosign.pub --type cyclonedx > sbom.cdx.intoto.jsonl diff --git a/pkg/attestation/sbom/rekor.go b/pkg/attestation/sbom/rekor.go new file mode 100644 index 0000000000..7336def6b5 --- /dev/null +++ b/pkg/attestation/sbom/rekor.go @@ -0,0 +1,94 @@ +package sbom + +import ( + "bytes" + "context" + "encoding/json" + "errors" + + "github.com/in-toto/in-toto-golang/in_toto" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/attestation" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/rekor" +) + +var ErrNoSBOMAttestation = xerrors.New("no SBOM attestation found") + +type Rekor struct { + client *rekor.Client +} + +func NewRekor(url string) (Rekor, error) { + c, err := rekor.NewClient(url) + if err != nil { + return Rekor{}, xerrors.Errorf("rekor client error: %w", err) + } + return Rekor{ + client: c, + }, nil +} + +func (r *Rekor) RetrieveSBOM(ctx context.Context, digest string) ([]byte, error) { + entryIDs, err := r.client.Search(ctx, digest) + if err != nil { + return nil, xerrors.Errorf("failed to search rekor records: %w", err) + } else if len(entryIDs) == 0 { + return nil, ErrNoSBOMAttestation + } + + log.Logger.Debugf("Found matching Rekor entries: %s", entryIDs) + + for _, ids := range lo.Chunk[rekor.EntryID](entryIDs, rekor.MaxGetEntriesLimit) { + entries, err := r.client.GetEntries(ctx, ids) + if err != nil { + return nil, xerrors.Errorf("failed to get entries: %w", err) + } + + for _, entry := range entries { + ref, err := r.inspectRecord(entry) + if errors.Is(err, ErrNoSBOMAttestation) { + continue + } else if err != nil { + return nil, xerrors.Errorf("rekor record inspection error: %w", err) + } + return ref, nil + } + } + return nil, ErrNoSBOMAttestation +} + +func (r *Rekor) inspectRecord(entry rekor.Entry) ([]byte, error) { + // TODO: Trivy SBOM should take precedence + raw, err := r.parseStatement(entry) + if err != nil { + return nil, err + } + return raw, nil +} + +func (r *Rekor) parseStatement(entry rekor.Entry) (json.RawMessage, error) { + // Skip base64-encoded attestation + if bytes.HasPrefix(entry.Statement, []byte(`eyJ`)) { + return nil, ErrNoSBOMAttestation + } + + // Parse statement of in-toto attestation + var raw json.RawMessage + statement := &in_toto.Statement{ + Predicate: &attestation.CosignPredicate{ + Data: &raw, // Extract CycloneDX or SPDX + }, + } + if err := json.Unmarshal(entry.Statement, &statement); err != nil { + return nil, xerrors.Errorf("attestation parse error: %w", err) + } + + // TODO: add support for SPDX + if statement.PredicateType != in_toto.PredicateCycloneDX { + return nil, xerrors.Errorf("unsupported predicate type %s: %w", statement.PredicateType, ErrNoSBOMAttestation) + } + return raw, nil +} diff --git a/pkg/attestation/sbom/rekor_test.go b/pkg/attestation/sbom/rekor_test.go new file mode 100644 index 0000000000..788e1e0662 --- /dev/null +++ b/pkg/attestation/sbom/rekor_test.go @@ -0,0 +1,53 @@ +package sbom_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/attestation/sbom" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/rekortest" +) + +func TestRekor_RetrieveSBOM(t *testing.T) { + tests := []struct { + name string + digest string + want string + wantErr string + }{ + { + name: "happy path", + digest: "sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03", + want: `{"bomFormat":"CycloneDX","specVersion":"1.4","version":2}`, + }, + { + name: "404", + digest: "sha256:unknown", + wantErr: "failed to search", + }, + } + + require.NoError(t, log.InitLogger(false, true)) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := rekortest.NewServer(t) + defer ts.Close() + + // Set the testing URL + rc, err := sbom.NewRekor(ts.URL()) + require.NoError(t, err) + + got, err := rc.RetrieveSBOM(context.Background(), tt.digest) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err, tt.name) + assert.Equal(t, tt.want, string(got)) + }) + } +} diff --git a/pkg/commands/artifact/run.go b/pkg/commands/artifact/run.go index 539807884c..c2584b7e30 100644 --- a/pkg/commands/artifact/run.go +++ b/pkg/commands/artifact/run.go @@ -433,6 +433,10 @@ func disabledAnalyzers(opts flag.Options) []analyzer.Type { analyzers = append(analyzers, analyzer.TypeLicenseFile) } + if len(opts.SBOMSources) == 0 { + analyzers = append(analyzers, analyzer.TypeExecutable) + } + return analyzers } diff --git a/pkg/fanal/analyzer/all/import.go b/pkg/fanal/analyzer/all/import.go index a7a80aa56b..45f48992c2 100644 --- a/pkg/fanal/analyzer/all/import.go +++ b/pkg/fanal/analyzer/all/import.go @@ -4,6 +4,7 @@ import ( _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/buildinfo" _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/command/apk" _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config/all" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/executable" _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/c/conan" _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/dotnet/deps" _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/dotnet/nuget" diff --git a/pkg/fanal/analyzer/analyzer.go b/pkg/fanal/analyzer/analyzer.go index 5a1b3e58f2..a48de4995f 100644 --- a/pkg/fanal/analyzer/analyzer.go +++ b/pkg/fanal/analyzer/analyzer.go @@ -10,6 +10,7 @@ import ( "strings" "sync" + "github.com/samber/lo" "golang.org/x/exp/slices" "golang.org/x/sync/semaphore" "golang.org/x/xerrors" @@ -138,8 +139,13 @@ type AnalysisResult struct { Licenses []types.LicenseFile SystemInstalledFiles []string // A list of files installed by OS package manager + // Files holds necessary file contents for the respective post-handler Files map[types.HandlerType][]types.File + // Digests contains SHA-256 digests of unpackaged files + // used to search for SBOM attestation. + Digests map[string]string + // For Red Hat BuildInfo *types.BuildInfo @@ -157,7 +163,7 @@ func NewAnalysisResult() *AnalysisResult { func (r *AnalysisResult) isEmpty() bool { return r.OS == nil && r.Repository == nil && len(r.PackageInfos) == 0 && len(r.Applications) == 0 && len(r.Secrets) == 0 && len(r.Licenses) == 0 && len(r.SystemInstalledFiles) == 0 && - r.BuildInfo == nil && len(r.Files) == 0 && len(r.CustomResources) == 0 + r.BuildInfo == nil && len(r.Files) == 0 && len(r.Digests) == 0 && len(r.CustomResources) == 0 } func (r *AnalysisResult) Sort() { @@ -254,6 +260,11 @@ func (r *AnalysisResult) Merge(new *AnalysisResult) { r.Applications = append(r.Applications, new.Applications...) } + // Merge SHA-256 digests of unpackaged files + if new.Digests != nil { + r.Digests = lo.Assign(r.Digests, new.Digests) + } + for t, files := range new.Files { if v, ok := r.Files[t]; ok { r.Files[t] = append(v, files...) diff --git a/pkg/fanal/analyzer/const.go b/pkg/fanal/analyzer/const.go index 1a25d4475e..4547812922 100644 --- a/pkg/fanal/analyzer/const.go +++ b/pkg/fanal/analyzer/const.go @@ -74,6 +74,11 @@ const ( // C/C++ TypeConanLock Type = "conan-lock" + // ============ + // Non-packaged + // ============ + TypeExecutable Type = "executable" + // ============ // Image Config // ============ diff --git a/pkg/fanal/analyzer/executable/executable.go b/pkg/fanal/analyzer/executable/executable.go new file mode 100644 index 0000000000..45e2cbd28a --- /dev/null +++ b/pkg/fanal/analyzer/executable/executable.go @@ -0,0 +1,56 @@ +package executable + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "io" + "os" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/utils" +) + +func init() { + analyzer.RegisterAnalyzer(&executableAnalyzer{}) +} + +const version = 1 + +// executableAnalyzer calculates SHA-256 for each binary not managed by package managers (called unpackaged binaries) +// so that it can search for SBOM attestation in post-handler. +type executableAnalyzer struct{} + +func (a executableAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + // Skip non-binaries + isBinary, err := utils.IsBinary(input.Content, input.Info.Size()) + if !isBinary || err != nil { + return nil, nil + } + + h := sha256.New() + if _, err = io.Copy(h, input.Content); err != nil { + return nil, xerrors.Errorf("sha256 error: %w", err) + } + s := hex.EncodeToString(h.Sum(nil)) + + return &analyzer.AnalysisResult{ + Digests: map[string]string{ + input.FilePath: "sha256:" + s, + }, + }, nil +} + +func (a executableAnalyzer) Required(_ string, fileInfo os.FileInfo) bool { + return utils.IsExecutable(fileInfo) +} + +func (a executableAnalyzer) Type() analyzer.Type { + return analyzer.TypeExecutable +} + +func (a executableAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/executable/executable_test.go b/pkg/fanal/analyzer/executable/executable_test.go new file mode 100644 index 0000000000..709836a080 --- /dev/null +++ b/pkg/fanal/analyzer/executable/executable_test.go @@ -0,0 +1,53 @@ +package executable + +import ( + "context" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" +) + +func Test_executableAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + filePath string + want *analyzer.AnalysisResult + }{ + { + name: "binary", + filePath: "testdata/binary", + want: &analyzer.AnalysisResult{ + Digests: map[string]string{ + "testdata/binary": "sha256:9f64a747e1b97f131fabb6b447296c9b6f0201e79fb3c5356e6c77e89b6a806a", + }, + }, + }, + { + name: "text", + filePath: "testdata/hello.txt", + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.filePath) + require.NoError(t, err) + defer f.Close() + + stat, err := f.Stat() + require.NoError(t, err) + + a := executableAnalyzer{} + got, err := a.Analyze(context.Background(), analyzer.AnalysisInput{ + FilePath: tt.filePath, + Content: f, + Info: stat, + }) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/executable/testdata/binary b/pkg/fanal/analyzer/executable/testdata/binary new file mode 100644 index 0000000000..82090ee2cb --- /dev/null +++ b/pkg/fanal/analyzer/executable/testdata/binary @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pkg/fanal/analyzer/executable/testdata/hello.txt b/pkg/fanal/analyzer/executable/testdata/hello.txt new file mode 100644 index 0000000000..ce01362503 --- /dev/null +++ b/pkg/fanal/analyzer/executable/testdata/hello.txt @@ -0,0 +1 @@ +hello diff --git a/pkg/fanal/analyzer/secret/secret.go b/pkg/fanal/analyzer/secret/secret.go index f948e702cf..1e13e15d1c 100644 --- a/pkg/fanal/analyzer/secret/secret.go +++ b/pkg/fanal/analyzer/secret/secret.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "io" - "math" "os" "path/filepath" "strings" @@ -13,10 +12,10 @@ import ( "golang.org/x/exp/slices" "golang.org/x/xerrors" - dio "github.com/aquasecurity/go-dep-parser/pkg/io" "github.com/aquasecurity/trivy/pkg/fanal/analyzer" "github.com/aquasecurity/trivy/pkg/fanal/secret" "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/fanal/utils" ) // To make sure SecretAnalyzer implements analyzer.Initializer @@ -78,7 +77,7 @@ func (a *SecretAnalyzer) Init(opt analyzer.AnalyzerOptions) error { func (a *SecretAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { // Do not scan binaries - binary, err := isBinary(input.Content, input.Info.Size()) + binary, err := utils.IsBinary(input.Content, input.Info.Size()) if binary || err != nil { return nil, nil } @@ -110,26 +109,6 @@ func (a *SecretAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput }, nil } -func isBinary(content dio.ReadSeekerAt, fileSize int64) (bool, error) { - headSize := int(math.Min(float64(fileSize), 300)) - head := make([]byte, headSize) - if _, err := content.Read(head); err != nil { - return false, err - } - if _, err := content.Seek(0, io.SeekStart); err != nil { - return false, err - } - - // cf. https://github.com/file/file/blob/f2a6e7cb7db9b5fd86100403df6b2f830c7f22ba/src/encoding.c#L151-L228 - for _, b := range head { - if b < 7 || b == 11 || (13 < b && b < 27) || (27 < b && b < 0x20) || b == 0x7f { - return true, nil - } - } - - return false, nil -} - func (a *SecretAnalyzer) Required(filePath string, fi os.FileInfo) bool { // Skip small files if fi.Size() < 10 { diff --git a/pkg/fanal/artifact/image/image_test.go b/pkg/fanal/artifact/image/image_test.go index 747f8bbdda..403387e262 100644 --- a/pkg/fanal/artifact/image/image_test.go +++ b/pkg/fanal/artifact/image/image_test.go @@ -2,29 +2,16 @@ package image_test import ( "context" - "encoding/json" "errors" - "net/http" - "net/http/httptest" "testing" "time" v1 "github.com/google/go-containerregistry/pkg/v1" - fakei "github.com/google/go-containerregistry/pkg/v1/fake" - "github.com/sigstore/rekor/pkg/generated/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "golang.org/x/exp/slices" "golang.org/x/xerrors" "github.com/aquasecurity/trivy/pkg/fanal/analyzer" - "github.com/aquasecurity/trivy/pkg/fanal/artifact" - image2 "github.com/aquasecurity/trivy/pkg/fanal/artifact/image" - "github.com/aquasecurity/trivy/pkg/fanal/cache" - "github.com/aquasecurity/trivy/pkg/fanal/image" - "github.com/aquasecurity/trivy/pkg/fanal/types" - "github.com/aquasecurity/trivy/pkg/log" - _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/command/apk" _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config/all" _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/php/composer" @@ -36,8 +23,13 @@ import ( _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/pkg/dpkg" _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/repo/apk" _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/secret" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + image2 "github.com/aquasecurity/trivy/pkg/fanal/artifact/image" + "github.com/aquasecurity/trivy/pkg/fanal/cache" _ "github.com/aquasecurity/trivy/pkg/fanal/handler/misconf" _ "github.com/aquasecurity/trivy/pkg/fanal/handler/sysfile" + "github.com/aquasecurity/trivy/pkg/fanal/image" + "github.com/aquasecurity/trivy/pkg/fanal/types" ) func TestArtifact_Inspect(t *testing.T) { @@ -1079,179 +1071,3 @@ func TestArtifact_Inspect(t *testing.T) { }) } } - -type fakeImage struct { - name string - repoDigests []string - fakei.FakeImage - types.ImageExtension -} - -func (f fakeImage) ID() (string, error) { - return "", nil -} - -func (f fakeImage) LayerIDs() ([]string, error) { - return nil, nil -} - -func (f fakeImage) Name() string { - return f.name -} - -func (f fakeImage) RepoDigests() []string { - return f.repoDigests -} - -func TestArtifact_InspectRekorAttestation(t *testing.T) { - type fields struct { - imageName string - repoDigests []string - } - tests := []struct { - name string - fields fields - artifactOpt artifact.Option - searchFile string - putBlobExpectations []cache.ArtifactCachePutBlobExpectation - want types.ArtifactReference - wantErr string - }{ - { - name: "happy path", - fields: fields{ - imageName: "test/image:10", - repoDigests: []string{ - "test/image@sha256:bc41182d7ef5ffc53a40b044e725193bc10142a1243f395ee852a8d9730fc2ad", - }, - }, - searchFile: "testdata/rekor-search.json", - putBlobExpectations: []cache.ArtifactCachePutBlobExpectation{ - { - Args: cache.ArtifactCachePutBlobArgs{ - BlobID: "sha256:8c90c68f385a8067778a200fd3e56e257d4d6dd563e519a7be65902ee0b6e861", - BlobInfo: types.BlobInfo{ - SchemaVersion: types.BlobJSONSchemaVersion, - OS: &types.OS{ - Family: "alpine", - Name: "3.16.2", - }, - PackageInfos: []types.PackageInfo{ - { - Packages: []types.Package{ - { - Name: "musl", - Version: "1.2.3-r0", - SrcName: "musl", - SrcVersion: "1.2.3-r0", - Licenses: []string{"MIT"}, - Ref: "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.2", - Layer: types.Layer{ - DiffID: "sha256:994393dc58e7931862558d06e46aa2bb17487044f670f310dffe1d24e4d1eec7", - }, - }, - }, - }, - }, - }, - }, - Returns: cache.ArtifactCachePutBlobReturns{}, - }, - }, - artifactOpt: artifact.Option{ - SBOMSources: []string{"rekor"}, - }, - want: types.ArtifactReference{ - Name: "test/image:10", - Type: types.ArtifactCycloneDX, - ID: "sha256:8c90c68f385a8067778a200fd3e56e257d4d6dd563e519a7be65902ee0b6e861", - BlobIDs: []string{ - "sha256:8c90c68f385a8067778a200fd3e56e257d4d6dd563e519a7be65902ee0b6e861", - }, - }, - }, - { - name: "503", - fields: fields{ - imageName: "test/image:10", - repoDigests: []string{ - "test/image@sha256:unknown", - }, - }, - searchFile: "testdata/rekor-search.json", - artifactOpt: artifact.Option{ - SBOMSources: []string{"rekor"}, - }, - wantErr: "remote SBOM fetching error", - }, - } - - log.InitLogger(false, true) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/api/v1/index/retrieve": - var params models.SearchIndex - err := json.NewDecoder(r.Body).Decode(¶ms) - require.NoError(t, err) - - if params.Hash == "sha256:bc41182d7ef5ffc53a40b044e725193bc10142a1243f395ee852a8d9730fc2ad" { - http.ServeFile(w, r, tt.searchFile) - } else { - http.Error(w, "something wrong", http.StatusInternalServerError) - } - case "/api/v1/log/entries/retrieve": - var params models.SearchLogQuery - err := json.NewDecoder(r.Body).Decode(¶ms) - require.NoError(t, err) - - if slices.Equal( - params.EntryUUIDs, - []string{ - "392f8ecba72f4326eb624a7403756250b5f2ad58842a99d1653cd6f147f4ce9eda2da350bd908a55", - "392f8ecba72f4326414eaca77bd19bf5f378725d7fd79309605a81b69cc0101f5cd3119d0a216523", - }, - ) { - http.ServeFile(w, r, "testdata/log-entries.json") - } else if slices.Equal( - params.EntryUUIDs, - []string{"392f8ecba72f4326eb624a7403756250b5f2ad58842a99d1653cd6f147f4ce9eda2da350bd908a55"}, - ) { - http.ServeFile(w, r, "testdata/log-entries-no-attestation.json") - } else { - http.Error(w, "something wrong", http.StatusInternalServerError) - } - } - return - })) - defer ts.Close() - - // Set the testing URL - tt.artifactOpt.RekorURL = ts.URL - - mockCache := new(cache.MockArtifactCache) - mockCache.ApplyPutBlobExpectations(tt.putBlobExpectations) - - fi := fakei.FakeImage{} - fi.ConfigFileReturns(nil, nil) - - img := &fakeImage{ - name: tt.fields.imageName, - repoDigests: tt.fields.repoDigests, - FakeImage: fi, - } - a, err := image2.NewArtifact(img, mockCache, tt.artifactOpt) - require.NoError(t, err) - - got, err := a.Inspect(context.Background()) - if tt.wantErr != "" { - assert.ErrorContains(t, err, tt.wantErr) - return - } - require.NoError(t, err, tt.name) - got.CycloneDX = nil - assert.Equal(t, tt.want, got) - }) - } -} diff --git a/pkg/fanal/artifact/image/remote_sbom.go b/pkg/fanal/artifact/image/remote_sbom.go index 45fada52a3..14ba3752e3 100644 --- a/pkg/fanal/artifact/image/remote_sbom.go +++ b/pkg/fanal/artifact/image/remote_sbom.go @@ -1,22 +1,17 @@ package image import ( - "bytes" "context" - "encoding/json" "errors" "os" "strings" - "github.com/in-toto/in-toto-golang/in_toto" - "github.com/samber/lo" "golang.org/x/xerrors" - "github.com/aquasecurity/trivy/pkg/attestation" + sbomatt "github.com/aquasecurity/trivy/pkg/attestation/sbom" "github.com/aquasecurity/trivy/pkg/fanal/artifact/sbom" "github.com/aquasecurity/trivy/pkg/fanal/log" ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" - "github.com/aquasecurity/trivy/pkg/rekor" "github.com/aquasecurity/trivy/pkg/types" ) @@ -47,45 +42,16 @@ func (a Artifact) inspectSBOMAttestation(ctx context.Context) (ftypes.ArtifactRe return ftypes.ArtifactReference{}, xerrors.Errorf("repo digest error: %w", err) } - client, err := rekor.NewClient(a.artifactOption.RekorURL) + client, err := sbomatt.NewRekor(a.artifactOption.RekorURL) if err != nil { return ftypes.ArtifactReference{}, xerrors.Errorf("failed to create rekor client: %w", err) } - entryIDs, err := client.Search(ctx, digest) - if err != nil { - return ftypes.ArtifactReference{}, xerrors.Errorf("failed to search rekor records: %w", err) - } else if len(entryIDs) == 0 { + raw, err := client.RetrieveSBOM(ctx, digest) + if errors.Is(err, sbomatt.ErrNoSBOMAttestation) { return ftypes.ArtifactReference{}, errNoSBOMFound - } - - log.Logger.Debugf("Found matching Rekor entries: %s", entryIDs) - - for _, ids := range lo.Chunk[rekor.EntryID](entryIDs, rekor.MaxGetEntriesLimit) { - entries, err := client.GetEntries(ctx, ids) - if err != nil { - return ftypes.ArtifactReference{}, xerrors.Errorf("failed to get entries: %w", err) - } - - for _, entry := range entries { - ref, err := a.inspectRekorRecord(ctx, entry) - if errors.Is(err, errNoSBOMFound) { - continue - } else if err != nil { - return ftypes.ArtifactReference{}, xerrors.Errorf("rekor record inspection error: %w", err) - } - return ref, nil - } - } - return ftypes.ArtifactReference{}, errNoSBOMFound -} - -func (a Artifact) inspectRekorRecord(ctx context.Context, entry rekor.Entry) (ftypes.ArtifactReference, error) { - - // TODO: Trivy SBOM should take precedence - raw, err := a.parseStatement(entry) - if err != nil { - return ftypes.ArtifactReference{}, err + } else if err != nil { + return ftypes.ArtifactReference{}, xerrors.Errorf("failed to retrieve SBOM attestation: %w", err) } f, err := os.CreateTemp("", "sbom-*") @@ -115,30 +81,6 @@ func (a Artifact) inspectRekorRecord(ctx context.Context, entry rekor.Entry) (ft return results, nil } -func (a Artifact) parseStatement(entry rekor.Entry) (json.RawMessage, error) { - // Skip base64-encoded attestation - if bytes.HasPrefix(entry.Statement, []byte(`eyJ`)) { - return nil, errNoSBOMFound - } - - // Parse statement of in-toto attestation - var raw json.RawMessage - statement := &in_toto.Statement{ - Predicate: &attestation.CosignPredicate{ - Data: &raw, // Extract CycloneDX or SPDX - }, - } - if err := json.Unmarshal(entry.Statement, &statement); err != nil { - return nil, xerrors.Errorf("attestation parse error: %w", err) - } - - // TODO: add support for SPDX - if statement.PredicateType != in_toto.PredicateCycloneDX { - return nil, xerrors.Errorf("unsupported predicate type %s: %w", statement.PredicateType, errNoSBOMFound) - } - return raw, nil -} - func repoDigest(img ftypes.Image) (string, error) { repoNameFull := img.Name() repoName, _, _ := strings.Cut(repoNameFull, ":") diff --git a/pkg/fanal/artifact/image/remote_sbom_test.go b/pkg/fanal/artifact/image/remote_sbom_test.go new file mode 100644 index 0000000000..a2e20c8d3d --- /dev/null +++ b/pkg/fanal/artifact/image/remote_sbom_test.go @@ -0,0 +1,155 @@ +package image_test + +import ( + "context" + "testing" + + fakei "github.com/google/go-containerregistry/pkg/v1/fake" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + image2 "github.com/aquasecurity/trivy/pkg/fanal/artifact/image" + "github.com/aquasecurity/trivy/pkg/fanal/cache" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/rekortest" +) + +type fakeImage struct { + name string + repoDigests []string + fakei.FakeImage + types.ImageExtension +} + +func (f fakeImage) ID() (string, error) { + return "", nil +} + +func (f fakeImage) LayerIDs() ([]string, error) { + return nil, nil +} + +func (f fakeImage) Name() string { + return f.name +} + +func (f fakeImage) RepoDigests() []string { + return f.repoDigests +} + +func TestArtifact_InspectRekorAttestation(t *testing.T) { + type fields struct { + imageName string + repoDigests []string + } + tests := []struct { + name string + fields fields + artifactOpt artifact.Option + putBlobExpectations []cache.ArtifactCachePutBlobExpectation + want types.ArtifactReference + wantErr string + }{ + { + name: "happy path", + fields: fields{ + imageName: "test/image:10", + repoDigests: []string{ + "test/image@sha256:782143e39f1e7a04e3f6da2d88b1c057e5657363c4f90679f3e8a071b7619e02", + }, + }, + putBlobExpectations: []cache.ArtifactCachePutBlobExpectation{ + { + Args: cache.ArtifactCachePutBlobArgs{ + BlobID: "sha256:8c90c68f385a8067778a200fd3e56e257d4d6dd563e519a7be65902ee0b6e861", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + OS: &types.OS{ + Family: "alpine", + Name: "3.16.2", + }, + PackageInfos: []types.PackageInfo{ + { + Packages: []types.Package{ + { + Name: "musl", + Version: "1.2.3-r0", + SrcName: "musl", + SrcVersion: "1.2.3-r0", + Licenses: []string{"MIT"}, + Ref: "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.2", + Layer: types.Layer{ + DiffID: "sha256:994393dc58e7931862558d06e46aa2bb17487044f670f310dffe1d24e4d1eec7", + }, + }, + }, + }, + }, + }, + }, + Returns: cache.ArtifactCachePutBlobReturns{}, + }, + }, + artifactOpt: artifact.Option{ + SBOMSources: []string{"rekor"}, + }, + want: types.ArtifactReference{ + Name: "test/image:10", + Type: types.ArtifactCycloneDX, + ID: "sha256:8c90c68f385a8067778a200fd3e56e257d4d6dd563e519a7be65902ee0b6e861", + BlobIDs: []string{ + "sha256:8c90c68f385a8067778a200fd3e56e257d4d6dd563e519a7be65902ee0b6e861", + }, + }, + }, + { + name: "503", + fields: fields{ + imageName: "test/image:10", + repoDigests: []string{ + "test/image@sha256:unknown", + }, + }, + artifactOpt: artifact.Option{ + SBOMSources: []string{"rekor"}, + }, + wantErr: "remote SBOM fetching error", + }, + } + + require.NoError(t, log.InitLogger(false, true)) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := rekortest.NewServer(t) + defer ts.Close() + + // Set the testing URL + tt.artifactOpt.RekorURL = ts.URL() + + mockCache := new(cache.MockArtifactCache) + mockCache.ApplyPutBlobExpectations(tt.putBlobExpectations) + + fi := fakei.FakeImage{} + fi.ConfigFileReturns(nil, nil) + + img := &fakeImage{ + name: tt.fields.imageName, + repoDigests: tt.fields.repoDigests, + FakeImage: fi, + } + a, err := image2.NewArtifact(img, mockCache, tt.artifactOpt) + require.NoError(t, err) + + got, err := a.Inspect(context.Background()) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err, tt.name) + got.CycloneDX = nil + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/artifact/image/testdata/log-entries-no-attestation.json b/pkg/fanal/artifact/image/testdata/log-entries-no-attestation.json deleted file mode 100644 index 5647c7b89e..0000000000 --- a/pkg/fanal/artifact/image/testdata/log-entries-no-attestation.json +++ /dev/null @@ -1,33 +0,0 @@ -[ - { - "392f8ecba72f4326eb624a7403756250b5f2ad58842a99d1653cd6f147f4ce9eda2da350bd908a55": { - "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI3ODIxNDNlMzlmMWU3YTA0ZTNmNmRhMmQ4OGIxYzA1N2U1NjU3MzYzYzRmOTA2NzlmM2U4YTA3MWI3NjE5ZTAyIn0sInBheWxvYWRIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZWJiZmRkZGE2Mjc3YWYxOTllOTNjNWJiNWNmNTk5OGE3OTMxMWRlMjM4ZTQ5YmNjOGFjMjQxMDI2OTg3NjFiYiJ9fSwicHVibGljS2V5IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTndSRU5EUVdseFowRjNTVUpCWjBsVllXaHNPRUZSZDFsWlYwNVpiblY2ZGxGdk9FVnJOMWRNVFVSdmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEpkMDlFU1RKTlJFVjRUbnBGTTFkb1kwNU5ha2wzVDBSSk1rMUVSWGxPZWtVelYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZMV21aRVF6bHBhbFZ5Y2xwQldFOWpXRllyUVhGSFJVbFRTbEV6VkhScVNuZEpkRUVLZFRFM1JtbDJhV3BuU2sxaFlVaEdORGNyVDNaMk9WUjFla0ZEUTNscFNVVjVVRFV5WlhJMlptRjVibVpLWVZWcU9FdFBRMEZWYTNkblowWkdUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZIUWxkVUNrTXdkVVUzZFRSUWNVUlZSakZZVjBjMFFsVldWVXBCZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBwUldVUldVakJTUVZGSUwwSkNjM2RIV1VWWVl6SkdlbUl5Um5KaFdFcG9UbXBGZUU1RlFtNWlWMFp3WWtNMWFtSXlNSGRMVVZsTFMzZFpRZ3BDUVVkRWRucEJRa0ZSVVdKaFNGSXdZMGhOTmt4NU9XaFpNazUyWkZjMU1HTjVOVzVpTWpsdVlrZFZkVmt5T1hSTlNVZE1RbWR2Y2tKblJVVkJaRm8xQ2tGblVVTkNTREJGWlhkQ05VRklZMEZEUjBOVE9FTm9VeTh5YUVZd1pFWnlTalJUWTFKWFkxbHlRbGs1ZDNwcVUySmxZVGhKWjFreVlqTkpRVUZCUjBNS01UZHRTbWhuUVVGQ1FVMUJVMFJDUjBGcFJVRm9TMDlCU2tkV1ZsaENiMWN4VERSNGFsazVlV0pXT0daVVVYTjVUU3R2VUVwSWVEazVTMjlMWVVwVlF3cEpVVVJDWkRsbGMxUTBNazFTVG5nM1ZtOUJNMXBhS3pWNGFraE5aV1I2YW1WeFEyWm9aVGN2ZDFweFlUbFVRVXRDWjJkeGFHdHFUMUJSVVVSQmQwNXZDa0ZFUW14QmFrVkJjbkJrZVhsRlJqYzNiMkp5VEVOTVVYcHpZbUl4TTJsc05qZDNkek00WTA1MGFtZE5RbWw2WTJWVWFrUmlZMlZMZVZGU04xUktOSE1LWkVOc2Nsa3hZMUJCYWtFNGFYQjZTVVE0VlUxQ2FHeGtTbVV2WlhKR2NHZHROMnN3TldGaWMybFBOM1Y1ZFZadVMyOVZOazByVFhKNlZWVXJaVGxHZHdwSlJHaENhblZSYTFkUll6MEtMUzB0TFMxRlRrUWdRMFZTVkVsR1NVTkJWRVV0TFMwdExRbz0ifX0=", - "integratedTime": 1661476639, - "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d", - "logIndex": 3280165, - "verification": { - "inclusionProof": { - "hashes": [ - "da70e43d33aadff047c7aa4542a1c2c0f039555b4ebb75773659b246a096d983", - "8e6876b02a01bc1e0491802b967f8b490e46fc3fb5e48986c092fe648377801b", - "eaf1241ffc88cfa3bc51697a678122d9425258967d2975ddd43bd720aa693a42", - "9420c625e610223867a58f840505674b0b3d741c24c432505c6738f2ac4f688d", - "be1b7b409a68ebcdc48c8ab773e72008454203fa4412344f25f6b1a13cb49773", - "5950c17122cae78ec19ea5f531887b7b7aad3ce14beeac68b91f115b388725df", - "664dc6a32f46aaa70be3e2206606890bebc928d4e876c405eade9a244778626e", - "48d515eeab9e86cbab194944afbc744e4c589c7b6701f1d635b70d180e0cfa3d", - "296ac93f733e66cf78544a1412c7724f6fd32ad1aeeff6359c89a5047d0093bc", - "ba20fa75d6d10494608f7716384ae46d62968d7a2ac0d1d49101e3b949d38c90", - "6d494b237648126525b08f975c736a55d1f7a64472fcc2782bbc16733c608d7b", - "efb36cfc54705d8cd921a621a9389ffa03956b15d68bfabadac2b4853852079b" - ], - "logIndex": 3280165, - "rootHash": "d714a81604a4a6ea1a6a485296b112b559eea0b6b93580afcb7d382a5944f03f", - "treeSize": 3280179 - }, - "signedEntryTimestamp": "MEQCICXqUEWZzu0q2tk89u7hEBIKCxmRTQGmH+DRcwvdZoPkAiBJEbBsMdLtTWxxg8XNrJ6bRH2QskAJsKnzsgBjAsAo9A==" - } - } - } -] - diff --git a/pkg/fanal/artifact/image/testdata/log-entries.json b/pkg/fanal/artifact/image/testdata/log-entries.json deleted file mode 100644 index 771851df73..0000000000 --- a/pkg/fanal/artifact/image/testdata/log-entries.json +++ /dev/null @@ -1,66 +0,0 @@ -[ - { - "392f8ecba72f4326414eaca77bd19bf5f378725d7fd79309605a81b69cc0101f5cd3119d0a216523": { - "attestation": { - "data": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2N5Y2xvbmVkeC5vcmcvc2NoZW1hIiwic3ViamVjdCI6W3sibmFtZSI6ImluZGV4LmRvY2tlci5pby9rbnF5ZjI2My9jb3NpZ24tdGVzdCIsImRpZ2VzdCI6eyJzaGEyNTYiOiJhNzc3YzljNjZiYTE3N2NjZmVhMjNmMmEyMTZmZjY3MjFlNzhhNjYyY2QxNzAxOTQ4OGM0MTcxMzUyOTljZDg5In19XSwicHJlZGljYXRlIjp7IkRhdGEiOnsiYm9tRm9ybWF0IjoiQ3ljbG9uZURYIiwiY29tcG9uZW50cyI6W3siYm9tLXJlZiI6InBrZzphcGsvYWxwaW5lL211c2xAMS4yLjMtcjA/ZGlzdHJvPTMuMTYuMiIsImxpY2Vuc2VzIjpbeyJleHByZXNzaW9uIjoiTUlUIn1dLCJuYW1lIjoibXVzbCIsInByb3BlcnRpZXMiOlt7Im5hbWUiOiJhcXVhc2VjdXJpdHk6dHJpdnk6UGtnVHlwZSIsInZhbHVlIjoiYWxwaW5lIn0seyJuYW1lIjoiYXF1YXNlY3VyaXR5OnRyaXZ5OlNyY05hbWUiLCJ2YWx1ZSI6Im11c2wifSx7Im5hbWUiOiJhcXVhc2VjdXJpdHk6dHJpdnk6U3JjVmVyc2lvbiIsInZhbHVlIjoiMS4yLjMtcjAifSx7Im5hbWUiOiJhcXVhc2VjdXJpdHk6dHJpdnk6TGF5ZXJEaWZmSUQiLCJ2YWx1ZSI6InNoYTI1Njo5OTQzOTNkYzU4ZTc5MzE4NjI1NThkMDZlNDZhYTJiYjE3NDg3MDQ0ZjY3MGYzMTBkZmZlMWQyNGU0ZDFlZWM3In1dLCJwdXJsIjoicGtnOmFway9hbHBpbmUvbXVzbEAxLjIuMy1yMD9kaXN0cm89My4xNi4yIiwidHlwZSI6ImxpYnJhcnkiLCJ2ZXJzaW9uIjoiMS4yLjMtcjAifSx7ImJvbS1yZWYiOiJmYWQ0ZWI5Ny0zZDJhLTQ0OTktYWNlNy0yYzk0NDQ0MTQ4YTciLCJuYW1lIjoiYWxwaW5lIiwicHJvcGVydGllcyI6W3sibmFtZSI6ImFxdWFzZWN1cml0eTp0cml2eTpUeXBlIiwidmFsdWUiOiJhbHBpbmUifSx7Im5hbWUiOiJhcXVhc2VjdXJpdHk6dHJpdnk6Q2xhc3MiLCJ2YWx1ZSI6Im9zLXBrZ3MifV0sInR5cGUiOiJvcGVyYXRpbmctc3lzdGVtIiwidmVyc2lvbiI6IjMuMTYuMiJ9XSwiZGVwZW5kZW5jaWVzIjpbeyJkZXBlbmRzT24iOlsicGtnOmFway9hbHBpbmUvbXVzbEAxLjIuMy1yMD9kaXN0cm89My4xNi4yIl0sInJlZiI6ImZhZDRlYjk3LTNkMmEtNDQ5OS1hY2U3LTJjOTQ0NDQxNDhhNyJ9LHsiZGVwZW5kc09uIjpbImZhZDRlYjk3LTNkMmEtNDQ5OS1hY2U3LTJjOTQ0NDQxNDhhNyJdLCJyZWYiOiJwa2c6b2NpL2FscGluZUBzaGEyNTY6YmM0MTE4MmQ3ZWY1ZmZjNTNhNDBiMDQ0ZTcyNTE5M2JjMTAxNDJhMTI0M2YzOTVlZTg1MmE4ZDk3MzBmYzJhZD9yZXBvc2l0b3J5X3VybD1pbmRleC5kb2NrZXIuaW8lMkZsaWJyYXJ5JTJGYWxwaW5lXHUwMDI2YXJjaD1hbWQ2NCJ9XSwibWV0YWRhdGEiOnsiY29tcG9uZW50Ijp7ImJvbS1yZWYiOiJwa2c6b2NpL2FscGluZUBzaGEyNTY6YmM0MTE4MmQ3ZWY1ZmZjNTNhNDBiMDQ0ZTcyNTE5M2JjMTAxNDJhMTI0M2YzOTVlZTg1MmE4ZDk3MzBmYzJhZD9yZXBvc2l0b3J5X3VybD1pbmRleC5kb2NrZXIuaW8lMkZsaWJyYXJ5JTJGYWxwaW5lXHUwMDI2YXJjaD1hbWQ2NCIsIm5hbWUiOiJhbHBpbmU6My4xNiIsInByb3BlcnRpZXMiOlt7Im5hbWUiOiJhcXVhc2VjdXJpdHk6dHJpdnk6U2NoZW1hVmVyc2lvbiIsInZhbHVlIjoiMiJ9LHsibmFtZSI6ImFxdWFzZWN1cml0eTp0cml2eTpJbWFnZUlEIiwidmFsdWUiOiJzaGEyNTY6OWM2ZjA3MjQ0NzI4NzNiYjUwYTJhZTY3YTllN2FkY2I1NzY3M2ExODNjZWE4YjA2ZWI3NzhkY2E4NTkxODFiNSJ9LHsibmFtZSI6ImFxdWFzZWN1cml0eTp0cml2eTpSZXBvRGlnZXN0IiwidmFsdWUiOiJhbHBpbmVAc2hhMjU2OmJjNDExODJkN2VmNWZmYzUzYTQwYjA0NGU3MjUxOTNiYzEwMTQyYTEyNDNmMzk1ZWU4NTJhOGQ5NzMwZmMyYWQifSx7Im5hbWUiOiJhcXVhc2VjdXJpdHk6dHJpdnk6RGlmZklEIiwidmFsdWUiOiJzaGEyNTY6OTk0MzkzZGM1OGU3OTMxODYyNTU4ZDA2ZTQ2YWEyYmIxNzQ4NzA0NGY2NzBmMzEwZGZmZTFkMjRlNGQxZWVjNyJ9LHsibmFtZSI6ImFxdWFzZWN1cml0eTp0cml2eTpSZXBvVGFnIiwidmFsdWUiOiJhbHBpbmU6My4xNiJ9XSwicHVybCI6InBrZzpvY2kvYWxwaW5lQHNoYTI1NjpiYzQxMTgyZDdlZjVmZmM1M2E0MGIwNDRlNzI1MTkzYmMxMDE0MmExMjQzZjM5NWVlODUyYThkOTczMGZjMmFkP3JlcG9zaXRvcnlfdXJsPWluZGV4LmRvY2tlci5pbyUyRmxpYnJhcnklMkZhbHBpbmVcdTAwMjZhcmNoPWFtZDY0IiwidHlwZSI6ImNvbnRhaW5lciJ9LCJ0aW1lc3RhbXAiOiIyMDIyLTA5LTE1VDEzOjUzOjQ5KzAwOjAwIiwidG9vbHMiOlt7Im5hbWUiOiJ0cml2eSIsInZlbmRvciI6ImFxdWFzZWN1cml0eSIsInZlcnNpb24iOiJkZXYifV19LCJzZXJpYWxOdW1iZXIiOiJ1cm46dXVpZDo2NDUzZmQ4Mi03MWY0LTQ3YzgtYWQxMi0wMTc3NTYxOWM0NDMiLCJzcGVjVmVyc2lvbiI6IjEuNCIsInZlcnNpb24iOjEsInZ1bG5lcmFiaWxpdGllcyI6W119LCJUaW1lc3RhbXAiOiIifX0=" - }, - "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI3ODIxNDNlMzlmMWU3YTA0ZTNmNmRhMmQ4OGIxYzA1N2U1NjU3MzYzYzRmOTA2NzlmM2U4YTA3MWI3NjE5ZTAyIn0sInBheWxvYWRIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZWJiZmRkZGE2Mjc3YWYxOTllOTNjNWJiNWNmNTk5OGE3OTMxMWRlMjM4ZTQ5YmNjOGFjMjQxMDI2OTg3NjFiYiJ9fSwicHVibGljS2V5IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTndSRU5EUVdseFowRjNTVUpCWjBsVllXaHNPRUZSZDFsWlYwNVpiblY2ZGxGdk9FVnJOMWRNVFVSdmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEpkMDlFU1RKTlJFVjRUbnBGTTFkb1kwNU5ha2wzVDBSSk1rMUVSWGxPZWtVelYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZMV21aRVF6bHBhbFZ5Y2xwQldFOWpXRllyUVhGSFJVbFRTbEV6VkhScVNuZEpkRUVLZFRFM1JtbDJhV3BuU2sxaFlVaEdORGNyVDNaMk9WUjFla0ZEUTNscFNVVjVVRFV5WlhJMlptRjVibVpLWVZWcU9FdFBRMEZWYTNkblowWkdUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZIUWxkVUNrTXdkVVUzZFRSUWNVUlZSakZZVjBjMFFsVldWVXBCZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBwUldVUldVakJTUVZGSUwwSkNjM2RIV1VWWVl6SkdlbUl5Um5KaFdFcG9UbXBGZUU1RlFtNWlWMFp3WWtNMWFtSXlNSGRMVVZsTFMzZFpRZ3BDUVVkRWRucEJRa0ZSVVdKaFNGSXdZMGhOTmt4NU9XaFpNazUyWkZjMU1HTjVOVzVpTWpsdVlrZFZkVmt5T1hSTlNVZE1RbWR2Y2tKblJVVkJaRm8xQ2tGblVVTkNTREJGWlhkQ05VRklZMEZEUjBOVE9FTm9VeTh5YUVZd1pFWnlTalJUWTFKWFkxbHlRbGs1ZDNwcVUySmxZVGhKWjFreVlqTkpRVUZCUjBNS01UZHRTbWhuUVVGQ1FVMUJVMFJDUjBGcFJVRm9TMDlCU2tkV1ZsaENiMWN4VERSNGFsazVlV0pXT0daVVVYTjVUU3R2VUVwSWVEazVTMjlMWVVwVlF3cEpVVVJDWkRsbGMxUTBNazFTVG5nM1ZtOUJNMXBhS3pWNGFraE5aV1I2YW1WeFEyWm9aVGN2ZDFweFlUbFVRVXRDWjJkeGFHdHFUMUJSVVVSQmQwNXZDa0ZFUW14QmFrVkJjbkJrZVhsRlJqYzNiMkp5VEVOTVVYcHpZbUl4TTJsc05qZDNkek00WTA1MGFtZE5RbWw2WTJWVWFrUmlZMlZMZVZGU04xUktOSE1LWkVOc2Nsa3hZMUJCYWtFNGFYQjZTVVE0VlUxQ2FHeGtTbVV2WlhKR2NHZHROMnN3TldGaWMybFBOM1Y1ZFZadVMyOVZOazByVFhKNlZWVXJaVGxHZHdwSlJHaENhblZSYTFkUll6MEtMUzB0TFMxRlRrUWdRMFZTVkVsR1NVTkJWRVV0TFMwdExRbz0ifX0=", - "integratedTime": 1661476639, - "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d", - "logIndex": 3280165, - "verification": { - "inclusionProof": { - "hashes": [ - "da70e43d33aadff047c7aa4542a1c2c0f039555b4ebb75773659b246a096d983", - "8e6876b02a01bc1e0491802b967f8b490e46fc3fb5e48986c092fe648377801b", - "eaf1241ffc88cfa3bc51697a678122d9425258967d2975ddd43bd720aa693a42", - "9420c625e610223867a58f840505674b0b3d741c24c432505c6738f2ac4f688d", - "be1b7b409a68ebcdc48c8ab773e72008454203fa4412344f25f6b1a13cb49773", - "5950c17122cae78ec19ea5f531887b7b7aad3ce14beeac68b91f115b388725df", - "664dc6a32f46aaa70be3e2206606890bebc928d4e876c405eade9a244778626e", - "48d515eeab9e86cbab194944afbc744e4c589c7b6701f1d635b70d180e0cfa3d", - "296ac93f733e66cf78544a1412c7724f6fd32ad1aeeff6359c89a5047d0093bc", - "ba20fa75d6d10494608f7716384ae46d62968d7a2ac0d1d49101e3b949d38c90", - "6d494b237648126525b08f975c736a55d1f7a64472fcc2782bbc16733c608d7b", - "efb36cfc54705d8cd921a621a9389ffa03956b15d68bfabadac2b4853852079b" - ], - "logIndex": 3280165, - "rootHash": "d714a81604a4a6ea1a6a485296b112b559eea0b6b93580afcb7d382a5944f03f", - "treeSize": 3280179 - }, - "signedEntryTimestamp": "MEQCICXqUEWZzu0q2tk89u7hEBIKCxmRTQGmH+DRcwvdZoPkAiBJEbBsMdLtTWxxg8XNrJ6bRH2QskAJsKnzsgBjAsAo9A==" - } - } - }, - { - "392f8ecba72f4326eb624a7403756250b5f2ad58842a99d1653cd6f147f4ce9eda2da350bd908a55": { - "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI3ODIxNDNlMzlmMWU3YTA0ZTNmNmRhMmQ4OGIxYzA1N2U1NjU3MzYzYzRmOTA2NzlmM2U4YTA3MWI3NjE5ZTAyIn0sInBheWxvYWRIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZWJiZmRkZGE2Mjc3YWYxOTllOTNjNWJiNWNmNTk5OGE3OTMxMWRlMjM4ZTQ5YmNjOGFjMjQxMDI2OTg3NjFiYiJ9fSwicHVibGljS2V5IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTndSRU5EUVdseFowRjNTVUpCWjBsVllXaHNPRUZSZDFsWlYwNVpiblY2ZGxGdk9FVnJOMWRNVFVSdmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEpkMDlFU1RKTlJFVjRUbnBGTTFkb1kwNU5ha2wzVDBSSk1rMUVSWGxPZWtVelYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZMV21aRVF6bHBhbFZ5Y2xwQldFOWpXRllyUVhGSFJVbFRTbEV6VkhScVNuZEpkRUVLZFRFM1JtbDJhV3BuU2sxaFlVaEdORGNyVDNaMk9WUjFla0ZEUTNscFNVVjVVRFV5WlhJMlptRjVibVpLWVZWcU9FdFBRMEZWYTNkblowWkdUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZIUWxkVUNrTXdkVVUzZFRSUWNVUlZSakZZVjBjMFFsVldWVXBCZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBwUldVUldVakJTUVZGSUwwSkNjM2RIV1VWWVl6SkdlbUl5Um5KaFdFcG9UbXBGZUU1RlFtNWlWMFp3WWtNMWFtSXlNSGRMVVZsTFMzZFpRZ3BDUVVkRWRucEJRa0ZSVVdKaFNGSXdZMGhOTmt4NU9XaFpNazUyWkZjMU1HTjVOVzVpTWpsdVlrZFZkVmt5T1hSTlNVZE1RbWR2Y2tKblJVVkJaRm8xQ2tGblVVTkNTREJGWlhkQ05VRklZMEZEUjBOVE9FTm9VeTh5YUVZd1pFWnlTalJUWTFKWFkxbHlRbGs1ZDNwcVUySmxZVGhKWjFreVlqTkpRVUZCUjBNS01UZHRTbWhuUVVGQ1FVMUJVMFJDUjBGcFJVRm9TMDlCU2tkV1ZsaENiMWN4VERSNGFsazVlV0pXT0daVVVYTjVUU3R2VUVwSWVEazVTMjlMWVVwVlF3cEpVVVJDWkRsbGMxUTBNazFTVG5nM1ZtOUJNMXBhS3pWNGFraE5aV1I2YW1WeFEyWm9aVGN2ZDFweFlUbFVRVXRDWjJkeGFHdHFUMUJSVVVSQmQwNXZDa0ZFUW14QmFrVkJjbkJrZVhsRlJqYzNiMkp5VEVOTVVYcHpZbUl4TTJsc05qZDNkek00WTA1MGFtZE5RbWw2WTJWVWFrUmlZMlZMZVZGU04xUktOSE1LWkVOc2Nsa3hZMUJCYWtFNGFYQjZTVVE0VlUxQ2FHeGtTbVV2WlhKR2NHZHROMnN3TldGaWMybFBOM1Y1ZFZadVMyOVZOazByVFhKNlZWVXJaVGxHZHdwSlJHaENhblZSYTFkUll6MEtMUzB0TFMxRlRrUWdRMFZTVkVsR1NVTkJWRVV0TFMwdExRbz0ifX0=", - "integratedTime": 1661476639, - "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d", - "logIndex": 3280165, - "verification": { - "inclusionProof": { - "hashes": [ - "da70e43d33aadff047c7aa4542a1c2c0f039555b4ebb75773659b246a096d983", - "8e6876b02a01bc1e0491802b967f8b490e46fc3fb5e48986c092fe648377801b", - "eaf1241ffc88cfa3bc51697a678122d9425258967d2975ddd43bd720aa693a42", - "9420c625e610223867a58f840505674b0b3d741c24c432505c6738f2ac4f688d", - "be1b7b409a68ebcdc48c8ab773e72008454203fa4412344f25f6b1a13cb49773", - "5950c17122cae78ec19ea5f531887b7b7aad3ce14beeac68b91f115b388725df", - "664dc6a32f46aaa70be3e2206606890bebc928d4e876c405eade9a244778626e", - "48d515eeab9e86cbab194944afbc744e4c589c7b6701f1d635b70d180e0cfa3d", - "296ac93f733e66cf78544a1412c7724f6fd32ad1aeeff6359c89a5047d0093bc", - "ba20fa75d6d10494608f7716384ae46d62968d7a2ac0d1d49101e3b949d38c90", - "6d494b237648126525b08f975c736a55d1f7a64472fcc2782bbc16733c608d7b", - "efb36cfc54705d8cd921a621a9389ffa03956b15d68bfabadac2b4853852079b" - ], - "logIndex": 3280165, - "rootHash": "d714a81604a4a6ea1a6a485296b112b559eea0b6b93580afcb7d382a5944f03f", - "treeSize": 3280179 - }, - "signedEntryTimestamp": "MEQCICXqUEWZzu0q2tk89u7hEBIKCxmRTQGmH+DRcwvdZoPkAiBJEbBsMdLtTWxxg8XNrJ6bRH2QskAJsKnzsgBjAsAo9A==" - } - } - } -] - diff --git a/pkg/fanal/artifact/image/testdata/rekor-search.json b/pkg/fanal/artifact/image/testdata/rekor-search.json deleted file mode 100644 index ed62fc9731..0000000000 --- a/pkg/fanal/artifact/image/testdata/rekor-search.json +++ /dev/null @@ -1,4 +0,0 @@ -[ - "392f8ecba72f4326eb624a7403756250b5f2ad58842a99d1653cd6f147f4ce9eda2da350bd908a55", - "392f8ecba72f4326414eaca77bd19bf5f378725d7fd79309605a81b69cc0101f5cd3119d0a216523" -] diff --git a/pkg/fanal/artifact/sbom/sbom.go b/pkg/fanal/artifact/sbom/sbom.go index d89e58b6a0..a9e26ec6c2 100644 --- a/pkg/fanal/artifact/sbom/sbom.go +++ b/pkg/fanal/artifact/sbom/sbom.go @@ -4,14 +4,12 @@ import ( "context" "crypto/sha256" "encoding/json" - "io" "os" "path/filepath" digest "github.com/opencontainers/go-digest" "golang.org/x/xerrors" - "github.com/aquasecurity/trivy/pkg/attestation" "github.com/aquasecurity/trivy/pkg/fanal/analyzer" "github.com/aquasecurity/trivy/pkg/fanal/artifact" "github.com/aquasecurity/trivy/pkg/fanal/cache" @@ -19,8 +17,6 @@ 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/cyclonedx" - "github.com/aquasecurity/trivy/pkg/sbom/spdx" ) type Artifact struct { @@ -54,12 +50,7 @@ func (a Artifact) Inspect(_ context.Context) (types.ArtifactReference, error) { } log.Logger.Infof("Detected SBOM format: %s", format) - // Rewind the SBOM file - if _, err = f.Seek(0, io.SeekStart); err != nil { - return types.ArtifactReference{}, xerrors.Errorf("seek error: %w", err) - } - - bom, err := a.Decode(f, format) + bom, err := sbom.Decode(f, format) if err != nil { return types.ArtifactReference{}, xerrors.Errorf("SBOM decode error: %w", err) } @@ -100,48 +91,6 @@ func (a Artifact) Inspect(_ context.Context) (types.ArtifactReference, error) { }, nil } -func (a Artifact) Decode(f io.Reader, format sbom.Format) (sbom.SBOM, error) { - var ( - v interface{} - bom sbom.SBOM - decoder interface{ Decode(any) error } - ) - - switch format { - case sbom.FormatCycloneDXJSON: - v = &cyclonedx.CycloneDX{SBOM: &bom} - decoder = json.NewDecoder(f) - case sbom.FormatAttestCycloneDXJSON: - // dsse envelope - // => in-toto attestation - // => cosign predicate - // => CycloneDX JSON - v = &attestation.Statement{ - Predicate: &attestation.CosignPredicate{ - Data: &cyclonedx.CycloneDX{SBOM: &bom}, - }, - } - decoder = json.NewDecoder(f) - case sbom.FormatSPDXJSON: - v = &spdx.SPDX{SBOM: &bom} - decoder = json.NewDecoder(f) - case sbom.FormatSPDXTV: - v = &spdx.SPDX{SBOM: &bom} - decoder = spdx.NewTVDecoder(f) - - default: - return sbom.SBOM{}, xerrors.Errorf("%s scanning is not yet supported", format) - - } - - // Decode a file content into sbom.SBOM - if err := decoder.Decode(v); err != nil { - return sbom.SBOM{}, xerrors.Errorf("failed to decode: %w", err) - } - - return bom, nil -} - func (a Artifact) Clean(reference types.ArtifactReference) error { return a.cache.DeleteBlobs(reference.BlobIDs) } diff --git a/pkg/fanal/handler/all/import.go b/pkg/fanal/handler/all/import.go index 3025b9f703..afe9c37bc3 100644 --- a/pkg/fanal/handler/all/import.go +++ b/pkg/fanal/handler/all/import.go @@ -4,4 +4,5 @@ import ( _ "github.com/aquasecurity/trivy/pkg/fanal/handler/gomod" _ "github.com/aquasecurity/trivy/pkg/fanal/handler/misconf" _ "github.com/aquasecurity/trivy/pkg/fanal/handler/sysfile" + _ "github.com/aquasecurity/trivy/pkg/fanal/handler/unpackaged" ) diff --git a/pkg/fanal/handler/unpackaged/unpackaged.go b/pkg/fanal/handler/unpackaged/unpackaged.go new file mode 100644 index 0000000000..5f450c7923 --- /dev/null +++ b/pkg/fanal/handler/unpackaged/unpackaged.go @@ -0,0 +1,92 @@ +package unpackaged + +import ( + "bytes" + "context" + "errors" + + "golang.org/x/exp/slices" + "golang.org/x/xerrors" + + sbomatt "github.com/aquasecurity/trivy/pkg/attestation/sbom" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/fanal/handler" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/sbom" +) + +func init() { + handler.RegisterPostHandlerInit(types.UnpackagedPostHandler, NewUnpackagedHandler) +} + +const version = 1 + +type unpackagedHook struct { + client sbomatt.Rekor +} + +func NewUnpackagedHandler(opt artifact.Option) (handler.PostHandler, error) { + c, err := sbomatt.NewRekor(opt.RekorURL) + if err != nil { + return nil, xerrors.Errorf("rekor client error: %w", err) + } + return unpackagedHook{ + client: c, + }, nil +} + +// Handle retrieves SBOM of unpackaged executable files in Rekor. +func (h unpackagedHook) Handle(ctx context.Context, res *analyzer.AnalysisResult, blob *types.BlobInfo) error { + for filePath, digest := range res.Digests { + // Skip files installed by OS package managers. + if slices.Contains(res.SystemInstalledFiles, filePath) { + continue + } + + // Retrieve SBOM from Rekor according to the file digest. + raw, err := h.client.RetrieveSBOM(ctx, digest) + if errors.Is(err, sbomatt.ErrNoSBOMAttestation) { + continue + } else if err != nil { + return err + } + + r := bytes.NewReader(raw) + + // Detect the SBOM format like CycloneDX, SPDX, etc. + format, err := sbom.DetectFormat(r) + if err != nil { + return err + } + + // Parse the fetched SBOM + bom, err := sbom.Decode(bytes.NewReader(raw), format) + if err != nil { + return err + } + + if len(bom.Applications) > 0 { + log.Logger.Infof("Found SBOM attestation in Rekor: %s", filePath) + // Take the first app since this SBOM should contain a single application. + app := bom.Applications[0] + app.FilePath = filePath // Use the original file path rather than the one in the SBOM. + blob.Applications = append(blob.Applications, app) + } + } + + return nil +} + +func (h unpackagedHook) Version() int { + return version +} + +func (h unpackagedHook) Type() types.HandlerType { + return types.UnpackagedPostHandler +} + +func (h unpackagedHook) Priority() int { + return types.UnpackagedPostHandlerPriority +} diff --git a/pkg/fanal/handler/unpackaged/unpackaged_test.go b/pkg/fanal/handler/unpackaged/unpackaged_test.go new file mode 100644 index 0000000000..b82e252eec --- /dev/null +++ b/pkg/fanal/handler/unpackaged/unpackaged_test.go @@ -0,0 +1,91 @@ +package unpackaged_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/fanal/handler/unpackaged" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/rekortest" +) + +func Test_unpackagedHook_Handle(t *testing.T) { + type args struct { + res *analyzer.AnalysisResult + blob *types.BlobInfo + } + tests := []struct { + name string + args args + want *types.BlobInfo + wantErr string + }{ + { + name: "happy path", + args: args{ + res: &analyzer.AnalysisResult{ + Digests: map[string]string{ + "go.mod": "sha256:23f4e10c43c7654e33a3c9570913c8c9c528292762f1a5c4a97253e9e4e4b238", + }, + }, + }, + want: &types.BlobInfo{ + Applications: []types.Application{ + { + Type: types.GoModule, + FilePath: "go.mod", + Libraries: []types.Package{ + { + Name: "github.com/spf13/cobra", + Version: "1.5.0", + Ref: "pkg:golang/github.com/spf13/cobra@1.5.0", + }, + }, + }, + }, + }, + }, + { + name: "404", + args: args{ + res: &analyzer.AnalysisResult{ + Digests: map[string]string{ + "go.mod": "sha256:unknown", + }, + }, + }, + wantErr: "failed to search", + }, + } + + require.NoError(t, log.InitLogger(false, true)) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := rekortest.NewServer(t) + defer ts.Close() + + // Set the testing URL + opt := artifact.Option{ + RekorURL: ts.URL(), + } + + got := &types.BlobInfo{} + h, err := unpackaged.NewUnpackagedHandler(opt) + require.NoError(t, err) + + err = h.Handle(context.Background(), tt.args.res, got) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err, tt.name) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/test/integration/containerd_test.go b/pkg/fanal/test/integration/containerd_test.go index c205c69242..157b016f0a 100644 --- a/pkg/fanal/test/integration/containerd_test.go +++ b/pkg/fanal/test/integration/containerd_test.go @@ -13,6 +13,8 @@ import ( "testing" "time" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/containerd/containerd" "github.com/containerd/containerd/namespaces" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -422,7 +424,12 @@ func TestContainerd_LocalImage(t *testing.T) { require.NoError(t, err) defer cleanup() - ar, err := aimage.NewArtifact(img, c, artifact.Option{}) + ar, err := aimage.NewArtifact(img, c, artifact.Option{ + DisabledAnalyzers: []analyzer.Type{ + analyzer.TypeExecutable, + analyzer.TypeLicenseFile, + }, + }) require.NoError(t, err) ref, err := ar.Inspect(ctx) @@ -545,7 +552,12 @@ func TestContainerd_PullImage(t *testing.T) { require.NoError(t, err) defer cleanup() - art, err := aimage.NewArtifact(img, c, artifact.Option{}) + art, err := aimage.NewArtifact(img, c, artifact.Option{ + DisabledAnalyzers: []analyzer.Type{ + analyzer.TypeExecutable, + analyzer.TypeLicenseFile, + }, + }) require.NoError(t, err) require.NotNil(t, art) diff --git a/pkg/fanal/test/integration/library_test.go b/pkg/fanal/test/integration/library_test.go index 458f530a64..63623f2c08 100644 --- a/pkg/fanal/test/integration/library_test.go +++ b/pkg/fanal/test/integration/library_test.go @@ -130,7 +130,10 @@ func TestFanal_Library_DockerLessMode(t *testing.T) { // don't scan licenses in the test - in parallel it will fail ar, err := aimage.NewArtifact(img, c, artifact.Option{ - DisabledAnalyzers: []analyzer.Type{analyzer.TypeLicenseFile}, + DisabledAnalyzers: []analyzer.Type{ + analyzer.TypeExecutable, + analyzer.TypeLicenseFile, + }, }) require.NoError(t, err) @@ -176,7 +179,10 @@ func TestFanal_Library_DockerMode(t *testing.T) { ar, err := aimage.NewArtifact(img, c, artifact.Option{ // disable license checking in the test - in parallel it will fail because of resource requirement - DisabledAnalyzers: []analyzer.Type{analyzer.TypeLicenseFile}, + DisabledAnalyzers: []analyzer.Type{ + analyzer.TypeExecutable, + analyzer.TypeLicenseFile, + }, }) require.NoError(t, err) @@ -212,7 +218,10 @@ func TestFanal_Library_TarMode(t *testing.T) { require.NoError(t, err, tt.name) ar, err := aimage.NewArtifact(img, c, artifact.Option{ - DisabledAnalyzers: []analyzer.Type{analyzer.TypeLicenseFile}, + DisabledAnalyzers: []analyzer.Type{ + analyzer.TypeExecutable, + analyzer.TypeLicenseFile, + }, }) require.NoError(t, err) diff --git a/pkg/fanal/test/integration/registry_test.go b/pkg/fanal/test/integration/registry_test.go index 3df7d75b5e..61aa71498c 100644 --- a/pkg/fanal/test/integration/registry_test.go +++ b/pkg/fanal/test/integration/registry_test.go @@ -12,6 +12,8 @@ import ( "path/filepath" "testing" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/docker/docker/client" "github.com/docker/go-connections/nat" "github.com/stretchr/testify/assert" @@ -202,7 +204,12 @@ func analyze(ctx context.Context, imageRef string, opt types.DockerOption) (*typ } defer cleanup() - ar, err := aimage.NewArtifact(img, c, artifact.Option{}) + ar, err := aimage.NewArtifact(img, c, artifact.Option{ + DisabledAnalyzers: []analyzer.Type{ + analyzer.TypeExecutable, + analyzer.TypeLicenseFile, + }, + }) if err != nil { return nil, err } diff --git a/pkg/fanal/types/handler.go b/pkg/fanal/types/handler.go index 0ac4f167c4..1be3c1bcdb 100644 --- a/pkg/fanal/types/handler.go +++ b/pkg/fanal/types/handler.go @@ -6,6 +6,7 @@ const ( SystemFileFilteringPostHandler HandlerType = "system-file-filter" GoModMergePostHandler HandlerType = "go-mod-merge" MisconfPostHandler HandlerType = "misconf" + UnpackagedPostHandler HandlerType = "unpackaged" // SystemFileFilteringPostHandlerPriority should be higher than other handlers. // Otherwise, other handlers need to process unnecessary files. @@ -13,4 +14,5 @@ const ( GoModMergePostHandlerPriority = 50 MisconfPostHandlerPriority = 50 + UnpackagedPostHandlerPriority = 50 ) diff --git a/pkg/fanal/utils/utils.go b/pkg/fanal/utils/utils.go index 1556d9100c..c947033011 100644 --- a/pkg/fanal/utils/utils.go +++ b/pkg/fanal/utils/utils.go @@ -3,8 +3,12 @@ package utils import ( "bufio" "fmt" + "io" + "math" "os" "os/exec" + + dio "github.com/aquasecurity/go-dep-parser/pkg/io" ) var ( @@ -57,9 +61,29 @@ func IsExecutable(fileInfo os.FileInfo) bool { return false } - // Check executable file + // Check unpackaged file if mode.Perm()&0111 != 0 { return true } return false } + +func IsBinary(content dio.ReadSeekerAt, fileSize int64) (bool, error) { + headSize := int(math.Min(float64(fileSize), 300)) + head := make([]byte, headSize) + if _, err := content.Read(head); err != nil { + return false, err + } + if _, err := content.Seek(0, io.SeekStart); err != nil { + return false, err + } + + // cf. https://github.com/file/file/blob/f2a6e7cb7db9b5fd86100403df6b2f830c7f22ba/src/encoding.c#L151-L228 + for _, b := range head { + if b < 7 || b == 11 || (13 < b && b < 27) || (27 < b && b < 0x20) || b == 0x7f { + return true, nil + } + } + + return false, nil +} diff --git a/pkg/rekor/client.go b/pkg/rekor/client.go index 02b0ee5654..a902c3a6f8 100644 --- a/pkg/rekor/client.go +++ b/pkg/rekor/client.go @@ -23,7 +23,7 @@ const ( uuidLen = 64 ) -var ErrOverGetEntriesLimit = xerrors.Errorf("Over get entries limit") +var ErrOverGetEntriesLimit = xerrors.Errorf("over get entries limit") // EntryID is a hex-format string. The length of the string is 80 or 64. // If the length is 80, it consists of two elements, the TreeID and the UUID. If the length is 64, diff --git a/pkg/rekortest/server.go b/pkg/rekortest/server.go new file mode 100644 index 0000000000..863aa90c2e --- /dev/null +++ b/pkg/rekortest/server.go @@ -0,0 +1,315 @@ +package rekortest + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/CycloneDX/cyclonedx-go" + "github.com/in-toto/in-toto-golang/in_toto" + slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" + "github.com/samber/lo" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/attestation" +) + +var ( + indexRes = map[string][]string{ + // Contain a SBOM attestation for a container image + "sha256:782143e39f1e7a04e3f6da2d88b1c057e5657363c4f90679f3e8a071b7619e02": { + "392f8ecba72f4326eb624a7403756250b5f2ad58842a99d1653cd6f147f4ce9eda2da350bd908a55", + "392f8ecba72f4326414eaca77bd19bf5f378725d7fd79309605a81b69cc0101f5cd3119d0a216523", + }, + // Contain a SBOM attestation for go.mod + "sha256:23f4e10c43c7654e33a3c9570913c8c9c528292762f1a5c4a97253e9e4e4b238": { + "24296fb24b8ad77aa715cdfd264ce34c4d544375d7bd7cd029bf5a48ef25217a13fdba562e0889ca", + }, + // Contain an empty SBOM attestation + "sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03": { + "24296fb24b8ad77a8d47be2e40bfe910f0ffc842e86b5685dd85d1c903ef78bb6362125816426fe9", + }, + } + + imageSBOMAttestation = in_toto.Statement{ + StatementHeader: in_toto.StatementHeader{ + Type: "https://in-toto.io/Statement/v0.1", + PredicateType: "https://cyclonedx.org/schema", + Subject: []in_toto.Subject{ + { + Name: "index.docker.io/knqyf263/cosign-test", + Digest: slsa.DigestSet{ + "sha256": "a777c9c66ba177ccfea23f2a216ff6721e78a662cd17019488c417135299cd89", + }, + }, + }, + }, + Predicate: &attestation.CosignPredicate{ + Data: &cyclonedx.BOM{ + BOMFormat: cyclonedx.BOMFormat, + SerialNumber: "urn:uuid:6453fd82-71f4-47c8-ad12-01775619c443", + SpecVersion: "1.4", + Version: 1, + Metadata: &cyclonedx.Metadata{ + Timestamp: "2022-09-15T13:53:49+00:00", + Tools: &[]cyclonedx.Tool{ + { + Vendor: "aquasecurity", + Name: "trivy", + Version: "dev", + }, + }, + Component: &cyclonedx.Component{ + BOMRef: "pkg:oci/alpine@sha256:bc41182d7ef5ffc53a40b044e725193bc10142a1243f395ee852a8d9730fc2ad?repository_url=index.docker.io%2Flibrary%2Falpine\u0026arch=amd64", + Type: cyclonedx.ComponentTypeContainer, + Name: "alpine:3.16", + PackageURL: "pkg:oci/alpine@sha256:bc41182d7ef5ffc53a40b044e725193bc10142a1243f395ee852a8d9730fc2ad?repository_url=index.docker.io%2Flibrary%2Falpine\u0026arch=amd64", + Properties: &[]cyclonedx.Property{ + {Name: "aquasecurity:trivy:SchemaVersion", Value: "2"}, + {Name: "aquasecurity:trivy:ImageID", Value: "sha256:9c6f0724472873bb50a2ae67a9e7adcb57673a183cea8b06eb778dca859181b5"}, + {Name: "aquasecurity:trivy:RepoDigest", Value: "alpine@sha256:bc41182d7ef5ffc53a40b044e725193bc10142a1243f395ee852a8d9730fc2ad"}, + {Name: "aquasecurity:trivy:DiffID", Value: "sha256:994393dc58e7931862558d06e46aa2bb17487044f670f310dffe1d24e4d1eec7"}, + {Name: "aquasecurity:trivy:RepoTag", Value: "alpine:3.16"}, + }, + }, + }, + Components: &[]cyclonedx.Component{ + { + BOMRef: "fad4eb97-3d2a-4499-ace7-2c94444148a7", + Type: cyclonedx.ComponentTypeOS, + Name: "alpine", + Version: "3.16.2", + Properties: &[]cyclonedx.Property{ + {Name: "aquasecurity:trivy:Type", Value: "alpine"}, + {Name: "aquasecurity:trivy:Class", Value: "os-pkgs"}, + }, + }, + { + BOMRef: "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.2", + Type: cyclonedx.ComponentTypeLibrary, + Name: "musl", + Version: "1.2.3-r0", + Licenses: &cyclonedx.Licenses{ + {Expression: "MIT"}, + }, + PackageURL: "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.2", + Properties: &[]cyclonedx.Property{ + {Name: "aquasecurity:trivy:PkgType", Value: "alpine"}, + {Name: "aquasecurity:trivy:SrcName", Value: "musl"}, + {Name: "aquasecurity:trivy:SrcVersion", Value: "1.2.3-r0"}, + {Name: "aquasecurity:trivy:LayerDiffID", Value: "sha256:994393dc58e7931862558d06e46aa2bb17487044f670f310dffe1d24e4d1eec7"}, + }, + }, + }, + Dependencies: &[]cyclonedx.Dependency{ + { + Ref: "pkg:oci/alpine@sha256:bc41182d7ef5ffc53a40b044e725193bc10142a1243f395ee852a8d9730fc2ad?repository_url=index.docker.io%2Flibrary%2Falpine&6arch=amd64", + Dependencies: &[]cyclonedx.Dependency{ + {Ref: "fad4eb97-3d2a-4499-ace7-2c94444148a7"}, + }, + }, + { + Ref: "fad4eb97-3d2a-4499-ace7-2c94444148a7", + Dependencies: &[]cyclonedx.Dependency{ + {Ref: "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.2"}, + }, + }, + }, + }, + }, + } + + gomodSBOMAttestation = in_toto.Statement{ + StatementHeader: in_toto.StatementHeader{ + Type: "https://in-toto.io/Statement/v0.1", + PredicateType: "https://cyclonedx.org/schema", + Subject: []in_toto.Subject{ + { + Name: "go.mod", + Digest: slsa.DigestSet{ + "sha256": "23f4e10c43c7654e33a3c9570913c8c9c528292762f1a5c4a97253e9e4e4b238", + }, + }, + }, + }, + Predicate: &attestation.CosignPredicate{ + Data: &cyclonedx.BOM{ + BOMFormat: cyclonedx.BOMFormat, + SerialNumber: "urn:uuid:8b16c9a3-e957-4c85-b43d-7dd05ea0421c", + SpecVersion: "1.4", + Version: 1, + Metadata: &cyclonedx.Metadata{ + Timestamp: "2022-10-21T09:50:08+00:00", + Tools: &[]cyclonedx.Tool{ + { + Vendor: "aquasecurity", + Name: "trivy", + Version: "dev", + }, + }, + Component: &cyclonedx.Component{ + BOMRef: "ef8385d7-a56f-495a-a220-7b0a2e940d39", + Type: cyclonedx.ComponentTypeApplication, + Name: "go.mod", + Properties: &[]cyclonedx.Property{ + {Name: "aquasecurity:trivy:SchemaVersion", Value: "2"}, + }, + }, + }, + Components: &[]cyclonedx.Component{ + { + BOMRef: "bb8b7541-2b08-4692-9363-8f79da5c1a31", + Type: cyclonedx.ComponentTypeApplication, + Name: "go.mod", + Properties: &[]cyclonedx.Property{ + {Name: "aquasecurity:trivy:Type", Value: "gomod"}, + {Name: "aquasecurity:trivy:Class", Value: "lang-pkgs"}, + }, + }, + { + BOMRef: "pkg:golang/github.com/spf13/cobra@1.5.0", + Type: cyclonedx.ComponentTypeLibrary, + Name: "github.com/spf13/cobra", + Version: "1.5.0", + PackageURL: "pkg:golang/github.com/spf13/cobra@1.5.0", + Properties: &[]cyclonedx.Property{ + {Name: "aquasecurity:trivy:PkgType", Value: "gomod"}, + }, + }, + }, + Dependencies: &[]cyclonedx.Dependency{ + { + Ref: "ef8385d7-a56f-495a-a220-7b0a2e940d39", + Dependencies: &[]cyclonedx.Dependency{ + {Ref: "bb8b7541-2b08-4692-9363-8f79da5c1a31"}, + }, + }, + { + Ref: "bb8b7541-2b08-4692-9363-8f79da5c1a31", + Dependencies: &[]cyclonedx.Dependency{ + {Ref: "pkg:golang/github.com/spf13/cobra@1.5.0"}, + }, + }, + }, + }, + }, + } + + emptySBOMAttestation = in_toto.Statement{ + StatementHeader: in_toto.StatementHeader{ + Type: "https://in-toto.io/Statement/v0.1", + PredicateType: "https://cyclonedx.org/schema", + }, + Predicate: &attestation.CosignPredicate{ + Data: &cyclonedx.BOM{ + BOMFormat: cyclonedx.BOMFormat, + SpecVersion: "1.4", + Version: 2, + }, + }, + } + + entries = map[string]models.LogEntryAnon{ + "392f8ecba72f4326414eaca77bd19bf5f378725d7fd79309605a81b69cc0101f5cd3119d0a216523": { + Attestation: &models.LogEntryAnonAttestation{ + Data: mustMarshal(imageSBOMAttestation), + }, + Body: "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI3ODIxNDNlMzlmMWU3YTA0ZTNmNmRhMmQ4OGIxYzA1N2U1NjU3MzYzYzRmOTA2NzlmM2U4YTA3MWI3NjE5ZTAyIn0sInBheWxvYWRIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZWJiZmRkZGE2Mjc3YWYxOTllOTNjNWJiNWNmNTk5OGE3OTMxMWRlMjM4ZTQ5YmNjOGFjMjQxMDI2OTg3NjFiYiJ9fSwicHVibGljS2V5IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTndSRU5EUVdseFowRjNTVUpCWjBsVllXaHNPRUZSZDFsWlYwNVpiblY2ZGxGdk9FVnJOMWRNVFVSdmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEpkMDlFU1RKTlJFVjRUbnBGTTFkb1kwNU5ha2wzVDBSSk1rMUVSWGxPZWtVelYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZMV21aRVF6bHBhbFZ5Y2xwQldFOWpXRllyUVhGSFJVbFRTbEV6VkhScVNuZEpkRUVLZFRFM1JtbDJhV3BuU2sxaFlVaEdORGNyVDNaMk9WUjFla0ZEUTNscFNVVjVVRFV5WlhJMlptRjVibVpLWVZWcU9FdFBRMEZWYTNkblowWkdUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZIUWxkVUNrTXdkVVUzZFRSUWNVUlZSakZZVjBjMFFsVldWVXBCZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBwUldVUldVakJTUVZGSUwwSkNjM2RIV1VWWVl6SkdlbUl5Um5KaFdFcG9UbXBGZUU1RlFtNWlWMFp3WWtNMWFtSXlNSGRMVVZsTFMzZFpRZ3BDUVVkRWRucEJRa0ZSVVdKaFNGSXdZMGhOTmt4NU9XaFpNazUyWkZjMU1HTjVOVzVpTWpsdVlrZFZkVmt5T1hSTlNVZE1RbWR2Y2tKblJVVkJaRm8xQ2tGblVVTkNTREJGWlhkQ05VRklZMEZEUjBOVE9FTm9VeTh5YUVZd1pFWnlTalJUWTFKWFkxbHlRbGs1ZDNwcVUySmxZVGhKWjFreVlqTkpRVUZCUjBNS01UZHRTbWhuUVVGQ1FVMUJVMFJDUjBGcFJVRm9TMDlCU2tkV1ZsaENiMWN4VERSNGFsazVlV0pXT0daVVVYTjVUU3R2VUVwSWVEazVTMjlMWVVwVlF3cEpVVVJDWkRsbGMxUTBNazFTVG5nM1ZtOUJNMXBhS3pWNGFraE5aV1I2YW1WeFEyWm9aVGN2ZDFweFlUbFVRVXRDWjJkeGFHdHFUMUJSVVVSQmQwNXZDa0ZFUW14QmFrVkJjbkJrZVhsRlJqYzNiMkp5VEVOTVVYcHpZbUl4TTJsc05qZDNkek00WTA1MGFtZE5RbWw2WTJWVWFrUmlZMlZMZVZGU04xUktOSE1LWkVOc2Nsa3hZMUJCYWtFNGFYQjZTVVE0VlUxQ2FHeGtTbVV2WlhKR2NHZHROMnN3TldGaWMybFBOM1Y1ZFZadVMyOVZOazByVFhKNlZWVXJaVGxHZHdwSlJHaENhblZSYTFkUll6MEtMUzB0TFMxRlRrUWdRMFZTVkVsR1NVTkJWRVV0TFMwdExRbz0ifX0=", + IntegratedTime: lo.ToPtr(int64(1661476639)), + LogID: lo.ToPtr("c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"), + LogIndex: lo.ToPtr(int64(3280165)), + Verification: nil, // TODO + }, + "392f8ecba72f4326eb624a7403756250b5f2ad58842a99d1653cd6f147f4ce9eda2da350bd908a55": { + Attestation: &models.LogEntryAnonAttestation{ + Data: []byte(`{"apiVersion":"0.0.1","kind":"intoto","spec":{"content":{"hash":{"algorithm":"sha256","value":"782143e39f1e7a04e3f6da2d88b1c057e5657363c4f90679f3e8a071b7619e02"},"payloadHash":{"algorithm":"sha256","value":"ebbfddda6277af199e93c5bb5cf5998a79311de238e49bcc8ac24102698761bb"}},"publicKey":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNwRENDQWlxZ0F3SUJBZ0lVYWhsOEFRd1lZV05ZbnV6dlFvOEVrN1dMTURvd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpJd09ESTJNREV4TnpFM1doY05Nakl3T0RJMk1ERXlOekUzV2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVLWmZEQzlpalVyclpBWE9jWFYrQXFHRUlTSlEzVHRqSndJdEEKdTE3Rml2aWpnSk1hYUhGNDcrT3Z2OVR1ekFDQ3lpSUV5UDUyZXI2ZmF5bmZKYVVqOEtPQ0FVa3dnZ0ZGTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVHQldUCkMwdUU3dTRQcURVRjFYV0c0QlVWVUpBd0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0pRWURWUjBSQVFIL0JCc3dHWUVYYzJGemIyRnJhWEpoTmpFeE5FQm5iV0ZwYkM1amIyMHdLUVlLS3dZQgpCQUdEdnpBQkFRUWJhSFIwY0hNNkx5OWhZMk52ZFc1MGN5NW5iMjluYkdVdVkyOXRNSUdMQmdvckJnRUVBZFo1CkFnUUNCSDBFZXdCNUFIY0FDR0NTOENoUy8yaEYwZEZySjRTY1JXY1lyQlk5d3pqU2JlYThJZ1kyYjNJQUFBR0MKMTdtSmhnQUFCQU1BU0RCR0FpRUFoS09BSkdWVlhCb1cxTDR4alk5eWJWOGZUUXN5TStvUEpIeDk5S29LYUpVQwpJUURCZDllc1Q0Mk1STng3Vm9BM1paKzV4akhNZWR6amVxQ2ZoZTcvd1pxYTlUQUtCZ2dxaGtqT1BRUURBd05vCkFEQmxBakVBcnBkeXlFRjc3b2JyTENMUXpzYmIxM2lsNjd3dzM4Y050amdNQml6Y2VUakRiY2VLeVFSN1RKNHMKZENsclkxY1BBakE4aXB6SUQ4VU1CaGxkSmUvZXJGcGdtN2swNWFic2lPN3V5dVZuS29VNk0rTXJ6VVUrZTlGdwpJRGhCanVRa1dRYz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="}}`), + }, + Body: "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI3ODIxNDNlMzlmMWU3YTA0ZTNmNmRhMmQ4OGIxYzA1N2U1NjU3MzYzYzRmOTA2NzlmM2U4YTA3MWI3NjE5ZTAyIn0sInBheWxvYWRIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZWJiZmRkZGE2Mjc3YWYxOTllOTNjNWJiNWNmNTk5OGE3OTMxMWRlMjM4ZTQ5YmNjOGFjMjQxMDI2OTg3NjFiYiJ9fSwicHVibGljS2V5IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTndSRU5EUVdseFowRjNTVUpCWjBsVllXaHNPRUZSZDFsWlYwNVpiblY2ZGxGdk9FVnJOMWRNVFVSdmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEpkMDlFU1RKTlJFVjRUbnBGTTFkb1kwNU5ha2wzVDBSSk1rMUVSWGxPZWtVelYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZMV21aRVF6bHBhbFZ5Y2xwQldFOWpXRllyUVhGSFJVbFRTbEV6VkhScVNuZEpkRUVLZFRFM1JtbDJhV3BuU2sxaFlVaEdORGNyVDNaMk9WUjFla0ZEUTNscFNVVjVVRFV5WlhJMlptRjVibVpLWVZWcU9FdFBRMEZWYTNkblowWkdUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZIUWxkVUNrTXdkVVUzZFRSUWNVUlZSakZZVjBjMFFsVldWVXBCZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBwUldVUldVakJTUVZGSUwwSkNjM2RIV1VWWVl6SkdlbUl5Um5KaFdFcG9UbXBGZUU1RlFtNWlWMFp3WWtNMWFtSXlNSGRMVVZsTFMzZFpRZ3BDUVVkRWRucEJRa0ZSVVdKaFNGSXdZMGhOTmt4NU9XaFpNazUyWkZjMU1HTjVOVzVpTWpsdVlrZFZkVmt5T1hSTlNVZE1RbWR2Y2tKblJVVkJaRm8xQ2tGblVVTkNTREJGWlhkQ05VRklZMEZEUjBOVE9FTm9VeTh5YUVZd1pFWnlTalJUWTFKWFkxbHlRbGs1ZDNwcVUySmxZVGhKWjFreVlqTkpRVUZCUjBNS01UZHRTbWhuUVVGQ1FVMUJVMFJDUjBGcFJVRm9TMDlCU2tkV1ZsaENiMWN4VERSNGFsazVlV0pXT0daVVVYTjVUU3R2VUVwSWVEazVTMjlMWVVwVlF3cEpVVVJDWkRsbGMxUTBNazFTVG5nM1ZtOUJNMXBhS3pWNGFraE5aV1I2YW1WeFEyWm9aVGN2ZDFweFlUbFVRVXRDWjJkeGFHdHFUMUJSVVVSQmQwNXZDa0ZFUW14QmFrVkJjbkJrZVhsRlJqYzNiMkp5VEVOTVVYcHpZbUl4TTJsc05qZDNkek00WTA1MGFtZE5RbWw2WTJWVWFrUmlZMlZMZVZGU04xUktOSE1LWkVOc2Nsa3hZMUJCYWtFNGFYQjZTVVE0VlUxQ2FHeGtTbVV2WlhKR2NHZHROMnN3TldGaWMybFBOM1Y1ZFZadVMyOVZOazByVFhKNlZWVXJaVGxHZHdwSlJHaENhblZSYTFkUll6MEtMUzB0TFMxRlRrUWdRMFZTVkVsR1NVTkJWRVV0TFMwdExRbz0ifX0=", + IntegratedTime: lo.ToPtr(int64(1661476639)), + LogID: lo.ToPtr("c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"), + LogIndex: lo.ToPtr(int64(3280165)), + Verification: nil, // TODO + }, + "24296fb24b8ad77aa715cdfd264ce34c4d544375d7bd7cd029bf5a48ef25217a13fdba562e0889ca": { + Attestation: &models.LogEntryAnonAttestation{ + Data: mustMarshal(gomodSBOMAttestation), + }, + Body: nil, // not used at the moment + IntegratedTime: lo.ToPtr(int64(1664451604)), + LogID: lo.ToPtr("c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"), + LogIndex: lo.ToPtr(int64(4215471)), + Verification: nil, // TODO + }, + "24296fb24b8ad77a8d47be2e40bfe910f0ffc842e86b5685dd85d1c903ef78bb6362125816426fe9": { + Attestation: &models.LogEntryAnonAttestation{ + Data: mustMarshal(emptySBOMAttestation), + }, + Body: "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI3ODIxNDNlMzlmMWU3YTA0ZTNmNmRhMmQ4OGIxYzA1N2U1NjU3MzYzYzRmOTA2NzlmM2U4YTA3MWI3NjE5ZTAyIn0sInBheWxvYWRIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZWJiZmRkZGE2Mjc3YWYxOTllOTNjNWJiNWNmNTk5OGE3OTMxMWRlMjM4ZTQ5YmNjOGFjMjQxMDI2OTg3NjFiYiJ9fSwicHVibGljS2V5IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTndSRU5EUVdseFowRjNTVUpCWjBsVllXaHNPRUZSZDFsWlYwNVpiblY2ZGxGdk9FVnJOMWRNVFVSdmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEpkMDlFU1RKTlJFVjRUbnBGTTFkb1kwNU5ha2wzVDBSSk1rMUVSWGxPZWtVelYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZMV21aRVF6bHBhbFZ5Y2xwQldFOWpXRllyUVhGSFJVbFRTbEV6VkhScVNuZEpkRUVLZFRFM1JtbDJhV3BuU2sxaFlVaEdORGNyVDNaMk9WUjFla0ZEUTNscFNVVjVVRFV5WlhJMlptRjVibVpLWVZWcU9FdFBRMEZWYTNkblowWkdUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZIUWxkVUNrTXdkVVUzZFRSUWNVUlZSakZZVjBjMFFsVldWVXBCZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBwUldVUldVakJTUVZGSUwwSkNjM2RIV1VWWVl6SkdlbUl5Um5KaFdFcG9UbXBGZUU1RlFtNWlWMFp3WWtNMWFtSXlNSGRMVVZsTFMzZFpRZ3BDUVVkRWRucEJRa0ZSVVdKaFNGSXdZMGhOTmt4NU9XaFpNazUyWkZjMU1HTjVOVzVpTWpsdVlrZFZkVmt5T1hSTlNVZE1RbWR2Y2tKblJVVkJaRm8xQ2tGblVVTkNTREJGWlhkQ05VRklZMEZEUjBOVE9FTm9VeTh5YUVZd1pFWnlTalJUWTFKWFkxbHlRbGs1ZDNwcVUySmxZVGhKWjFreVlqTkpRVUZCUjBNS01UZHRTbWhuUVVGQ1FVMUJVMFJDUjBGcFJVRm9TMDlCU2tkV1ZsaENiMWN4VERSNGFsazVlV0pXT0daVVVYTjVUU3R2VUVwSWVEazVTMjlMWVVwVlF3cEpVVVJDWkRsbGMxUTBNazFTVG5nM1ZtOUJNMXBhS3pWNGFraE5aV1I2YW1WeFEyWm9aVGN2ZDFweFlUbFVRVXRDWjJkeGFHdHFUMUJSVVVSQmQwNXZDa0ZFUW14QmFrVkJjbkJrZVhsRlJqYzNiMkp5VEVOTVVYcHpZbUl4TTJsc05qZDNkek00WTA1MGFtZE5RbWw2WTJWVWFrUmlZMlZMZVZGU04xUktOSE1LWkVOc2Nsa3hZMUJCYWtFNGFYQjZTVVE0VlUxQ2FHeGtTbVV2WlhKR2NHZHROMnN3TldGaWMybFBOM1Y1ZFZadVMyOVZOazByVFhKNlZWVXJaVGxHZHdwSlJHaENhblZSYTFkUll6MEtMUzB0TFMxRlRrUWdRMFZTVkVsR1NVTkJWRVV0TFMwdExRbz0ifX0=", + IntegratedTime: lo.ToPtr(int64(1661476639)), + LogID: lo.ToPtr("c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"), + LogIndex: lo.ToPtr(int64(3280165)), + Verification: nil, // TODO + }, + } +) + +type Server struct { + ts *httptest.Server +} + +func NewServer(t *testing.T) *Server { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/v1/index/retrieve": + var params models.SearchIndex + err := json.NewDecoder(r.Body).Decode(¶ms) + require.NoError(t, err) + + if res, ok := indexRes[params.Hash]; ok { + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(res) + require.NoError(t, err) + } else { + http.Error(w, "something wrong", http.StatusNotFound) + } + case "/api/v1/log/entries/retrieve": + var params models.SearchLogQuery + err := json.NewDecoder(r.Body).Decode(¶ms) + require.NoError(t, err) + + resEntries := models.LogEntry{} + for _, uuid := range params.EntryUUIDs { + if e, ok := entries[uuid]; !ok { + http.Error(w, "no such uuid", http.StatusNotFound) + return + } else { + resEntries[uuid] = e + } + } + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode([]models.LogEntry{resEntries}) + require.NoError(t, err) + } + return + })) + + return &Server{ts: ts} +} + +func (s *Server) URL() string { + return s.ts.URL +} + +func (s *Server) Close() { + s.ts.Close() +} + +func mustMarshal(v any) []byte { + b, err := json.Marshal(v) + if err != nil { + panic(err) + } + return b +} diff --git a/pkg/sbom/cyclonedx/unmarshal.go b/pkg/sbom/cyclonedx/unmarshal.go index 1744fb1b55..d0f2b4f16f 100644 --- a/pkg/sbom/cyclonedx/unmarshal.go +++ b/pkg/sbom/cyclonedx/unmarshal.go @@ -6,19 +6,18 @@ import ( "strconv" "strings" - "github.com/aquasecurity/trivy/pkg/log" - cdx "github.com/CycloneDX/cyclonedx-go" "github.com/samber/lo" "golang.org/x/xerrors" ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" "github.com/aquasecurity/trivy/pkg/purl" - "github.com/aquasecurity/trivy/pkg/sbom" + "github.com/aquasecurity/trivy/pkg/types" ) type CycloneDX struct { - *sbom.SBOM + *types.SBOM dependencies map[string][]string components map[string]cdx.Component @@ -27,7 +26,7 @@ type CycloneDX struct { func (c *CycloneDX) UnmarshalJSON(b []byte) error { log.Logger.Debug("Unmarshaling CycloneDX JSON...") if c.SBOM == nil { - c.SBOM = &sbom.SBOM{} + c.SBOM = &types.SBOM{} } bom := cdx.NewBOM() decoder := cdx.NewBOMDecoder(bytes.NewReader(b), cdx.BOMFileFormatJSON) diff --git a/pkg/sbom/cyclonedx/unmarshal_test.go b/pkg/sbom/cyclonedx/unmarshal_test.go index 76dc5fa4dc..4c209e0bab 100644 --- a/pkg/sbom/cyclonedx/unmarshal_test.go +++ b/pkg/sbom/cyclonedx/unmarshal_test.go @@ -5,26 +5,25 @@ import ( "os" "testing" - "github.com/aquasecurity/trivy/pkg/sbom" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" "github.com/aquasecurity/trivy/pkg/sbom/cyclonedx" + "github.com/aquasecurity/trivy/pkg/types" ) func TestUnmarshaler_Unmarshal(t *testing.T) { tests := []struct { name string inputFile string - want sbom.SBOM + want types.SBOM wantErr string }{ { name: "happy path", inputFile: "testdata/happy/bom.json", - want: sbom.SBOM{ + want: types.SBOM{ OS: &ftypes.OS{ Family: "alpine", Name: "3.16.0", @@ -127,7 +126,7 @@ func TestUnmarshaler_Unmarshal(t *testing.T) { { name: "happy path for unrelated bom", inputFile: "testdata/happy/unrelated-bom.json", - want: sbom.SBOM{ + want: types.SBOM{ Applications: []ftypes.Application{ { Type: "composer", @@ -152,7 +151,7 @@ func TestUnmarshaler_Unmarshal(t *testing.T) { { name: "happy path for independent library bom", inputFile: "testdata/happy/independent-library-bom.json", - want: sbom.SBOM{ + want: types.SBOM{ Applications: []ftypes.Application{ { Type: "composer", @@ -182,7 +181,7 @@ func TestUnmarshaler_Unmarshal(t *testing.T) { { name: "happy path only os component", inputFile: "testdata/happy/os-only-bom.json", - want: sbom.SBOM{ + want: types.SBOM{ OS: &ftypes.OS{ Family: "alpine", Name: "3.16.0", @@ -195,12 +194,12 @@ func TestUnmarshaler_Unmarshal(t *testing.T) { { name: "happy path empty component", inputFile: "testdata/happy/empty-bom.json", - want: sbom.SBOM{}, + want: types.SBOM{}, }, { name: "happy path empty metadata component", inputFile: "testdata/happy/empty-metadata-component-bom.json", - want: sbom.SBOM{}, + want: types.SBOM{}, }, { name: "sad path invalid purl", diff --git a/pkg/sbom/sbom.go b/pkg/sbom/sbom.go index 0a2536988f..78f9b1cd11 100644 --- a/pkg/sbom/sbom.go +++ b/pkg/sbom/sbom.go @@ -8,22 +8,14 @@ import ( "strings" "github.com/in-toto/in-toto-golang/in_toto" - stypes "github.com/spdx/tools-golang/spdx" "golang.org/x/xerrors" "github.com/aquasecurity/trivy/pkg/attestation" - "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/sbom/cyclonedx" + "github.com/aquasecurity/trivy/pkg/sbom/spdx" + "github.com/aquasecurity/trivy/pkg/types" ) -type SBOM struct { - OS *types.OS - Packages []types.PackageInfo - Applications []types.Application - - CycloneDX *types.CycloneDX - SPDX *stypes.Document2_2 -} - type Format string const ( @@ -39,6 +31,9 @@ const ( var ErrUnknownFormat = xerrors.New("Unknown SBOM format") func DetectFormat(r io.ReadSeeker) (Format, error) { + // Rewind the SBOM file at the end + defer r.Seek(0, io.SeekStart) + type ( cyclonedx struct { // XML specific field @@ -109,3 +104,45 @@ func DetectFormat(r io.ReadSeeker) (Format, error) { return FormatUnknown, nil } + +func Decode(f io.Reader, format Format) (types.SBOM, error) { + var ( + v interface{} + bom types.SBOM + decoder interface{ Decode(any) error } + ) + + switch format { + case FormatCycloneDXJSON: + v = &cyclonedx.CycloneDX{SBOM: &bom} + decoder = json.NewDecoder(f) + case FormatAttestCycloneDXJSON: + // dsse envelope + // => in-toto attestation + // => cosign predicate + // => CycloneDX JSON + v = &attestation.Statement{ + Predicate: &attestation.CosignPredicate{ + Data: &cyclonedx.CycloneDX{SBOM: &bom}, + }, + } + decoder = json.NewDecoder(f) + case FormatSPDXJSON: + v = &spdx.SPDX{SBOM: &bom} + decoder = json.NewDecoder(f) + case FormatSPDXTV: + v = &spdx.SPDX{SBOM: &bom} + decoder = spdx.NewTVDecoder(f) + + default: + return types.SBOM{}, xerrors.Errorf("%s scanning is not yet supported", format) + + } + + // Decode a file content into sbom.SBOM + if err := decoder.Decode(v); err != nil { + return types.SBOM{}, xerrors.Errorf("failed to decode: %w", err) + } + + return bom, nil +} diff --git a/pkg/sbom/spdx/unmarshal.go b/pkg/sbom/spdx/unmarshal.go index e610cf1cac..2cd1b78fdf 100644 --- a/pkg/sbom/spdx/unmarshal.go +++ b/pkg/sbom/spdx/unmarshal.go @@ -16,7 +16,7 @@ import ( ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" "github.com/aquasecurity/trivy/pkg/purl" - "github.com/aquasecurity/trivy/pkg/sbom" + "github.com/aquasecurity/trivy/pkg/types" ) var ( @@ -24,7 +24,7 @@ var ( ) type SPDX struct { - *sbom.SBOM + *types.SBOM } func NewTVDecoder(r io.Reader) *TVDecoder { diff --git a/pkg/sbom/spdx/unmarshal_test.go b/pkg/sbom/spdx/unmarshal_test.go index c5089ac802..461cb72633 100644 --- a/pkg/sbom/spdx/unmarshal_test.go +++ b/pkg/sbom/spdx/unmarshal_test.go @@ -10,21 +10,21 @@ import ( "github.com/stretchr/testify/require" ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" - "github.com/aquasecurity/trivy/pkg/sbom" "github.com/aquasecurity/trivy/pkg/sbom/spdx" + "github.com/aquasecurity/trivy/pkg/types" ) func TestUnmarshaler_Unmarshal(t *testing.T) { tests := []struct { name string inputFile string - want sbom.SBOM + want types.SBOM wantErr string }{ { name: "happy path", inputFile: "testdata/happy/bom.json", - want: sbom.SBOM{ + want: types.SBOM{ OS: &ftypes.OS{ Family: "alpine", Name: "3.16.0", @@ -113,7 +113,7 @@ func TestUnmarshaler_Unmarshal(t *testing.T) { { name: "happy path for unrelated bom", inputFile: "testdata/happy/unrelated-bom.json", - want: sbom.SBOM{ + want: types.SBOM{ Applications: []ftypes.Application{ { Type: "composer", @@ -138,7 +138,7 @@ func TestUnmarshaler_Unmarshal(t *testing.T) { { name: "happy path only os component", inputFile: "testdata/happy/os-only-bom.json", - want: sbom.SBOM{ + want: types.SBOM{ OS: &ftypes.OS{ Family: "alpine", Name: "3.16.0", @@ -148,7 +148,7 @@ func TestUnmarshaler_Unmarshal(t *testing.T) { { name: "happy path empty component", inputFile: "testdata/happy/empty-bom.json", - want: sbom.SBOM{}, + want: types.SBOM{}, }, { name: "sad path invalid purl", @@ -163,7 +163,7 @@ func TestUnmarshaler_Unmarshal(t *testing.T) { require.NoError(t, err) defer f.Close() - v := &spdx.SPDX{SBOM: &sbom.SBOM{}} + v := &spdx.SPDX{SBOM: &types.SBOM{}} err = json.NewDecoder(f).Decode(v) if tt.wantErr != "" { require.Error(t, err) diff --git a/pkg/types/sbom.go b/pkg/types/sbom.go index bc3e093734..f22cdab578 100644 --- a/pkg/types/sbom.go +++ b/pkg/types/sbom.go @@ -1,5 +1,20 @@ package types +import ( + stypes "github.com/spdx/tools-golang/spdx" + + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +type SBOM struct { + OS *types.OS + Packages []types.PackageInfo + Applications []types.Application + + CycloneDX *types.CycloneDX + SPDX *stypes.Document2_2 +} + type SBOMSource = string const (