diff --git a/CHANGELOG.md b/CHANGELOG.md index 469c765686..74cb41e0cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ All notable changes to `src-cli` are documented in this file. ## Unreleased +### Added + +- SBOM support: Added `--image` and `--exclude-image` flags to `src sbom fetch` for filtering which docker images SBOMs are fetched for. Both flags support glob patterns (e.g., `frontend`, `*api*`) and comma-separated lists. The `sourcegraph/` image name prefix is optional. + ## 6.4.0 ## 6.3.0 diff --git a/cmd/src/sbom_fetch.go b/cmd/src/sbom_fetch.go index d6fa90f79a..6a8ad4aa85 100644 --- a/cmd/src/sbom_fetch.go +++ b/cmd/src/sbom_fetch.go @@ -25,13 +25,23 @@ func init() { Usage: - src sbom fetch -v + src sbom fetch -v [--image ] [--exclude-image ] Examples: - $ src sbom fetch -v 5.8.0 # Fetch all SBOMs for the 5.8.0 release + $ src sbom fetch -v 5.8.0 # Fetch all SBOMs for the 5.8.0 release - $ src sbom fetch -v 5.8.123 -internal -d /tmp/sboms # Fetch all SBOMs for the internal 5.8.123 release and store them in /tmp/sboms + $ src sbom fetch -v 5.8.0 --image frontend # Fetch SBOM only for the frontend image + + $ src sbom fetch -v 5.8.0 --image "redis*" # Fetch SBOMs for all images with names beginning with 'redis' + + $ src sbom fetch -v 5.8.0 --image "frontend,redis*" # Fetch SBOMs for frontend, and all redis images + + $ src sbom fetch -v 5.8.0 --exclude-image "sg,*redis*" # Fetch SBOMs for all images, except sg and redis + + $ src sbom fetch -v 5.8.0 --image "postgres*" --exclude-image "*exporter*" # Fetch SBOMs for all postgres images, except exporters + + $ src sbom fetch -v 5.8.123 -internal -d /tmp/sboms # Fetch all SBOMs for the internal 5.8.123 release and store them in /tmp/sboms ` flagSet := flag.NewFlagSet("fetch", flag.ExitOnError) @@ -39,6 +49,8 @@ Examples: outputDirFlag := flagSet.String("d", "sourcegraph-sboms", "The directory to store validated SBOMs in.") internalReleaseFlag := flagSet.Bool("internal", false, "Fetch SBOMs for an internal release. Defaults to false.") insecureIgnoreTransparencyLogFlag := flagSet.Bool("insecure-ignore-tlog", false, "Disable transparency log verification. Defaults to false.") + imageFlag := flagSet.String("image", "", "Filter list of image names, to only fetch SBOMs for Docker images with names matching these patterns. Supports literal names, like frontend, and glob patterns like '*postgres*'. Multiple patterns can be specified as a comma-separated list (e.g., 'frontend,*postgres-1?-*'). The 'sourcegraph/' prefix is optional. If not specified, SBOMs for all images are fetched.") + excludeImageFlag := flagSet.String("exclude-image", "", "Exclude Docker images with names matching these patterns from being fetched. Supports the same formats as --image. Takes precedence over --image filters.") handler := func(args []string) error { c := cosignConfig{ @@ -73,6 +85,24 @@ Examples: c.insecureIgnoreTransparencyLog = true } + if imageFlag != nil && *imageFlag != "" { + // Parse comma-separated patterns + patterns := strings.Split(*imageFlag, ",") + for i, pattern := range patterns { + patterns[i] = strings.TrimSpace(pattern) + } + c.imageFilters = patterns + } + + if excludeImageFlag != nil && *excludeImageFlag != "" { + // Parse comma-separated exclude patterns + patterns := strings.Split(*excludeImageFlag, ",") + for i, pattern := range patterns { + patterns[i] = strings.TrimSpace(pattern) + } + c.excludeImageFilters = patterns + } + out := output.NewOutput(flagSet.Output(), output.OutputOpts{Verbose: *verbose}) if err := verifyCosign(); err != nil { diff --git a/cmd/src/sbom_utils.go b/cmd/src/sbom_utils.go index 44e46279fa..40dae7b03e 100644 --- a/cmd/src/sbom_utils.go +++ b/cmd/src/sbom_utils.go @@ -10,6 +10,7 @@ import ( "net/http" "os/exec" "path" + "path/filepath" "regexp" "strings" "time" @@ -26,6 +27,8 @@ type cosignConfig struct { version string internalRelease bool insecureIgnoreTransparencyLog bool + imageFilters []string + excludeImageFilters []string } // TokenResponse represents the JSON response from dockerHub's token service @@ -251,7 +254,21 @@ func (c cosignConfig) getImageList() ([]string, error) { if image != "" { // Strip off a version suffix if present parts := strings.SplitN(image, ":", 2) - images = append(images, parts[0]) + imageName := parts[0] + + // If the --image arg was provided, and if the image name doesn't match any of the filters + // then skip this image + if len(c.imageFilters) > 0 && !matchesImageFilter(c.imageFilters, imageName) { + continue + } + + // If the --exclude-image arg was provided, and if the image name does match any of the filters + // then skip this image + if len(c.excludeImageFilters) > 0 && matchesImageFilter(c.excludeImageFilters, imageName) { + continue + } + + images = append(images, imageName) } } @@ -270,3 +287,24 @@ func (c *cosignConfig) getImageReleaseListURL() string { return fmt.Sprintf("%s/release/%s/%s", imageListBaseURL, c.version, imageListFilename) } } + +// matchesImageFilter checks if the image name from the list of published images +// matches any user-provided --image or --exclude-image glob patterns +// It matches against both the full image name, and the image name without the "sourcegraph/" prefix. +func matchesImageFilter(patterns []string, imageName string) bool { + for _, pattern := range patterns { + // Try matching against the full image name + if matched, _ := filepath.Match(pattern, imageName); matched { + return true + } + + // Try matching against the image name without "sourcegraph/" prefix + if strings.HasPrefix(imageName, "sourcegraph/") { + shortName := strings.TrimPrefix(imageName, "sourcegraph/") + if matched, _ := filepath.Match(pattern, shortName); matched { + return true + } + } + } + return false +}