From 80bc6f69e0135ec52a2bddc728e8bc2a46bbfe6d 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 +++
7 files changed, 705 insertions(+)
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("Severity | \n")
+ sb.WriteString("AdvisoryGhsaId | \n")
+ sb.WriteString("AdvisorySummary | \n")
+ sb.WriteString("AdvisoryUrl | \n")
+ sb.WriteString("
\n")
+ for _, v := range i.Vulnerabilities {
+ sb.WriteString("\n")
+ sb.WriteString(fmt.Sprintf("%s | \n", v.Severity))
+ sb.WriteString(fmt.Sprintf("%s | \n", v.AdvisoryGhsaId))
+ sb.WriteString(fmt.Sprintf("%s | \n", v.AdvisorySummary))
+ sb.WriteString(fmt.Sprintf("%s | \n", v.AdvisoryUrl))
+ }
+ sb.WriteString("
\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("Check | Score |
\n")
+ for _, check := range checks.Checks {
+ sb.WriteString(fmt.Sprintf("%s | %d |
\n", check.Name, check.Score))
+ }
+ sb.WriteString("
\n")
+ if len(checks.Vulnerabilities) > 0 {
+ sb.WriteString("\n")
+ sb.WriteString("Vulnerability | Severity | Summary |
\n")
+ for _, vulns := range checks.Vulnerabilities {
+ sb.WriteString(fmt.Sprintf("%s | %s | %s |
\n", vulns.AdvisoryUrl, vulns.Severity, vulns.AdvisorySummary))
+ }
+ sb.WriteString("
\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"`
+}
+