From 34621504fbfca26b447f80ba4cd19fe411d09a4f Mon Sep 17 00:00:00 2001 From: Oliver Chang Date: Tue, 29 Jun 2021 13:09:40 +1000 Subject: [PATCH] :sparkles: Add a Vulnerabilities check. (#628) Uses OSV to check this. Fixes #52. --- README.md | 1 + checks/checks.md | 7 +++ checks/checks.yaml | 8 +++ checks/vulnerabilities.go | 108 ++++++++++++++++++++++++++++++++++++ e2e/vulnerabilities_test.go | 64 +++++++++++++++++++++ 5 files changed, 188 insertions(+) create mode 100644 checks/vulnerabilities.go create mode 100644 e2e/vulnerabilities_test.go diff --git a/README.md b/README.md index d7fd5015..b8ea5ef8 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ SAST | Does the project use static code analysis tools, e.g. [Code Active | Did the project get any commits in the last 90 days? Branch-Protection | Does the project use [Branch Protection](https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/about-protected-branches) ? Packaging | Does the project build and publish official packages from CI/CD, e.g. [GitHub Publishing](https://docs.github.com/en/free-pro-team@latest/actions/guides/about-packaging-with-github-actions#workflows-for-publishing-packages) ? +Vulnerabilities | Does the project have unfixed vulnerabilities? Uses the [OSV service](https://osv.dev). To see detailed information about each check and remediation steps, check out the [checks documentation page](checks/checks.md). diff --git a/checks/checks.md b/checks/checks.md index e9920b17..95e4f13f 100644 --- a/checks/checks.md +++ b/checks/checks.md @@ -140,3 +140,10 @@ This check tries to determine if a project's GitHub workflows follow the princip **Remediation steps** - Set permissions as `read-all` or `contents: read` as described in GitHub's [documentation](https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#permissions). +## Vulnerabilities + +This check determines whether if there are open, unfixed vulnerabilities in the project using the [OSV](https://osv.dev) service. + +**Remediation steps** +- Fix the vulnerabilities. The details of each vulnerability can be found on . + diff --git a/checks/checks.yaml b/checks/checks.yaml index 26349b47..0176bed8 100644 --- a/checks/checks.yaml +++ b/checks/checks.yaml @@ -251,3 +251,11 @@ checks: - >- For GitHub, check out the steps [here](https://docs.github.com/en/github/administering-a-repository/managing-a-branch-protection-rule). + Vulnerabilities: + description: >- + This check determines whether if there are open, unfixed vulnerabilities + in the project using the [OSV](https://osv.dev) service. + remediation: + - >- + Fix the vulnerabilities. The details of each vulnerability can be found + on . diff --git a/checks/vulnerabilities.go b/checks/vulnerabilities.go new file mode 100644 index 00000000..a9453992 --- /dev/null +++ b/checks/vulnerabilities.go @@ -0,0 +1,108 @@ +// Copyright 2021 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. + +package checks + +import ( + "bytes" + "encoding/json" + "errors" + "net/http" + "strings" + + "github.com/google/go-github/v32/github" + + "github.com/ossf/scorecard/checker" +) + +const ( + // CheckVulnerabilities is the registered name for the OSV check. + CheckVulnerabilities = "Vulnerabilities" + osvQueryEndpoint = "https://api.osv.dev/v1/query" +) + +// ErrNoCommits is the error for when there are no commits found. +var ErrNoCommits = errors.New("no commits found") + +type osvQuery struct { + Commit string `json:"commit"` +} + +type osvResponse struct { + Vulns []struct { + ID string `json:"id"` + } `json:"vulns"` +} + +//nolint:gochecknoinits +func init() { + registerCheck(CheckVulnerabilities, HasUnfixedVulnerabilities) +} + +func (resp *osvResponse) getVulnerabilities() []string { + ids := make([]string, 0, len(resp.Vulns)) + for _, vuln := range resp.Vulns { + ids = append(ids, vuln.ID) + } + return ids +} + +func HasUnfixedVulnerabilities(c *checker.CheckRequest) checker.CheckResult { + commits, _, err := c.Client.Repositories.ListCommits(c.Ctx, c.Owner, c.Repo, &github.CommitsListOptions{ + ListOptions: github.ListOptions{ + PerPage: 1, + }, + }) + if err != nil { + return checker.MakeRetryResult(CheckVulnerabilities, err) + } + + if len(commits) != 1 || commits[0].SHA == nil { + return checker.MakeInconclusiveResult(CheckVulnerabilities, ErrNoCommits) + } + + query, err := json.Marshal(&osvQuery{ + Commit: *commits[0].SHA, + }) + if err != nil { + panic("!! failed to marshal OSV query.") + } + + req, err := http.NewRequestWithContext(c.Ctx, http.MethodPost, osvQueryEndpoint, bytes.NewReader(query)) + if err != nil { + return checker.MakeRetryResult(CheckVulnerabilities, err) + } + + // Use our own http client as the one from CheckRequest adds GitHub tokens to the headers. + httpClient := &http.Client{} + resp, err := httpClient.Do(req) + if err != nil { + return checker.MakeRetryResult(CheckVulnerabilities, err) + } + defer resp.Body.Close() + + var osvResp osvResponse + decoder := json.NewDecoder(resp.Body) + if err := decoder.Decode(&osvResp); err != nil { + return checker.MakeRetryResult(CheckVulnerabilities, err) + } + + vulnIDs := osvResp.getVulnerabilities() + if len(vulnIDs) > 0 { + c.Logf("HEAD is vulnerable to %s", strings.Join(vulnIDs, ", ")) + return checker.MakeFailResult(CheckVulnerabilities, nil) + } + + return checker.MakePassResult(CheckVulnerabilities) +} diff --git a/e2e/vulnerabilities_test.go b/e2e/vulnerabilities_test.go new file mode 100644 index 00000000..27c0c6a1 --- /dev/null +++ b/e2e/vulnerabilities_test.go @@ -0,0 +1,64 @@ +// Copyright 2021 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. + +//nolint:dupl // repeating test cases that are slightly different is acceptable +package e2e + +import ( + "context" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/ossf/scorecard/checker" + "github.com/ossf/scorecard/checks" +) + +var _ = Describe("E2E TEST:Vulnerabilities", func() { + Context("E2E TEST:Validating vulnerabilities status", func() { + It("Should return that there are no vulnerabilities", func() { + l := log{} + checkRequest := checker.CheckRequest{ + Ctx: context.Background(), + Client: ghClient, + HTTPClient: httpClient, + RepoClient: nil, + Owner: "ossf", + Repo: "scorecard", + GraphClient: graphClient, + Logf: l.Logf, + } + result := checks.HasUnfixedVulnerabilities(&checkRequest) + Expect(result.Error).Should(BeNil()) + Expect(result.Pass).Should(BeTrue()) + }) + + It("Should return that there are vulnerabilities", func() { + l := log{} + checkRequest := checker.CheckRequest{ + Ctx: context.Background(), + Client: ghClient, + HTTPClient: httpClient, + RepoClient: nil, + Owner: "oliverchang", + Repo: "open62541", + GraphClient: graphClient, + Logf: l.Logf, + } + result := checks.HasUnfixedVulnerabilities(&checkRequest) + Expect(result.Error).Should(BeNil()) + Expect(result.Pass).Should(BeFalse()) + }) + }) +})