github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/fanal/artifact/image/remote_sbom.go (about) 1 package image 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "os" 8 "path/filepath" 9 10 "github.com/google/go-containerregistry/pkg/name" 11 v1 "github.com/google/go-containerregistry/pkg/v1" 12 "github.com/samber/lo" 13 "golang.org/x/exp/slices" 14 "golang.org/x/xerrors" 15 16 sbomatt "github.com/devseccon/trivy/pkg/attestation/sbom" 17 "github.com/devseccon/trivy/pkg/fanal/artifact/sbom" 18 "github.com/devseccon/trivy/pkg/fanal/log" 19 ftypes "github.com/devseccon/trivy/pkg/fanal/types" 20 "github.com/devseccon/trivy/pkg/oci" 21 "github.com/devseccon/trivy/pkg/remote" 22 "github.com/devseccon/trivy/pkg/types" 23 ) 24 25 var errNoSBOMFound = xerrors.New("remote SBOM not found") 26 27 type inspectRemoteSBOM func(context.Context) (ftypes.ArtifactReference, error) 28 29 func (a Artifact) retrieveRemoteSBOM(ctx context.Context) (ftypes.ArtifactReference, error) { 30 for _, sbomSource := range a.artifactOption.SBOMSources { 31 var inspect inspectRemoteSBOM 32 switch sbomSource { 33 case types.SBOMSourceOCI: 34 inspect = a.inspectOCIReferrerSBOM 35 case types.SBOMSourceRekor: 36 inspect = a.inspectRekorSBOMAttestation 37 default: 38 // Never reach here as the "--sbom-sources" values are validated beforehand 39 continue 40 } 41 42 ref, err := inspect(ctx) 43 if errors.Is(err, errNoSBOMFound) { 44 // Try the next SBOM source 45 log.Logger.Debugf("No SBOM found in the source: %s", sbomSource) 46 continue 47 } else if err != nil { 48 return ftypes.ArtifactReference{}, xerrors.Errorf("SBOM searching error: %w", err) 49 } 50 return ref, nil 51 } 52 return ftypes.ArtifactReference{}, errNoSBOMFound 53 } 54 55 func (a Artifact) inspectOCIReferrerSBOM(ctx context.Context) (ftypes.ArtifactReference, error) { 56 digest, err := repoDigest(a.image, a.artifactOption.Insecure) 57 if err != nil { 58 return ftypes.ArtifactReference{}, xerrors.Errorf("repo digest error: %w", err) 59 } 60 61 // Fetch referrers 62 index, err := remote.Referrers(ctx, digest, a.artifactOption.ImageOption.RegistryOptions) 63 if err != nil { 64 return ftypes.ArtifactReference{}, xerrors.Errorf("unable to fetch referrers: %w", err) 65 } 66 manifest, err := index.IndexManifest() 67 if err != nil { 68 return ftypes.ArtifactReference{}, xerrors.Errorf("unable to get manifest: %w", err) 69 } 70 for _, m := range lo.FromPtr(manifest).Manifests { 71 // Unsupported artifact type 72 if !slices.Contains(oci.SupportedSBOMArtifactTypes, m.ArtifactType) { 73 continue 74 } 75 res, err := a.parseReferrer(ctx, digest.Context().String(), m) 76 if err != nil { 77 log.Logger.Warnf("Error with SBOM via OCI referrers (%s): %s", m.Digest.String(), err) 78 continue 79 } 80 return res, nil 81 } 82 return ftypes.ArtifactReference{}, errNoSBOMFound 83 } 84 85 func (a Artifact) parseReferrer(ctx context.Context, repo string, desc v1.Descriptor) (ftypes.ArtifactReference, error) { 86 const fileName string = "referrer.sbom" 87 repoName := fmt.Sprintf("%s@%s", repo, desc.Digest) 88 referrer, err := oci.NewArtifact(repoName, true, a.artifactOption.ImageOption.RegistryOptions) 89 if err != nil { 90 return ftypes.ArtifactReference{}, xerrors.Errorf("OCI error: %w", err) 91 } 92 93 tmpDir, err := os.MkdirTemp("", "trivy-sbom-*") 94 if err != nil { 95 return ftypes.ArtifactReference{}, xerrors.Errorf("mkdir temp error: %w", err) 96 } 97 defer os.RemoveAll(tmpDir) 98 99 // Download SBOM to local filesystem 100 if err = referrer.Download(ctx, tmpDir, oci.DownloadOption{ 101 MediaType: desc.ArtifactType, 102 Filename: fileName, 103 }); err != nil { 104 return ftypes.ArtifactReference{}, xerrors.Errorf("SBOM download error: %w", err) 105 } 106 107 res, err := a.inspectSBOMFile(ctx, filepath.Join(tmpDir, fileName)) 108 if err != nil { 109 return res, xerrors.Errorf("SBOM error: %w", err) 110 } 111 112 // Found SBOM 113 log.Logger.Infof("Found SBOM (%s) in the OCI referrers", res.Type) 114 115 return res, nil 116 } 117 118 func (a Artifact) inspectRekorSBOMAttestation(ctx context.Context) (ftypes.ArtifactReference, error) { 119 digest, err := repoDigest(a.image, a.artifactOption.Insecure) 120 if err != nil { 121 return ftypes.ArtifactReference{}, xerrors.Errorf("repo digest error: %w", err) 122 } 123 124 client, err := sbomatt.NewRekor(a.artifactOption.RekorURL) 125 if err != nil { 126 return ftypes.ArtifactReference{}, xerrors.Errorf("failed to create rekor client: %w", err) 127 } 128 129 raw, err := client.RetrieveSBOM(ctx, digest.DigestStr()) 130 if errors.Is(err, sbomatt.ErrNoSBOMAttestation) { 131 return ftypes.ArtifactReference{}, errNoSBOMFound 132 } else if err != nil { 133 return ftypes.ArtifactReference{}, xerrors.Errorf("failed to retrieve SBOM attestation: %w", err) 134 } 135 136 f, err := os.CreateTemp("", "sbom-*") 137 if err != nil { 138 return ftypes.ArtifactReference{}, xerrors.Errorf("failed to create a temporary file: %w", err) 139 } 140 defer os.Remove(f.Name()) 141 142 if _, err = f.Write(raw); err != nil { 143 return ftypes.ArtifactReference{}, xerrors.Errorf("copy error: %w", err) 144 } 145 if err = f.Close(); err != nil { 146 return ftypes.ArtifactReference{}, xerrors.Errorf("failed to close %s: %w", f.Name(), err) 147 } 148 res, err := a.inspectSBOMFile(ctx, f.Name()) 149 if err != nil { 150 return res, xerrors.Errorf("SBOM error: %w", err) 151 } 152 153 // Found SBOM 154 log.Logger.Infof("Found SBOM (%s) in Rekor (%s)", res.Type, a.artifactOption.RekorURL) 155 156 return res, nil 157 } 158 159 func (a Artifact) inspectSBOMFile(ctx context.Context, filePath string) (ftypes.ArtifactReference, error) { 160 ar, err := sbom.NewArtifact(filePath, a.cache, a.artifactOption) 161 if err != nil { 162 return ftypes.ArtifactReference{}, xerrors.Errorf("failed to new artifact: %w", err) 163 } 164 165 results, err := ar.Inspect(ctx) 166 if err != nil { 167 return ftypes.ArtifactReference{}, xerrors.Errorf("failed to inspect: %w", err) 168 } 169 results.Name = a.image.Name() 170 171 return results, nil 172 } 173 174 func repoDigest(img ftypes.Image, insecure bool) (name.Digest, error) { 175 repoNameFull := img.Name() 176 ref, err := name.ParseReference(repoNameFull) 177 if err != nil { 178 return name.Digest{}, xerrors.Errorf("image name parse error: %w", err) 179 } 180 181 for _, rd := range img.RepoDigests() { 182 opts := lo.Ternary(insecure, []name.Option{name.Insecure}, nil) 183 digest, err := name.NewDigest(rd, opts...) 184 if err != nil { 185 continue 186 } 187 if ref.Context().String() == digest.Context().String() { 188 return digest, nil 189 } 190 } 191 return name.Digest{}, xerrors.Errorf("no repo digest found: %w", errNoSBOMFound) 192 }