github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/internal/packager/sbom/catalog.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // SPDX-FileCopyrightText: 2021-Present The Jackal Authors
     3  
     4  // Package sbom contains tools for generating SBOMs.
     5  package sbom
     6  
     7  import (
     8  	"embed"
     9  	"fmt"
    10  	"os"
    11  	"path/filepath"
    12  	"regexp"
    13  
    14  	"github.com/Racer159/jackal/src/config"
    15  	"github.com/Racer159/jackal/src/pkg/layout"
    16  	"github.com/Racer159/jackal/src/pkg/message"
    17  	"github.com/Racer159/jackal/src/pkg/transform"
    18  	"github.com/Racer159/jackal/src/pkg/utils"
    19  	"github.com/anchore/stereoscope/pkg/file"
    20  	"github.com/anchore/stereoscope/pkg/image"
    21  	"github.com/anchore/syft/syft"
    22  	"github.com/anchore/syft/syft/artifact"
    23  	syftFile "github.com/anchore/syft/syft/file"
    24  	"github.com/anchore/syft/syft/format"
    25  	"github.com/anchore/syft/syft/format/syftjson"
    26  	"github.com/anchore/syft/syft/linux"
    27  	"github.com/anchore/syft/syft/pkg"
    28  	"github.com/anchore/syft/syft/pkg/cataloger"
    29  	"github.com/anchore/syft/syft/sbom"
    30  	"github.com/anchore/syft/syft/source"
    31  	"github.com/defenseunicorns/pkg/helpers"
    32  	v1 "github.com/google/go-containerregistry/pkg/v1"
    33  )
    34  
    35  // Builder is the main struct used to build SBOM artifacts.
    36  type Builder struct {
    37  	spinner    *message.Spinner
    38  	cachePath  string
    39  	imagesPath string
    40  	outputDir  string
    41  	jsonList   []byte
    42  }
    43  
    44  //go:embed viewer/*
    45  var viewerAssets embed.FS
    46  var transformRegex = regexp.MustCompile(`(?m)[^a-zA-Z0-9\.\-]`)
    47  
    48  var componentPrefix = "jackal-component-"
    49  
    50  // Catalog catalogs the given components and images to create an SBOM.
    51  func Catalog(componentSBOMs map[string]*layout.ComponentSBOM, imageList []transform.Image, paths *layout.PackagePaths) error {
    52  	imageCount := len(imageList)
    53  	componentCount := len(componentSBOMs)
    54  	builder := Builder{
    55  		spinner:    message.NewProgressSpinner("Creating SBOMs for %d images and %d components with files.", imageCount, componentCount),
    56  		cachePath:  config.GetAbsCachePath(),
    57  		imagesPath: paths.Images.Base,
    58  		outputDir:  paths.SBOMs.Path,
    59  	}
    60  	defer builder.spinner.Stop()
    61  
    62  	// Ensure the sbom directory exists
    63  	_ = helpers.CreateDirectory(builder.outputDir, helpers.ReadWriteExecuteUser)
    64  
    65  	// Generate a list of images and files for the sbom viewer
    66  	json, err := builder.generateJSONList(componentSBOMs, imageList)
    67  	if err != nil {
    68  		builder.spinner.Errorf(err, "Unable to generate the SBOM image list")
    69  		return err
    70  	}
    71  	builder.jsonList = json
    72  
    73  	// Generate SBOM for each image
    74  	currImage := 1
    75  	for _, refInfo := range imageList {
    76  		builder.spinner.Updatef("Creating image SBOMs (%d of %d): %s", currImage, imageCount, refInfo.Reference)
    77  
    78  		// Get the image that we are creating an SBOM for
    79  		img, err := utils.LoadOCIImage(paths.Images.Base, refInfo)
    80  		if err != nil {
    81  			builder.spinner.Errorf(err, "Unable to load the image to generate an SBOM")
    82  			return err
    83  		}
    84  
    85  		jsonData, err := builder.createImageSBOM(img, refInfo.Reference)
    86  		if err != nil {
    87  			builder.spinner.Errorf(err, "Unable to create SBOM for image %s", refInfo.Reference)
    88  			return err
    89  		}
    90  
    91  		if err = builder.createSBOMViewerAsset(refInfo.Reference, jsonData); err != nil {
    92  			builder.spinner.Errorf(err, "Unable to create SBOM viewer for image %s", refInfo.Reference)
    93  			return err
    94  		}
    95  
    96  		currImage++
    97  	}
    98  
    99  	currComponent := 1
   100  
   101  	// Generate SBOM for each component
   102  	for component := range componentSBOMs {
   103  		builder.spinner.Updatef("Creating component file SBOMs (%d of %d): %s", currComponent, componentCount, component)
   104  
   105  		if componentSBOMs[component] == nil {
   106  			message.Debugf("Component %s has invalid SBOM, skipping", component)
   107  			continue
   108  		}
   109  
   110  		jsonData, err := builder.createFileSBOM(*componentSBOMs[component], component)
   111  		if err != nil {
   112  			builder.spinner.Errorf(err, "Unable to create SBOM for component %s", component)
   113  			return err
   114  		}
   115  
   116  		if err = builder.createSBOMViewerAsset(fmt.Sprintf("%s%s", componentPrefix, component), jsonData); err != nil {
   117  			builder.spinner.Errorf(err, "Unable to create SBOM viewer for component %s", component)
   118  			return err
   119  		}
   120  
   121  		currComponent++
   122  	}
   123  
   124  	// Include the compare tool if there are any image SBOMs OR component SBOMs
   125  	if len(componentSBOMs) > 0 || len(imageList) > 0 {
   126  		if err := builder.createSBOMCompareAsset(); err != nil {
   127  			builder.spinner.Errorf(err, "Unable to create SBOM compare tool")
   128  			return err
   129  		}
   130  	}
   131  
   132  	if err := paths.SBOMs.Archive(); err != nil {
   133  		builder.spinner.Errorf(err, "Unable to archive SBOMs")
   134  		return err
   135  	}
   136  
   137  	builder.spinner.Success()
   138  
   139  	return nil
   140  }
   141  
   142  // createImageSBOM uses syft to generate SBOM for an image,
   143  // some code/structure migrated from https://github.com/testifysec/go-witness/blob/v0.1.12/attestation/syft/syft.go.
   144  func (b *Builder) createImageSBOM(img v1.Image, src string) ([]byte, error) {
   145  	// Get the image reference.
   146  	refInfo, err := transform.ParseImageRef(src)
   147  	if err != nil {
   148  		return nil, fmt.Errorf("failed to create ref for image %s: %w", src, err)
   149  	}
   150  
   151  	// Create the sbom.
   152  	imageCachePath := filepath.Join(b.cachePath, layout.ImagesDir)
   153  
   154  	// Ensure the image cache directory exists.
   155  	if err := helpers.CreateDirectory(imageCachePath, helpers.ReadWriteExecuteUser); err != nil {
   156  		return nil, err
   157  	}
   158  
   159  	syftImage := image.NewImage(img, file.NewTempDirGenerator("jackal"), imageCachePath, image.WithTags(refInfo.Reference))
   160  	if err := syftImage.Read(); err != nil {
   161  		return nil, err
   162  	}
   163  
   164  	syftSource, err := source.NewFromStereoscopeImageObject(syftImage, "", nil)
   165  	if err != nil {
   166  		return nil, err
   167  	}
   168  
   169  	catalog, relationships, distro, err := syft.CatalogPackages(syftSource, cataloger.DefaultConfig())
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  
   174  	artifact := sbom.SBOM{
   175  		Descriptor: sbom.Descriptor{
   176  			Name: "jackal",
   177  		},
   178  		Source: syftSource.Describe(),
   179  		Artifacts: sbom.Artifacts{
   180  			Packages:          catalog,
   181  			LinuxDistribution: distro,
   182  		},
   183  		Relationships: relationships,
   184  	}
   185  
   186  	jsonData, err := format.Encode(artifact, syftjson.NewFormatEncoder())
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  
   191  	// Write the sbom to disk using the image ref as the filename
   192  	filename := fmt.Sprintf("%s.json", refInfo.Reference)
   193  	sbomFile, err := b.createSBOMFile(filename)
   194  	if err != nil {
   195  		return nil, err
   196  	}
   197  	defer sbomFile.Close()
   198  
   199  	if _, err = sbomFile.Write(jsonData); err != nil {
   200  		return nil, err
   201  	}
   202  
   203  	// Return the json data
   204  	return jsonData, nil
   205  }
   206  
   207  // createPathSBOM uses syft to generate SBOM for a filepath.
   208  func (b *Builder) createFileSBOM(componentSBOM layout.ComponentSBOM, component string) ([]byte, error) {
   209  	catalog := pkg.NewCollection()
   210  	relationships := []artifact.Relationship{}
   211  	parentSource, err := source.NewFromDirectoryPath(componentSBOM.Component.Base)
   212  	if err != nil {
   213  		return nil, err
   214  	}
   215  
   216  	for _, sbomFile := range componentSBOM.Files {
   217  		// Create the sbom source
   218  		fileSource, err := source.NewFromFile(source.FileConfig{Path: sbomFile})
   219  		if err != nil {
   220  			return nil, err
   221  		}
   222  
   223  		// Dogsled distro since this is not a linux image we are scanning
   224  		cat, rel, _, err := syft.CatalogPackages(fileSource, cataloger.DefaultConfig())
   225  		if err != nil {
   226  			return nil, err
   227  		}
   228  
   229  		for pkg := range cat.Enumerate() {
   230  			containsSource := false
   231  
   232  			// See if the source locations for this package contain the file Jackal indexed
   233  			for _, location := range pkg.Locations.ToSlice() {
   234  				if location.RealPath == fileSource.Describe().Metadata.(source.FileSourceMetadata).Path {
   235  					containsSource = true
   236  				}
   237  			}
   238  
   239  			// If the locations do not contain the source file (i.e. the package was inside a tarball), add the file source
   240  			if !containsSource {
   241  				sourceLocation := syftFile.NewLocation(fileSource.Describe().Metadata.(source.FileSourceMetadata).Path)
   242  				pkg.Locations.Add(sourceLocation)
   243  			}
   244  
   245  			catalog.Add(pkg)
   246  		}
   247  
   248  		for _, r := range rel {
   249  			relationships = append(relationships, artifact.Relationship{
   250  				From: parentSource,
   251  				To:   r.To,
   252  				Type: r.Type,
   253  				Data: r.Data,
   254  			})
   255  		}
   256  	}
   257  
   258  	artifact := sbom.SBOM{
   259  		Descriptor: sbom.Descriptor{
   260  			Name: "jackal",
   261  		},
   262  		Source: parentSource.Describe(),
   263  		Artifacts: sbom.Artifacts{
   264  			Packages:          catalog,
   265  			LinuxDistribution: &linux.Release{},
   266  		},
   267  		Relationships: relationships,
   268  	}
   269  
   270  	jsonData, err := format.Encode(artifact, syftjson.NewFormatEncoder())
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  
   275  	// Write the sbom to disk using the component prefix and name as the filename
   276  	filename := fmt.Sprintf("%s%s.json", componentPrefix, component)
   277  	sbomFile, err := b.createSBOMFile(filename)
   278  	if err != nil {
   279  		return nil, err
   280  	}
   281  	defer sbomFile.Close()
   282  
   283  	if _, err = sbomFile.Write(jsonData); err != nil {
   284  		return nil, err
   285  	}
   286  
   287  	// Return the json data
   288  	return jsonData, nil
   289  }
   290  
   291  func (b *Builder) getNormalizedFileName(identifier string) string {
   292  	return transformRegex.ReplaceAllString(identifier, "_")
   293  }
   294  
   295  func (b *Builder) createSBOMFile(filename string) (*os.File, error) {
   296  	path := filepath.Join(b.outputDir, b.getNormalizedFileName(filename))
   297  	return os.Create(path)
   298  }