mirror of
https://github.com/SAP/jenkins-library.git
synced 2024-12-14 11:03:09 +02:00
185 lines
4.2 KiB
Go
185 lines
4.2 KiB
Go
|
package cnbutils
|
||
|
|
||
|
import (
|
||
|
"archive/tar"
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"regexp"
|
||
|
|
||
|
"github.com/pkg/errors"
|
||
|
|
||
|
"github.com/SAP/jenkins-library/pkg/log"
|
||
|
"github.com/anchore/syft/syft"
|
||
|
"github.com/anchore/syft/syft/sbom"
|
||
|
"github.com/anchore/syft/syft/source"
|
||
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||
|
)
|
||
|
|
||
|
var cycloneDxXML = syft.FormatByID(syft.CycloneDxXMLFormatID)
|
||
|
|
||
|
func MergeSBOMFiles(pattern, output, img, dockerConfigFile string, utils BuildUtils) error {
|
||
|
if dockerConfigFile != "" {
|
||
|
os.Setenv("DOCKER_CONFIG", filepath.Dir(dockerConfigFile))
|
||
|
defer os.Unsetenv("DOCKER_CONFIG")
|
||
|
}
|
||
|
|
||
|
log.Entry().Debugf("reading remote image %s", img)
|
||
|
remoteImage, err := utils.GetRemoteImageInfo(img)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
imgConfig, err := remoteImage.ConfigFile()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
layerSHA, exists := imgConfig.Config.Labels["io.buildpacks.base.sbom"]
|
||
|
if !exists {
|
||
|
return fmt.Errorf("image %q does not contain label \"io.buildpacks.base.sbom\"", img)
|
||
|
}
|
||
|
|
||
|
var bom *sbom.SBOM
|
||
|
log.Entry().Debug("found SBOM layer")
|
||
|
bom, err = readBOMFromLayer(remoteImage, layerSHA)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
log.Entry().Debugf("initial source.ImageMetadata: %#v", bom.Source.ImageMetadata)
|
||
|
|
||
|
imageMetaData, err := extractImageMetaData(remoteImage)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
bom.Source.ImageMetadata = *imageMetaData
|
||
|
bom.Source.ImageMetadata.UserInput = img
|
||
|
log.Entry().Debugf("updated source.ImageMetadata: %#v", bom.Source.ImageMetadata)
|
||
|
|
||
|
log.Entry().Debugf("search for sbom file using the pattern %s", pattern)
|
||
|
syftFiles, err := utils.Glob(pattern)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
for _, syftFile := range syftFiles {
|
||
|
log.Entry().Debugf("reading Syft SBOM file %q", syftFile)
|
||
|
f, err := utils.Open(syftFile)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer f.Close()
|
||
|
|
||
|
bill, _, err := syft.Decode(f)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
for p := range bill.Artifacts.PackageCatalog.Enumerate() {
|
||
|
bom.Artifacts.PackageCatalog.Add(p)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
outFile, err := utils.Abs(output)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
out, err := utils.Create(outFile)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer out.Close()
|
||
|
|
||
|
log.Entry().Debugf("saving CycloneDX SBOM file to %q", outFile)
|
||
|
err = cycloneDxXML.Encode(out, *bom)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func readBOMFromLayer(img v1.Image, layerDiffSHA string) (*sbom.SBOM, error) {
|
||
|
layerDiffDigest, err := v1.NewHash(layerDiffSHA)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrapf(err, "failed to parse layer sha %q", layerDiffSHA)
|
||
|
}
|
||
|
|
||
|
log.Entry().Debugf("looking for the layer %q", layerDiffDigest.String())
|
||
|
|
||
|
sbomLayer, err := img.LayerByDiffID(layerDiffDigest)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrapf(err, "failed get layer %q", layerDiffDigest)
|
||
|
}
|
||
|
|
||
|
rc, err := sbomLayer.Uncompressed()
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "failed to get uncompressed reader")
|
||
|
}
|
||
|
|
||
|
tr := tar.NewReader(rc)
|
||
|
sbomRegex := regexp.MustCompile(`cnb/sbom/[a-z0-9]+\.syft\.json`)
|
||
|
for {
|
||
|
hdr, err := tr.Next()
|
||
|
if err == io.EOF {
|
||
|
break
|
||
|
}
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "failed to read tar content")
|
||
|
}
|
||
|
|
||
|
log.Entry().Debugf("checking SBOM layer file %q", hdr.Name)
|
||
|
if sbomRegex.Match([]byte(hdr.Name)) {
|
||
|
log.Entry().Debugf("file %q matches the regex", hdr.Name)
|
||
|
buf := &bytes.Buffer{}
|
||
|
_, err = io.Copy(buf, tr)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "failed to read SBOM file from the layer")
|
||
|
}
|
||
|
|
||
|
bom, _, err := syft.Decode(buf)
|
||
|
return bom, errors.Wrap(err, "failed to decode SBOM file")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil, errors.New("no sbom file found")
|
||
|
}
|
||
|
|
||
|
func extractImageMetaData(img v1.Image) (*source.ImageMetadata, error) {
|
||
|
imageDigest, err := img.Digest()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
imageMediaType, err := img.MediaType()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
imageSize, err := img.Size()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
imageRawManifest, err := img.RawManifest()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
imageRawConfig, err := img.RawConfigFile()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return &source.ImageMetadata{
|
||
|
ID: imageDigest.String(),
|
||
|
Size: imageSize,
|
||
|
MediaType: string(imageMediaType),
|
||
|
RawConfig: imageRawConfig,
|
||
|
RawManifest: imageRawManifest,
|
||
|
}, nil
|
||
|
}
|