From df4cb43632cf2d13c4fde36ee2f09caee8a6495a Mon Sep 17 00:00:00 2001 From: naveensrinivasan <172697+naveensrinivasan@users.noreply.github.com> Date: Thu, 23 Feb 2023 17:28:59 -0600 Subject: [PATCH] Add Dependency Analysis Action and Dockerfile New scorecard action https://github.com/ossf/scorecard-action/issues/1070 - Add workflow to publish dependency analysis Docker image - Add a new filter function to filter slices - Add a GetScorecardChecks function to get scorecard checks - Add a GetScore function to get score of a repo - Add a Validate function to validate token, owner, repo, commitSHA, and PR - Add a new action file for OSSF Scorecard dependency analysis - Add structs for ScorecardResult, Check, DependencyDiff, and V Signed-off-by: naveensrinivasan <172697+naveensrinivasan@users.noreply.github.com> --- .../workflows/publish-dependency-image.yml | 45 +++ Dockerfile-dependency-analysis | 35 ++ dependency-analysis/README.md | 16 + dependency-analysis/action.yaml | 23 ++ dependency-analysis/main.go | 327 ++++++++++++++++++ dependency-analysis/main_test.go | 215 ++++++++++++ dependency-analysis/types.go | 44 +++ go.mod | 2 +- 8 files changed, 706 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/publish-dependency-image.yml create mode 100644 Dockerfile-dependency-analysis create mode 100644 dependency-analysis/README.md create mode 100644 dependency-analysis/action.yaml create mode 100644 dependency-analysis/main.go create mode 100644 dependency-analysis/main_test.go create mode 100644 dependency-analysis/types.go diff --git a/.github/workflows/publish-dependency-image.yml b/.github/workflows/publish-dependency-image.yml new file mode 100644 index 00000000..e13f9729 --- /dev/null +++ b/.github/workflows/publish-dependency-image.yml @@ -0,0 +1,45 @@ +name: Publish Dependency Analysis Docker image + +on: + push: + branches: + - main + tags: + - 'v*' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Log in to the Container registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-dependency-analysis + + - name: Build and push Docker image + uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + with: + context: . + push: true + file: ./Dockerfile-dependency-analysis + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/Dockerfile-dependency-analysis b/Dockerfile-dependency-analysis new file mode 100644 index 00000000..5beee5e0 --- /dev/null +++ b/Dockerfile-dependency-analysis @@ -0,0 +1,35 @@ +# Copyright 2023 Security Scorecard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Testing: docker run -e GITHUB_REPOSITORY_OWNER=naveensrinivasan \ +# -e GITHUB_REPOSITORY=scorecard-action \ +# -e GITHUB_SHA=3fd6b13799a3e63276d0913fefa90c0e9ca32e31 \ +# -e GITHUB_TOKEN=GH_TOKEN \ +# -e GITHUB_PR_NUMBER=9 \ + +#v1.19 go +FROM golang:1.19.5@sha256:bb9811fad43a7d6fd2173248d8331b2dcf5ac9af20976b1937ecd214c5b8c383 AS builder +WORKDIR / +ENV CGO_ENABLED=0 +COPY go.mod go.sum ./ +COPY dependency-analysis/*.go / + +FROM builder AS build +ARG TARGETOS +ARG TARGETARCH +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o /dependency-analysis / + +FROM gcr.io/distroless/base@sha256:122585ba4c098993df9f8dc7285433e8a19974de32528ee3a4b07308808c84ce +COPY --from=build /dependency-analysis /dependency-analysis +ENTRYPOINT ["/dependency-analysis"] diff --git a/dependency-analysis/README.md b/dependency-analysis/README.md new file mode 100644 index 00000000..fb6b1de6 --- /dev/null +++ b/dependency-analysis/README.md @@ -0,0 +1,16 @@ +# OpenSSF Dependency Analysis + +This repository contains the source code for the OpenSSF Dependency Analysis project. + +## Overview +The OpenSSF Dependency Analysis project is to check the security posture of a project's dependencies. +It uses https://docs.github.com/en/rest/dependency-graph/dependency-review?apiVersion=2022-11-28#get-a-diff-of-the-dependencies-between-commits +to get the dependencies of a project and then uses https://api.securityscorecards.dev to get the security posture of the dependencies. +https://github.com/ossf/scorecard-action/issues/1070 + +## Usage +The project is a GitHub Action that can be used in a workflow. The workflow can be triggered on a push or pull request event. + +This will run the action on the latest commit on the default branch of the repository and will create a comment on the pull request with the results of the analysis. + +Something like this: https://github.com/ossf-tests/vulpy/pull/2#issuecomment-1442310469 \ No newline at end of file diff --git a/dependency-analysis/action.yaml b/dependency-analysis/action.yaml new file mode 100644 index 00000000..b8d8cb86 --- /dev/null +++ b/dependency-analysis/action.yaml @@ -0,0 +1,23 @@ +# Copyright 2023 Security Scorecard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Action syntax: https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions. + +name: "OSSF Scorecard dependency analysis" +description: "Run OSSF Scorecard dependency analysis on your repository to get quality metrics on your dependencies." +author: "OSSF - github.com/ossf/scorecard" + +runs: + using: "docker" + image: "docker://ghcr.io/ossf/scorecard-action-dependency-analysis:latest" diff --git a/dependency-analysis/main.go b/dependency-analysis/main.go new file mode 100644 index 00000000..73cb8ee0 --- /dev/null +++ b/dependency-analysis/main.go @@ -0,0 +1,327 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "strconv" + "strings" + + "github.com/google/go-github/v46/github" + "golang.org/x/oauth2" +) + +func main() { + vulnerabilities := "" + result := "" + + owner := os.Getenv("GITHUB_REPOSITORY_OWNER") + repo := os.Getenv("GITHUB_REPOSITORY") + commitSHA := os.Getenv("GITHUB_SHA") + token := os.Getenv("GITHUB_TOKEN") + pr := os.Getenv("GITHUB_PR_NUMBER") + ghUser := os.Getenv("GITHUB_ACTOR") + if err := Validate(token, owner, repo, commitSHA, pr); err != nil { + log.Fatal(err) + } + + ownerRepo := strings.Split(repo, "/") + owner = ownerRepo[0] + repo = ownerRepo[1] + checks, err := GetScorecardChecks() + if err != nil { + log.Fatal(err) + } + + defaultBranch, err := getDefaultBranch(owner, repo, token) + if err != nil { + log.Fatal(err) + } + + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + tc := oauth2.NewClient(context.Background(), ts) + client := github.NewClient(tc) + data, err := GetDependencyDiff(owner, repo, token, defaultBranch, commitSHA) + if err != nil { + log.Fatal(err) + } + + m := make(map[string]DependencyDiff) + for _, dep := range data { + m[dep.SourceRepositoryURL] = dep + } + + for k, i := range m { + url := strings.TrimPrefix(k, "https://") + scorecard, error := GetScore(url) + if error != nil && len(i.Vulnerabilities) > 0 { + sb := strings.Builder{} + sb.WriteString(fmt.Sprintf("
Vulnerabilties %s\n
", i.SourceRepositoryURL)) + sb.WriteString("\n") + sb.WriteString("\n") + sb.WriteString("\n") + sb.WriteString("\n") + sb.WriteString("\n") + sb.WriteString("\n") + sb.WriteString("\n") + for _, v := range i.Vulnerabilities { + sb.WriteString("\n") + sb.WriteString(fmt.Sprintf("\n", v.Severity)) + sb.WriteString(fmt.Sprintf("\n", v.AdvisoryGhsaId)) + sb.WriteString(fmt.Sprintf("\n", v.AdvisorySummary)) + sb.WriteString(fmt.Sprintf("\n", v.AdvisoryUrl)) + } + sb.WriteString("
SeverityAdvisoryGhsaIdAdvisorySummaryAdvisoryUrl
%s%s%s%s
\n") + sb.WriteString("
\n") + vulnerabilities += sb.String() + continue + } + scorecard.Checks = filter(scorecard.Checks, func(check Check) bool { + for _, c := range checks { + if check.Name == c { + return true + } + } + return false + }) + scorecard.Vulnerabilities = i.Vulnerabilities + result += GitHubIssueComment(scorecard) + } + // convert pr to int + prInt, err := strconv.Atoi(pr) + if err != nil { + log.Fatal(err) + } + // create or update comment + if vulnerabilities == "" && result == "" { + return + } + if err := createOrUpdateComment(client, owner, ghUser, repo, prInt, "## Scorecard Results
\n"+vulnerabilities+"
"+result); err != nil { + log.Fatal(err) + } + if vulnerabilities != "" { + // this will fail the workflow if there are any vulnerabilities + os.Exit(1) + } +} + +// getDefaultBranch gets the default branch of the repository. +func getDefaultBranch(owner, repo, token string) (string, error) { + ctx := context.Background() + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token}, + ) + tc := oauth2.NewClient(ctx, ts) + client := github.NewClient(tc) + + repository, _, err := client.Repositories.Get(ctx, owner, repo) + if err != nil { + return "", fmt.Errorf("failed to get repository: %v", err) + } + + return repository.GetDefaultBranch(), nil +} + +// Validate validates the input parameters. +func Validate(token string, owner string, repo string, commitSHA string, pr string) error { + if token == "" { + return fmt.Errorf("token is empty") + } + if owner == "" { + return fmt.Errorf("owner is empty") + } + if repo == "" { + return fmt.Errorf("repo is empty") + } + if commitSHA == "" { + return fmt.Errorf("commitSHA is empty") + } + if pr == "" { + return fmt.Errorf("pr is empty") + } + return nil +} + +// createOrUpdateComment creates a new comment on the pull request or updates an existing one. +func createOrUpdateComment(client *github.Client, owner, githubUser, repo string, prNum int, commentBody string) error { + comments, _, err := client.Issues.ListComments(context.Background(), owner, repo, prNum, &github.IssueListCommentsOptions{}) + if err != nil { + return fmt.Errorf("failed to get comments: %v", err) + } + // Check if the user has already left a comment on the pull request. + var existingComment *github.IssueComment + for _, comment := range comments { + if comment.GetUser().GetLogin() == githubUser { + existingComment = comment + break + } + } + + // If the user has already left a comment, update it. + if existingComment != nil { + existingComment.Body = &commentBody + _, _, err = client.Issues.EditComment(context.Background(), owner, repo, *existingComment.ID, existingComment) + if err != nil { + return fmt.Errorf("failed to update comment: %v", err) + } + log.Println("Comment updated successfully!") + } else { + // Otherwise, create a new comment. + newComment := &github.IssueComment{ + Body: &commentBody, + } + _, _, err = client.Issues.CreateComment(context.Background(), owner, repo, prNum, newComment) + if err != nil { + return fmt.Errorf("failed to create comment: %v", err) + } + log.Println("Comment created successfully!") + } + return nil +} + +// GitHubIssueComment returns a markdown string for a GitHub issue comment. +func GitHubIssueComment(checks ScorecardResult) string { + if checks.Repo.Name == "" { + return "" + } + sb := strings.Builder{} + sb.WriteString(fmt.Sprintf("
%s - %s\n
", checks.Repo.Name, checks.Date)) + sb.WriteString(fmt.Sprintf( + "https://api.securityscorecards.dev/projects/%s

