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 }