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 }