mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 07:40:48 -08:00
feat(vuln): support non-packaged binaries (#3019)
This commit is contained in:
@@ -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
|
||||
[sbom-attest]: sbom.md#keyless-signing
|
||||
|
||||
[plugin-attest]: https://github.com/aquasecurity/trivy-plugin-attest
|
||||
|
||||
[bat]: https://github.com/sharkdp/bat
|
||||
@@ -61,7 +61,9 @@ $ COSIGN_EXPERIMENTAL=1 cosign verify-attestation --type cyclonedx <IMAGE>
|
||||
|
||||
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 <IMAGE> > sbom.cdx.intoto.jsonl
|
||||
|
||||
94
pkg/attestation/sbom/rekor.go
Normal file
94
pkg/attestation/sbom/rekor.go
Normal file
@@ -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
|
||||
}
|
||||
53
pkg/attestation/sbom/rekor_test.go
Normal file
53
pkg/attestation/sbom/rekor_test.go
Normal file
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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...)
|
||||
|
||||
@@ -74,6 +74,11 @@ const (
|
||||
// C/C++
|
||||
TypeConanLock Type = "conan-lock"
|
||||
|
||||
// ============
|
||||
// Non-packaged
|
||||
// ============
|
||||
TypeExecutable Type = "executable"
|
||||
|
||||
// ============
|
||||
// Image Config
|
||||
// ============
|
||||
|
||||
56
pkg/fanal/analyzer/executable/executable.go
Normal file
56
pkg/fanal/analyzer/executable/executable.go
Normal file
@@ -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
|
||||
}
|
||||
53
pkg/fanal/analyzer/executable/executable_test.go
Normal file
53
pkg/fanal/analyzer/executable/executable_test.go
Normal file
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
1
pkg/fanal/analyzer/executable/testdata/binary
vendored
Normal file
1
pkg/fanal/analyzer/executable/testdata/binary
vendored
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
pkg/fanal/analyzer/executable/testdata/hello.txt
vendored
Normal file
1
pkg/fanal/analyzer/executable/testdata/hello.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
hello
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, ":")
|
||||
|
||||
155
pkg/fanal/artifact/image/remote_sbom_test.go
Normal file
155
pkg/fanal/artifact/image/remote_sbom_test.go
Normal file
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
[
|
||||
"392f8ecba72f4326eb624a7403756250b5f2ad58842a99d1653cd6f147f4ce9eda2da350bd908a55",
|
||||
"392f8ecba72f4326414eaca77bd19bf5f378725d7fd79309605a81b69cc0101f5cd3119d0a216523"
|
||||
]
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
92
pkg/fanal/handler/unpackaged/unpackaged.go
Normal file
92
pkg/fanal/handler/unpackaged/unpackaged.go
Normal file
@@ -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
|
||||
}
|
||||
91
pkg/fanal/handler/unpackaged/unpackaged_test.go
Normal file
91
pkg/fanal/handler/unpackaged/unpackaged_test.go
Normal file
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
315
pkg/rekortest/server.go
Normal file
315
pkg/rekortest/server.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user