", + checks.Repo.Name, + checks.Repo.Name)) + sb.WriteString("\n") + sb.WriteString("\n") + for _, check := range checks.Checks { + sb.WriteString(fmt.Sprintf("\n", check.Name, check.Score)) + } + sb.WriteString("
CheckScore
%s%d
\n") + if len(checks.Vulnerabilities) > 0 { + sb.WriteString("\n") + sb.WriteString("\n") + for _, vulns := range checks.Vulnerabilities { + sb.WriteString(fmt.Sprintf("\n", vulns.AdvisoryUrl, vulns.Severity, vulns.AdvisorySummary)) + } + sb.WriteString("
VulnerabilitySeveritySummary
%s%s%s
\n") + } + + sb.WriteString("
") + return sb.String() +} + +// GetDependencyDiff returns the dependency diff between two commits. It returns an error if the dependency graph is not enabled. +func GetDependencyDiff(owner, repo, token, base, head string) ([]DependencyDiff, error) { + if owner == "" { + return nil, fmt.Errorf("owner is required") + } + if repo == "" { + return nil, fmt.Errorf("repo is required") + } + if token == "" { + return nil, fmt.Errorf("token is required") + } + resp, err := GetGitHubDependencyDiff(owner, repo, token, base, head) + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + // if the dependency graph is not enabled, we can't get the dependency diff + return nil, fmt.Errorf("failed to get dependency diff, please enable dependency graph https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/configuring-the-dependency-graph : %v", resp.Status) + } + if err != nil { + return nil, fmt.Errorf("failed to get dependency diff: %w", err) + } + + var data []DependencyDiff + err = json.NewDecoder(resp.Body).Decode(&data) + if err != nil { + return nil, err + } + // filter out the dependencies that are not added + var filteredData []DependencyDiff + for _, dep := range data { + // also if the source repo doesn't start with GitHub.com, we can ignore it + if dep.ChangeType == "added" && dep.SourceRepositoryURL != "" && strings.HasPrefix(dep.SourceRepositoryURL, "https://github.com") { + filteredData = append(filteredData, dep) + } + } + return filteredData, nil +} + +// GetGitHubDependencyDiff returns the dependency diff between two commits. It returns an error if the dependency graph is not enabled. +func GetGitHubDependencyDiff(owner string, repo string, token string, base string, head string) (*http.Response, error) { + req, err := http.NewRequest("GET", + fmt.Sprintf("https://api.github.com/repos/%s/%s/dependency-graph/compare/%s...%s", owner, repo, base, head), nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + // handle err + } + req.Header.Set("Accept", "application/vnd.github+json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to get response: %w", err) + } + return resp, nil +} + +// filter returns a new slice containing all elements of slice that satisfy the predicate f. +func filter[T any](slice []T, f func(T) bool) []T { + var n []T + for _, e := range slice { + if f(e) { + n = append(n, e) + } + } + return n +} + +// GetScorecardChecks returns the list of checks to run. +// This uses the SCORECARD_CHECKS environment variable to get the path to the checks list. +func GetScorecardChecks() ([]string, error) { + fileName := os.Getenv("SCORECARD_CHECKS") + if fileName == "" { + // default to critical and high severity checks + return []string{"Dangerous-Workflow", "Binary-Artifacts", "Branch-Protection", "Code-Review", "Dependency-Update-Tool"}, nil + } + f, err := os.Open(fileName) + if err != nil { + return nil, err + } + defer f.Close() + decoder := json.NewDecoder(f) + var checksFromFile []string + err = decoder.Decode(&checksFromFile) + if err != nil { + return nil, err + } + return checksFromFile, nil +} + +// GetScore returns the scorecard result for a given repository. +func GetScore(repo string) (ScorecardResult, error) { + req, err := http.NewRequest("GET", fmt.Sprintf("https://api.securityscorecards.dev/projects/%s", repo), nil) + if err != nil { + return ScorecardResult{}, fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Accept", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return ScorecardResult{}, fmt.Errorf("failed to get response: %w", err) + } + defer resp.Body.Close() + result, err := ioutil.ReadAll(resp.Body) + if err != nil { + return ScorecardResult{}, fmt.Errorf("failed to read response: %w", err) + } + var scorecard ScorecardResult + err = json.Unmarshal(result, &scorecard) + if err != nil { + return ScorecardResult{}, fmt.Errorf("failed to unmarshal response: %w", err) + } + return scorecard, nil +} diff --git a/dependency-analysis/main_test.go b/dependency-analysis/main_test.go new file mode 100644 index 00000000..4fe885e8 --- /dev/null +++ b/dependency-analysis/main_test.go @@ -0,0 +1,215 @@ +package main + +import ( + "os" + "path" + "reflect" + "testing" +) + +func Test_filter(t *testing.T) { + type args[T any] struct { + slice []T + f func(T) bool + } + type testCase[T any] struct { + name string + args args[T] + want []T + } + tests := []testCase[string]{ + { + name: "default true", + args: args[string]{ + slice: []string{"a"}, + f: func(s string) bool { return s == "a" }, + }, + want: []string{"a"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := filter(tt.args.slice, tt.args.f); !reflect.DeepEqual(got, tt.want) { + t.Errorf("filter() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetScorecardChecks(t *testing.T) { + tests := []struct { + name string + want []string + fileContent string + wantErr bool + }{ + { + name: "default", + want: []string{"Dangerous-Workflow", "Binary-Artifacts", "Branch-Protection", "Code-Review", "Dependency-Update-Tool"}, + wantErr: false, + }, + { + name: "file with data", + want: []string{"Binary-Artifacts", "Pinned-Dependencies"}, + fileContent: `[ + "Binary-Artifacts", + "Pinned-Dependencies" +]`, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.fileContent != "" { + dir, err := os.MkdirTemp("", "scorecard-checks") + if err != nil { + t.Errorf("GetScorecardChecks() error = %v, wantErr %v", err, tt.wantErr) + return + } + defer os.RemoveAll(dir) + + if err := os.WriteFile(path.Join(dir, "scorecard.txt"), []byte(tt.fileContent), 0644); err != nil { + t.Errorf("GetScorecardChecks() error = %v, wantErr %v", err, tt.wantErr) + return + } + t.Setenv("SCORECARD_CHECKS", path.Join(dir, "scorecard.txt")) + } + got, err := GetScorecardChecks() + if (err != nil) != tt.wantErr { + t.Errorf("GetScorecardChecks() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetScorecardChecks() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetScore(t *testing.T) { + type args struct { + repo string + } + tests := []struct { + name string + args args + score float64 + wantErr bool + }{ + { + name: "default", + args: args{ + repo: "github.com/ossf/scorecard", + }, + score: 5.0, + wantErr: false, + }, + { + name: "invalid repo", + args: args{ + repo: "github.com/ossf/invalid", + }, + score: 0.0, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetScore(tt.args.repo) + if (err != nil) != tt.wantErr { + t.Errorf("GetScore() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got.Score < tt.score { + t.Errorf("GetScore() got = %v, want %v", got, tt.score) + } + }) + } +} + +func TestValidate(t *testing.T) { + type args struct { + token string + owner string + repo string + commitSHA string + pr string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "default", + args: args{ + token: "token", + owner: "ossf", + repo: "scorecard", + commitSHA: "commitSHA", + pr: "1", + }, + wantErr: false, + }, + { + name: "invalid token", + args: args{ + owner: "ossf", + repo: "scorecard", + commitSHA: "commitSHA", + pr: "1", + }, + wantErr: true, + }, + { + name: "invalid owner", + args: args{ + repo: "scorecard", + token: "token", + commitSHA: "commitSHA", + pr: "1", + }, + wantErr: true, + }, + { + name: "invalid repo", + args: args{ + owner: "ossf", + token: "token", + commitSHA: "commitSHA", + pr: "1", + }, + wantErr: true, + }, + { + name: "invalid pr", + args: args{ + owner: "ossf", + repo: "scorecard", + token: "token", + commitSHA: "commitSHA", + }, + wantErr: true, + }, + { + name: "invalid commitSHA", + args: args{ + owner: "ossf", + repo: "scorecard", + token: "token", + pr: "1", + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := Validate(tt.args.token, tt.args.owner, tt.args.repo, tt.args.commitSHA, tt.args.pr); (err != nil) != tt.wantErr { + t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/dependency-analysis/types.go b/dependency-analysis/types.go new file mode 100644 index 00000000..2f191d88 --- /dev/null +++ b/dependency-analysis/types.go @@ -0,0 +1,44 @@ +package main + +type ScorecardResult struct { + Date string `json:"date"` + Repo struct { + Name string `json:"name"` + Commit string `json:"commit"` + } `json:"repo"` + Scorecard struct { + Version string `json:"version"` + Commit string `json:"commit"` + } `json:"scorecard"` + Score float64 `json:"score"` + Checks []Check `json:"checks"` + Vulnerabilities []Vulnerability +} +type Check struct { + Name string `json:"name"` + Score int `json:"score,omitempty"` + Reason string `json:"reason"` + Details []string `json:"details"` + Documentation struct { + Short string `json:"short"` + Url string `json:"url"` + } `json:"documentation"` +} +type DependencyDiff struct { + ChangeType string `json:"change_type"` + Manifest string `json:"manifest"` + Ecosystem string `json:"ecosystem"` + Name string `json:"name"` + Version string `json:"version"` + PackageURL string `json:"package_url"` + License string `json:"license"` + SourceRepositoryURL string `json:"source_repository_url"` + Vulnerabilities []Vulnerability `json:"vulnerabilities"` +} +type Vulnerability struct { + Severity string `json:"severity"` + AdvisoryGhsaId string `json:"advisory_ghsa_id"` + AdvisorySummary string `json:"advisory_summary"` + AdvisoryUrl string `json:"advisory_url"` +} + diff --git a/go.mod b/go.mod index 91096eec..da799253 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/sigstore/cosign v1.13.1 github.com/spf13/cobra v1.6.1 golang.org/x/net v0.7.0 + golang.org/x/oauth2 v0.3.0 ) require ( @@ -252,7 +253,6 @@ require ( golang.org/x/crypto v0.4.0 // indirect golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15 // indirect golang.org/x/mod v0.7.0 // indirect - golang.org/x/oauth2 v0.3.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.5.0 // indirect golang.org/x/term v0.5.0 // indirect