From 88b17f4b477cfbfdb54ae855e4d7da8053d83acd Mon Sep 17 00:00:00 2001 From: Warren Gifford Date: Fri, 13 Mar 2026 01:35:23 -0700 Subject: [PATCH] remove sbom dependent commands --- .gitignore | 1 - CHANGELOG.md | 4 + cmd/src/main.go | 1 - cmd/src/sbom.go | 36 ----- cmd/src/sbom_fetch.go | 295 ----------------------------------- cmd/src/sbom_utils.go | 310 ------------------------------------- cmd/src/signature.go | 36 ----- cmd/src/signature_fetch.go | 202 ------------------------ 8 files changed, 4 insertions(+), 881 deletions(-) delete mode 100644 cmd/src/sbom.go delete mode 100644 cmd/src/sbom_fetch.go delete mode 100644 cmd/src/sbom_utils.go delete mode 100644 cmd/src/signature.go delete mode 100644 cmd/src/signature_fetch.go diff --git a/.gitignore b/.gitignore index 5a3d0f8b09..e0f1fba3a9 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,3 @@ bazel-zoekt bazel-src-cli .DS_Store samples -sourcegraph-sboms/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 409805aa3e..0f87c3d286 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ All notable changes to `src-cli` are documented in this file. ### Changed +### Removed + +- Removed `src sbom` and `src signature` commands. SBOMs and container signatures are no longer published as of Sourcegraph 7.1.0. + ## 6.7.1104 ### Added diff --git a/cmd/src/main.go b/cmd/src/main.go index 00f0366781..fa308072b7 100644 --- a/cmd/src/main.go +++ b/cmd/src/main.go @@ -61,7 +61,6 @@ The commands are: orgs,org manages organizations teams,team manages teams repos,repo manages repositories - sbom manages SBOM (Software Bill of Materials) data search search for results on Sourcegraph search-jobs manages search jobs serve-git serves your local git repositories over HTTP for Sourcegraph to pull diff --git a/cmd/src/sbom.go b/cmd/src/sbom.go deleted file mode 100644 index ff1a0a5c33..0000000000 --- a/cmd/src/sbom.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "flag" - "fmt" -) - -var sbomCommands commander - -func init() { - usage := `'src sbom' fetches and verifies SBOM (Software Bill of Materials) data for Sourcegraph containers. - -Usage: - - src sbom command [command options] - -The commands are: - - fetch fetch SBOMs for a released version of Sourcegraph -` - flagSet := flag.NewFlagSet("sbom", flag.ExitOnError) - handler := func(args []string) error { - sbomCommands.run(flagSet, "src sbom", usage, args) - return nil - } - - // Register the command. - commands = append(commands, &command{ - flagSet: flagSet, - aliases: []string{"sbom"}, - handler: handler, - usageFunc: func() { - fmt.Println(usage) - }, - }) -} diff --git a/cmd/src/sbom_fetch.go b/cmd/src/sbom_fetch.go deleted file mode 100644 index 6a8ad4aa85..0000000000 --- a/cmd/src/sbom_fetch.go +++ /dev/null @@ -1,295 +0,0 @@ -package main - -import ( - "bytes" - "encoding/base64" - "encoding/json" - "flag" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "unicode" - - "github.com/sourcegraph/sourcegraph/lib/output" - - "github.com/sourcegraph/src-cli/internal/cmderrors" -) - -const sbomPublicKey = "https://storage.googleapis.com/sourcegraph-release-sboms/keys/cosign_keyring-cosign-1.pub" - -func init() { - usage := ` -'src sbom fetch' fetches and verifies SBOMs for the given release version of Sourcegraph. - -Usage: - - 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 --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) - versionFlag := flagSet.String("v", "", "The version of Sourcegraph to fetch SBOMs for.") - 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{ - publicKey: sbomPublicKey, - } - - if err := flagSet.Parse(args); err != nil { - return err - } - - if len(flagSet.Args()) != 0 { - return cmderrors.Usage("additional arguments not allowed") - } - - if versionFlag == nil || *versionFlag == "" { - return cmderrors.Usage("version is required") - } - c.version = sanitizeVersion(*versionFlag) - - if outputDirFlag == nil || *outputDirFlag == "" { - return cmderrors.Usage("output directory is required") - } - c.outputDir = getOutputDir(*outputDirFlag, c.version) - - if internalReleaseFlag == nil || !*internalReleaseFlag { - c.internalRelease = false - } else { - c.internalRelease = true - } - - if insecureIgnoreTransparencyLogFlag != nil && *insecureIgnoreTransparencyLogFlag { - 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 { - return cmderrors.ExitCode(1, err) - } - - images, err := c.getImageList() - if err != nil { - return err - } - - out.Writef("Fetching SBOMs and validating signatures for all %d images in the Sourcegraph %s release...\n", len(images), c.version) - - if c.insecureIgnoreTransparencyLog { - out.WriteLine(output.Line("⚠️", output.StyleWarning, "WARNING: Transparency log verification is disabled, increasing the risk that SBOMs may have been tampered with.")) - out.WriteLine(output.Line("️", output.StyleWarning, " This setting should only be used for testing or under explicit instruction from Sourcegraph.\n")) - } - - var successCount, failureCount int - for _, image := range images { - stopSpinner := make(chan bool) - go spinner(image, stopSpinner) - - _, err = c.getSBOMForImageVersion(image, c.version) - - stopSpinner <- true - - if err != nil { - out.WriteLine(output.Line(output.EmojiFailure, output.StyleWarning, - fmt.Sprintf("\r%s: error fetching and validating SBOM:\n %v", image, err))) - failureCount += 1 - } else { - out.WriteLine(output.Line("\r\u2705", output.StyleSuccess, image)) - successCount += 1 - } - } - - out.Write("") - if failureCount == 0 && successCount == 0 { - out.WriteLine(output.Line("🔴", output.StyleWarning, "Failed to fetch SBOMs for any images")) - } - if failureCount > 0 { - out.WriteLine(output.Line("🟠", output.StyleOrange, fmt.Sprintf("Fetched verified SBOMs for %d images, but failed to fetch SBOMs for %d images", successCount, failureCount))) - } else if successCount > 0 { - out.WriteLine(output.Line("🟢", output.StyleSuccess, fmt.Sprintf("Fetched verified SBOMs for %d images", successCount))) - } - - out.Writef("\nFetched and validated SBOMs have been written to `%s`.\n", c.outputDir) - out.WriteLine(output.Linef("", output.StyleBold, "Your Sourcegraph deployment may not use all of these images. Please check your deployment to confirm which images are used.\n")) - - if failureCount > 0 || successCount == 0 { - return cmderrors.ExitCode1 - } - - return nil - } - - sbomCommands = append(sbomCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src sbom %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - }, - }) -} - -func (c cosignConfig) getSBOMForImageVersion(image string, version string) (string, error) { - hash, err := getImageDigest(image, version) - if err != nil { - return "", err - } - - sbom, err := c.getSBOMForImageHash(image, hash) - if err != nil { - return "", err - } - - return sbom, nil -} - -func (c cosignConfig) getSBOMForImageHash(image string, hash string) (string, error) { - tempDir, err := os.MkdirTemp("", "sbom-") - if err != nil { - return "", fmt.Errorf("failed to create temporary directory: %w", err) - } - defer os.RemoveAll(tempDir) - - outputFile := filepath.Join(tempDir, "attestation.json") - - cosignArgs := []string{ - "verify-attestation", - "--key", c.publicKey, - "--type", "cyclonedx", - fmt.Sprintf("%s@%s", image, hash), - "--output-file", outputFile, - } - - if c.insecureIgnoreTransparencyLog { - cosignArgs = append(cosignArgs, "--insecure-ignore-tlog") - } - - cmd := exec.Command("cosign", cosignArgs...) - - output, err := cmd.CombinedOutput() - if err != nil { - return "", fmt.Errorf("SBOM fetching or validation failed: %w\nOutput: %s", err, output) - } - - attestation, err := os.ReadFile(outputFile) - if err != nil { - return "", fmt.Errorf("failed to read SBOM file: %w", err) - } - - sbom, err := extractSBOM(attestation) - if err != nil { - return "", fmt.Errorf("failed to extract SBOM from attestation: %w", err) - } - - c.storeSBOM(sbom, image) - - return sbom, nil -} - -type attestation struct { - PayloadType string `json:"payloadType"` - Base64Payload string `json:"payload"` -} - -func extractSBOM(attestationBytes []byte) (string, error) { - // Ensure we only use the first line - occasionally Cosign includes multiple lines - lines := bytes.Split(attestationBytes, []byte("\n")) - if len(lines) == 0 { - return "", fmt.Errorf("attestation is empty") - } - - var a attestation - if err := json.Unmarshal(lines[0], &a); err != nil { - return "", fmt.Errorf("failed to unmarshal attestation: %w", err) - } - - if a.PayloadType != "application/vnd.in-toto+json" { - return "", fmt.Errorf("unexpected payload type: %s", a.PayloadType) - } - - decodedPayload, err := base64.StdEncoding.DecodeString(a.Base64Payload) - if err != nil { - return "", fmt.Errorf("failed to decode payload: %w", err) - } - - // Unmarshal the decoded payload to extract predicate - var payload map[string]json.RawMessage - if err := json.Unmarshal(decodedPayload, &payload); err != nil { - return "", fmt.Errorf("failed to unmarshal decoded payload: %w", err) - } - - // Extract just the predicate field - predicate, ok := payload["predicate"] - if !ok { - return "", fmt.Errorf("no predicate field found in payload") - } - - return string(predicate), nil -} - -func (c cosignConfig) storeSBOM(sbom string, image string) error { - // Make the image name safe for use as a filename - safeImageName := strings.Map(func(r rune) rune { - if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-' || r == '_' { - return r - } - return '_' - }, image) - - // Create the output file path - outputFile := filepath.Join(c.outputDir, safeImageName+".cdx.json") - - // Ensure the output directory exists - if err := os.MkdirAll(c.outputDir, 0755); err != nil { - return fmt.Errorf("failed to create output directory: %w", err) - } - - // Write the SBOM to the file - if err := os.WriteFile(outputFile, []byte(sbom), 0644); err != nil { - return fmt.Errorf("failed to write SBOM file: %w", err) - } - - return nil -} diff --git a/cmd/src/sbom_utils.go b/cmd/src/sbom_utils.go deleted file mode 100644 index 5bae2c970c..0000000000 --- a/cmd/src/sbom_utils.go +++ /dev/null @@ -1,310 +0,0 @@ -package main - -// Utility functions used by the SBOM and Signature commands. - -import ( - "bufio" - "encoding/json" - "fmt" - "io" - "net/http" - "os/exec" - "path" - "path/filepath" - "regexp" - "strings" - "time" - - "github.com/sourcegraph/sourcegraph/lib/errors" -) - -const imageListBaseURL = "https://storage.googleapis.com/sourcegraph-release-sboms" -const imageListFilename = "release-image-list.txt" - -type cosignConfig struct { - publicKey string - outputDir string - version string - internalRelease bool - insecureIgnoreTransparencyLog bool - imageFilters []string - excludeImageFilters []string -} - -// TokenResponse represents the JSON response from dockerHub's token service -type dockerHubTokenResponse struct { - Token string `json:"token"` -} - -// getImageDigest returns the sha256 hash for the given image and tag -// It supports multiple registries -func getImageDigest(image string, tag string) (string, error) { - if strings.HasPrefix(image, "sourcegraph/") { - return getImageDigestDockerHub(image, tag) - } else if strings.HasPrefix(image, "us-central1-docker.pkg.dev/") { - return getImageDigestGcloud(image, tag) - } else { - return "", fmt.Errorf("unsupported image registry: %s", image) - } -} - -// -// Implement functionality for Docker Hub - -// getImageDigestDockerHub returns the sha256 digest for the given image and tag from DockerHub -func getImageDigestDockerHub(image string, tag string) (string, error) { - // Construct the DockerHub manifest URL - url := fmt.Sprintf("https://registry-1.docker.io/v2/%s/manifests/%s", image, tag) - - token, err := getDockerHubAuthToken(image) - if err != nil { - return "", err - } - - // Create a new HTTP request with the authorization header - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return "", err - } - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) - req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json") - - // Make the HTTP request - resp, err := http.DefaultClient.Do(req) - if err != nil { - return "", fmt.Errorf("failed to fetch image manifest: %v", err) - } - defer resp.Body.Close() - - // Check for a successful response - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("failed to get manifest - check %s is a valid Sourcegraph release, status code: %d", tag, resp.StatusCode) - } - - // Get the image digest from the `Docker-Content-Digest` header - digest := resp.Header.Get("Docker-Content-Digest") - if digest == "" { - return "", fmt.Errorf("digest not found in response headers") - } - // Return the image's digest (hash) - return digest, nil -} - -// getDockerHubAuthToken returns an auth token with scope to pull the given image -// Note that the token has a short validity so it should be used immediately -func getDockerHubAuthToken(image string) (string, error) { - // Set the DockerHub authentication URL - url := fmt.Sprintf("https://auth.docker.io/token?service=registry.docker.io&scope=repository:%s:pull", image) - - // Create a new HTTP request - resp, err := http.Get(url) - if err != nil { - return "", fmt.Errorf("failed to get token: %v", err) - } - defer resp.Body.Close() - - // Check if the response status is 200 OK - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("failed to get token, status code: %d", resp.StatusCode) - } - - // Read the response body - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("failed to read response body: %v", err) - } - - // Unmarshal the JSON response - var tokenResponse dockerHubTokenResponse - if err := json.Unmarshal(body, &tokenResponse); err != nil { - return "", fmt.Errorf("failed to parse token response: %v", err) - } - - // Return the token - return tokenResponse.Token, nil -} - -// -// Implement functionality for GCP Artifact Registry - -// getImageDigestGcloud fetches the OCI image manifest from GCP Artifact Registry and returns the image digest -func getImageDigestGcloud(image string, tag string) (string, error) { - // Validate image path to ensure it's a valid GCP Artifact Registry image - if !strings.HasPrefix(image, "us-central1-docker.pkg.dev/") { - return "", fmt.Errorf("invalid image format: %s", image) - } - - // Get the GCP access token - token, err := getGcloudAccessToken() - if err != nil { - return "", fmt.Errorf("error getting access token: %v", err) - } - - parts := strings.SplitN(image, "/", 2) - if len(parts) != 2 { - return "", fmt.Errorf("invalid image format: %s", image) - } - domain := parts[0] - repositoryPath := parts[1] - - // Create the URL to fetch the manifest for the specific image and tag - url := fmt.Sprintf("https://%s/v2/%s/manifests/%s", domain, repositoryPath, tag) - - // Create a new HTTP GET request - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return "", fmt.Errorf("failed to create HTTP request: %v", err) - } - - // Add the Authorization and Accept headers - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) - req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json") - - // Perform the HTTP request - resp, err := http.DefaultClient.Do(req) - if err != nil { - return "", fmt.Errorf("failed to fetch manifest: %v", err) - } - defer resp.Body.Close() - - // Check if the request was successful - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return "", fmt.Errorf("failed to get manifest, status code: %d, response: %s", resp.StatusCode, string(body)) - } - - // Get the image digest from the `Docker-Content-Digest` header - digest := resp.Header.Get("Docker-Content-Digest") - if digest == "" { - return "", fmt.Errorf("digest not found in response headers") - } - - return digest, nil -} - -// getGcloudAccessToken runs 'gcloud auth print-access-token' and returns the access token -func getGcloudAccessToken() (string, error) { - // Execute the gcloud command to get the access token - cmd := exec.Command("gcloud", "auth", "print-access-token") - out, err := cmd.Output() - if err != nil { - return "", fmt.Errorf("failed to retrieve access token using `gcloud auth`. Ensure that gcloud is installed and you have authenticated: %v", err) - } - - // Trim any extra whitespace or newlines - token := strings.TrimSpace(string(out)) - return token, nil -} - -var spinnerChars = []rune{'⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'} - -func spinner(name string, stop chan bool) { - i := 0 - for { - select { - case <-stop: - return - default: - fmt.Printf("\r%s %s", string(spinnerChars[i%len(spinnerChars)]), name) - i++ - time.Sleep(100 * time.Millisecond) - } - } -} - -func getOutputDir(parentDir, version string) string { - return path.Join(parentDir, "sourcegraph-"+version) -} - -// sanitizeVersion removes any leading "v" from the version string -func sanitizeVersion(version string) string { - return strings.TrimPrefix(version, "v") -} - -func verifyCosign() error { - _, err := exec.LookPath("cosign") - if err != nil { - return errors.New("SBOM verification requires 'cosign' to be installed and available in $PATH. See https://docs.sigstore.dev/cosign/system_config/installation/") - } - return nil -} - -func (c cosignConfig) getImageList() ([]string, error) { - imageReleaseListURL := c.getImageReleaseListURL() - - resp, err := http.Get(imageReleaseListURL) - if err != nil { - return nil, fmt.Errorf("failed to fetch image list: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - // Compare version number against a regex that matches versions up to and including 5.8.0 - versionRegex := regexp.MustCompile(`^v?[0-5]\.([0-7]\.[0-9]+|8\.0)$`) - if versionRegex.MatchString(c.version) { - return nil, fmt.Errorf("unsupported version %s: SBOMs are only available for Sourcegraph releases after 5.8.0", c.version) - } - return nil, fmt.Errorf("failed to fetch list of images - check that %s is a valid Sourcegraph release: HTTP status %d", c.version, resp.StatusCode) - } - - scanner := bufio.NewScanner(resp.Body) - var images []string - for scanner.Scan() { - image := strings.TrimSpace(scanner.Text()) - if image != "" { - // Strip off a version suffix if present - parts := strings.SplitN(image, ":", 2) - 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) - } - } - - if err := scanner.Err(); err != nil { - return nil, fmt.Errorf("error reading image list: %w", err) - } - - return images, nil -} - -// getImageReleaseListURL returns the URL for the list of images in a release, based on the version and whether it's an internal release. -func (c *cosignConfig) getImageReleaseListURL() string { - if c.internalRelease { - return fmt.Sprintf("%s/release-internal/%s/%s", imageListBaseURL, c.version, imageListFilename) - } else { - 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 after, ok := strings.CutPrefix(imageName, "sourcegraph/"); ok { - shortName := after - if matched, _ := filepath.Match(pattern, shortName); matched { - return true - } - } - } - return false -} diff --git a/cmd/src/signature.go b/cmd/src/signature.go deleted file mode 100644 index 0d61014aec..0000000000 --- a/cmd/src/signature.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "flag" - "fmt" -) - -var signatureCommands commander - -func init() { - usage := `'src signature' verifies published signatures for Sourcegraph containers. - -Usage: - - src signature command [command options] - -The commands are: - - verify verify signatures for a Sourcegraph release -` - flagSet := flag.NewFlagSet("signature", flag.ExitOnError) - handler := func(args []string) error { - signatureCommands.run(flagSet, "src signature", usage, args) - return nil - } - - // Register the command. - commands = append(commands, &command{ - flagSet: flagSet, - aliases: []string{"signature", "sig"}, - handler: handler, - usageFunc: func() { - fmt.Println(usage) - }, - }) -} diff --git a/cmd/src/signature_fetch.go b/cmd/src/signature_fetch.go deleted file mode 100644 index 4b9fd03a16..0000000000 --- a/cmd/src/signature_fetch.go +++ /dev/null @@ -1,202 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/sourcegraph/sourcegraph/lib/output" - - "github.com/sourcegraph/src-cli/internal/cmderrors" -) - -const signaturePublicKey = "https://storage.googleapis.com/sourcegraph-release-sboms/keys/cosign_keyring-cosign_image_signing_key-1.pub" - -func init() { - usage := ` -'src signature verify' verifies signatures for the given release version of Sourcegraph. - -Usage: - - src signature verify -v - -Examples: - - $ src signature verify -v 5.11.4013 # Verify all signatures for the 5.11.4013 release - - $ src signature verify -v 6.0.0 -d /tmp/signatures # Verify all signatures for the 6.0.0 release and write verified image digests under /tmp/signatures -` - - flagSet := flag.NewFlagSet("verify", flag.ExitOnError) - versionFlag := flagSet.String("v", "", "The version of Sourcegraph to verify signatures for.") - outputDirFlag := flagSet.String("d", "", "The directory to store verified image digests in.") - insecureIgnoreTransparencyLogFlag := flagSet.Bool("insecure-ignore-tlog", false, "Disable transparency log verification. Defaults to false.") - - handler := func(args []string) error { - c := cosignConfig{ - publicKey: signaturePublicKey, - internalRelease: false, - } - - if err := flagSet.Parse(args); err != nil { - return err - } - - if len(flagSet.Args()) != 0 { - return cmderrors.Usage("additional arguments not allowed") - } - - if versionFlag == nil || *versionFlag == "" { - return cmderrors.Usage("version is required") - } - c.version = sanitizeVersion(*versionFlag) - - if outputDirFlag != nil && *outputDirFlag != "" { - c.outputDir = getOutputDir(*outputDirFlag, c.version) - } - - if insecureIgnoreTransparencyLogFlag != nil && *insecureIgnoreTransparencyLogFlag { - c.insecureIgnoreTransparencyLog = true - } - - out := output.NewOutput(flagSet.Output(), output.OutputOpts{Verbose: *verbose}) - - if err := verifyCosign(); err != nil { - return cmderrors.ExitCode(1, err) - } - - images, err := c.getImageList() - if err != nil { - return err - } - - out.Writef("Verifying signatures for all %d images in the Sourcegraph %s release...\n", len(images), c.version) - - if c.insecureIgnoreTransparencyLog { - out.WriteLine(output.Line("⚠️", output.StyleWarning, "WARNING: Transparency log verification is disabled, increasing the risk that images may have been tampered with.")) - out.WriteLine(output.Line("️", output.StyleWarning, " This setting should only be used for testing or under explicit instruction from Sourcegraph.\n")) - } - - var successCount, failureCount int - var verifiedDigests []string - for _, image := range images { - stopSpinner := make(chan bool) - go spinner(image, stopSpinner) - - verifiedDigest, err := c.verifySignatureForImageVersion(image, c.version) - verifiedDigests = append(verifiedDigests, image+"@"+verifiedDigest) - - stopSpinner <- true - - if err != nil { - out.WriteLine(output.Line(output.EmojiFailure, output.StyleWarning, - fmt.Sprintf("\r%s: error verifying signature:\n %v", image, err))) - failureCount += 1 - } else { - out.WriteLine(output.Line("\r\u2705", output.StyleSuccess, image+"@"+verifiedDigest)) - successCount += 1 - } - } - - out.Write("") - if successCount > 0 && failureCount > 0 { - out.WriteLine(output.Line("🟠", output.StyleOrange, fmt.Sprintf("Verified signatures and digests for %d images, but failed to verify signatures for %d images", successCount, failureCount))) - } else if successCount > 0 && failureCount == 0 { - out.WriteLine(output.Line("🟢", output.StyleSuccess, fmt.Sprintf("Verified signatures and digests for %d images", successCount))) - } else { - out.WriteLine(output.Line("🔴", output.StyleWarning, "Failed to verify signatures for any images")) - - } - - if c.outputDir != "" { - if err = c.writeVerifiedDigests(verifiedDigests); err != nil { - out.WriteLine(output.Line("🔴", output.StyleWarning, err.Error())) - return cmderrors.ExitCode1 - } - out.Writef("\nVerified digests have been written to `%s`.\n", c.getOutputFilepath()) - } - out.WriteLine(output.Linef("", output.StyleBold, "Your Sourcegraph deployment may not use all of these images. Please check your deployment to confirm which images are used.\n")) - - if failureCount > 0 || successCount == 0 { - return cmderrors.ExitCode1 - } - - return nil - } - - signatureCommands = append(signatureCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src signature %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - }, - }) -} - -func (c cosignConfig) verifySignatureForImageVersion(image string, version string) (string, error) { - digest, err := getImageDigest(image, version) - if err != nil { - return "", err - } - - err = c.verifySignatureForImageHash(image, digest) - if err != nil { - return "", err - } - - // Only return the digest if the signature is verified - return digest, nil -} - -func (c cosignConfig) verifySignatureForImageHash(image string, hash string) error { - tempDir, err := os.MkdirTemp("", "signature-") - if err != nil { - return fmt.Errorf("failed to create temporary directory: %w", err) - } - defer os.RemoveAll(tempDir) - - cosignArgs := []string{ - "verify", - "--key", c.publicKey, - fmt.Sprintf("%s@%s", image, hash), - } - - if c.insecureIgnoreTransparencyLog { - cosignArgs = append(cosignArgs, "--insecure-ignore-tlog") - } - - cmd := exec.Command("cosign", cosignArgs...) - - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("signature verification failed: %w\nOutput: %s", err, output) - } - - return nil -} - -func (c cosignConfig) writeVerifiedDigests(verifiedDigests []string) error { - // Create the output file - outputFile := c.getOutputFilepath() - err := os.MkdirAll(c.outputDir, 0755) - if err != nil { - return fmt.Errorf("failed to create output directory: %w", err) - } - - // Write the verified digests to the file, one per line - err = os.WriteFile(outputFile, []byte(strings.Join(verifiedDigests, "\n")+"\n"), 0644) - if err != nil { - return fmt.Errorf("failed to write verified digests to file: %w", err) - } - - return nil -} - -func (c cosignConfig) getOutputFilepath() string { - return filepath.Join(c.outputDir, "verified-digests.txt") -}