forked from aquasecurity/trivy
/
remote_sbom.go
149 lines (127 loc) · 4.54 KB
/
remote_sbom.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
package image
import (
"bytes"
"context"
"encoding/json"
"errors"
"os"
"strings"
"github.com/in-toto/in-toto-golang/in_toto"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/attestation"
"github.com/aquasecurity/trivy/pkg/fanal/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"
)
var errNoSBOMFound = xerrors.New("remote SBOM not found")
func (a Artifact) retrieveRemoteSBOM(ctx context.Context) (ftypes.ArtifactReference, error) {
for _, sbomFrom := range a.artifactOption.SBOMSources {
switch sbomFrom {
case types.SBOMSourceRekor:
ref, err := a.inspectSBOMAttestation(ctx)
if errors.Is(err, errNoSBOMFound) {
// Try the next SBOM source
continue
} else if err != nil {
return ftypes.ArtifactReference{}, xerrors.Errorf("Rekor attestation searching error: %w", err)
}
// Found SBOM
log.Logger.Infof("Found SBOM (%s) attestation in Rekor", ref.Type)
return ref, nil
}
}
return ftypes.ArtifactReference{}, errNoSBOMFound
}
func (a Artifact) inspectSBOMAttestation(ctx context.Context) (ftypes.ArtifactReference, error) {
digest, err := repoDigest(a.image)
if err != nil {
return ftypes.ArtifactReference{}, xerrors.Errorf("repo digest error: %w", err)
}
client, err := rekor.NewClient(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 {
return ftypes.ArtifactReference{}, errNoSBOMFound
}
log.Logger.Debugf("Found matching Rekor entries: %s", entryIDs)
for _, id := range entryIDs {
log.Logger.Debugf("Inspecting Rekor entry: %s", id)
ref, err := a.inspectRekorRecord(ctx, client, id)
if errors.Is(err, rekor.ErrNoAttestation) || 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, client *rekor.Client, entryID rekor.EntryID) (ftypes.ArtifactReference, error) {
entry, err := client.GetEntry(ctx, entryID)
if err != nil {
return ftypes.ArtifactReference{}, xerrors.Errorf("failed to get rekor entry: %w", err)
}
// TODO: Trivy SBOM should take precedence
raw, err := a.parseStatement(entry)
if err != nil {
return ftypes.ArtifactReference{}, err
}
f, err := os.CreateTemp("", "sbom-*")
if err != nil {
return ftypes.ArtifactReference{}, xerrors.Errorf("failed to create a temporary file: %w", err)
}
defer os.Remove(f.Name())
if _, err = f.Write(raw); err != nil {
return ftypes.ArtifactReference{}, xerrors.Errorf("failed to write statement: %w", err)
}
if err = f.Close(); err != nil {
return ftypes.ArtifactReference{}, xerrors.Errorf("failed to close %s: %w", f.Name(), err)
}
ar, err := sbom.NewArtifact(f.Name(), a.cache, a.artifactOption)
if err != nil {
return ftypes.ArtifactReference{}, xerrors.Errorf("failed to new artifact: %w", err)
}
results, err := ar.Inspect(ctx)
if err != nil {
return ftypes.ArtifactReference{}, xerrors.Errorf("failed to inspect: %w", err)
}
results.Name = a.image.Name()
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, ":")
for _, rd := range img.RepoDigests() {
if name, d, found := strings.Cut(rd, "@"); found && name == repoName {
return d, nil
}
}
return "", xerrors.Errorf("no repo digest found: %w", errNoSBOMFound)
}