diff --git a/checker/raw_result.go b/checker/raw_result.go index 2fd8836a..c27b6a18 100644 --- a/checker/raw_result.go +++ b/checker/raw_result.go @@ -18,6 +18,7 @@ import "time" // RawResults contains results before a policy // is applied. +//nolint type RawResults struct { VulnerabilitiesResults VulnerabilitiesData BinaryArtifactResults BinaryArtifactData @@ -27,6 +28,7 @@ type RawResults struct { CodeReviewResults CodeReviewData WebhookResults WebhooksData MaintainedResults MaintainedData + SignedReleasesResults SignedReleasesData } // MaintainedData contains the raw results @@ -63,6 +65,12 @@ type BinaryArtifactData struct { Files []File } +// SignedReleasesData contains the raw results +// for the Signed-Releases check. +type SignedReleasesData struct { + Releases []Release +} + // DependencyUpdateToolData contains the raw results // for the Dependency-Update-Tool check. type DependencyUpdateToolData struct { @@ -227,3 +235,17 @@ type Vulnerability struct { ID string // TODO(vuln): Add additional fields, if needed. } + +// Release represents a project release. +type Release struct { + Tag string + URL string + Assets []ReleaseAsset + // TODO: add needed fields, e.g. Path. +} + +// ReleaseAsset represents a release asset. +type ReleaseAsset struct { + Name string + URL string +} diff --git a/checks/evaluation/signed_releases.go b/checks/evaluation/signed_releases.go new file mode 100644 index 00000000..e1f6847b --- /dev/null +++ b/checks/evaluation/signed_releases.go @@ -0,0 +1,91 @@ +// 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 evaluation + +import ( + "fmt" + "strings" + + "github.com/ossf/scorecard/v4/checker" + sce "github.com/ossf/scorecard/v4/errors" +) + +var artifactExtensions = []string{".asc", ".minisig", ".sig", ".sign"} + +const releaseLookBack = 5 + +// SignedReleases applies the score policy for the Signed-Releases check. +func SignedReleases(name string, dl checker.DetailLogger, r *checker.SignedReleasesData) checker.CheckResult { + if r == nil { + e := sce.WithMessage(sce.ErrScorecardInternal, "empty raw data") + return checker.CreateRuntimeErrorResult(name, e) + } + + totalReleases := 0 + totalSigned := 0 + + for _, release := range r.Releases { + if len(release.Assets) == 0 { + continue + } + + dl.Debug(&checker.LogMessage{ + Text: fmt.Sprintf("GitHub release found: %s", release.Tag), + }) + + totalReleases++ + signed := false + + for _, asset := range release.Assets { + for _, suffix := range artifactExtensions { + if strings.HasSuffix(asset.Name, suffix) { + dl.Info(&checker.LogMessage{ + Path: asset.URL, + Type: checker.FileTypeURL, + Text: fmt.Sprintf("signed release artifact: %s", asset.Name), + }) + signed = true + break + } + } + if signed { + totalSigned++ + break + } + } + + if !signed { + dl.Warn(&checker.LogMessage{ + Path: release.URL, + Type: checker.FileTypeURL, + Text: fmt.Sprintf("release artifact %s not signed", release.Tag), + }) + } + if totalReleases >= releaseLookBack { + break + } + } + + if totalReleases == 0 { + dl.Warn(&checker.LogMessage{ + Text: "no GitHub releases found", + }) + // Generic summary. + return checker.CreateInconclusiveResult(name, "no releases found") + } + + reason := fmt.Sprintf("%d out of %d artifacts are signed", totalSigned, totalReleases) + return checker.CreateProportionalScoreResult(name, reason, totalSigned, totalReleases) +} diff --git a/checks/raw/signed_releases.go b/checks/raw/signed_releases.go new file mode 100644 index 00000000..62e669b7 --- /dev/null +++ b/checks/raw/signed_releases.go @@ -0,0 +1,49 @@ +// Copyright 2020 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 raw + +import ( + "fmt" + + "github.com/ossf/scorecard/v4/checker" +) + +// SignedReleases checks for presence of signed release check. +func SignedReleases(c *checker.CheckRequest) (checker.SignedReleasesData, error) { + releases, err := c.RepoClient.ListReleases() + if err != nil { + return checker.SignedReleasesData{}, fmt.Errorf("%w", err) + } + + var results checker.SignedReleasesData + for i, r := range releases { + results.Releases = append(results.Releases, + checker.Release{ + Tag: r.TagName, + URL: r.URL, + }) + + for _, asset := range r.Assets { + a := checker.ReleaseAsset{ + URL: asset.URL, + Name: asset.Name, + } + results.Releases[i].Assets = append(results.Releases[i].Assets, a) + } + } + + // Return raw results. + return results, nil +} diff --git a/checks/raw/vulnerabilities.go b/checks/raw/vulnerabilities.go index 61f90b04..80d148f3 100644 --- a/checks/raw/vulnerabilities.go +++ b/checks/raw/vulnerabilities.go @@ -15,28 +15,29 @@ package raw import ( + "errors" + "fmt" + "github.com/ossf/scorecard/v4/checker" "github.com/ossf/scorecard/v4/clients" - sce "github.com/ossf/scorecard/v4/errors" ) +var errNoCommitFound = errors.New("no commit found") + // Vulnerabilities retrieves the raw data for the Vulnerabilities check. func Vulnerabilities(c *checker.CheckRequest) (checker.VulnerabilitiesData, error) { commits, err := c.RepoClient.ListCommits() if err != nil { - return checker.VulnerabilitiesData{}, - sce.WithMessage(sce.ErrScorecardInternal, "Client.Repositories.ListCommits") + return checker.VulnerabilitiesData{}, fmt.Errorf("repoClient.ListCommits: %w", err) } if len(commits) < 1 || commits[0].SHA == "" { - return checker.VulnerabilitiesData{}, - sce.WithMessage(sce.ErrScorecardInternal, "no commits found") + return checker.VulnerabilitiesData{}, fmt.Errorf("%w", errNoCommitFound) } resp, err := c.VulnerabilitiesClient.HasUnfixedVulnerabilities(c.Ctx, commits[0].SHA) if err != nil { - return checker.VulnerabilitiesData{}, - sce.WithMessage(sce.ErrScorecardInternal, "VulnerabilitiesClient.HasUnfixedVulnerabilities") + return checker.VulnerabilitiesData{}, fmt.Errorf("vulnerabilitiesClient.HasUnfixedVulnerabilities: %w", err) } vulnIDs := getVulnerabilities(&resp) diff --git a/checks/signed_releases.go b/checks/signed_releases.go index 1f759673..b8856922 100644 --- a/checks/signed_releases.go +++ b/checks/signed_releases.go @@ -15,20 +15,14 @@ package checks import ( - "fmt" - "strings" - "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/checks/evaluation" + "github.com/ossf/scorecard/v4/checks/raw" sce "github.com/ossf/scorecard/v4/errors" ) -const ( - // CheckSignedReleases is the registered name for SignedReleases. - CheckSignedReleases = "Signed-Releases" - releaseLookBack = 5 -) - -var artifactExtensions = []string{".asc", ".minisig", ".sig", ".sign"} +// CheckSignedReleases is the registered name for SignedReleases. +const CheckSignedReleases = "Signed-Releases" //nolint:gochecknoinits func init() { @@ -40,60 +34,17 @@ func init() { // SignedReleases runs Signed-Releases check. func SignedReleases(c *checker.CheckRequest) checker.CheckResult { - releases, err := c.RepoClient.ListReleases() + rawData, err := raw.SignedReleases(c) if err != nil { - e := sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("Client.Repositories.ListReleases: %v", err)) + e := sce.WithMessage(sce.ErrScorecardInternal, err.Error()) return checker.CreateRuntimeErrorResult(CheckSignedReleases, e) } - totalReleases := 0 - totalSigned := 0 - for _, r := range releases { - if len(r.Assets) == 0 { - continue - } - c.Dlogger.Debug(&checker.LogMessage{ - Text: fmt.Sprintf("GitHub release found: %s", r.TagName), - }) - totalReleases++ - signed := false - for _, asset := range r.Assets { - for _, suffix := range artifactExtensions { - if strings.HasSuffix(asset.Name, suffix) { - c.Dlogger.Info(&checker.LogMessage{ - Path: asset.URL, - Type: checker.FileTypeURL, - Text: fmt.Sprintf("signed release artifact: %s", asset.Name), - }) - signed = true - break - } - } - if signed { - totalSigned++ - break - } - } - if !signed { - c.Dlogger.Warn(&checker.LogMessage{ - Path: r.URL, - Type: checker.FileTypeURL, - Text: fmt.Sprintf("release artifact %s not signed", r.TagName), - }) - } - if totalReleases >= releaseLookBack { - break - } + // Return raw results. + if c.RawResults != nil { + c.RawResults.SignedReleasesResults = rawData } - if totalReleases == 0 { - c.Dlogger.Warn(&checker.LogMessage{ - Text: "no GitHub releases found", - }) - // Generic summary. - return checker.CreateInconclusiveResult(CheckSignedReleases, "no releases found") - } - - reason := fmt.Sprintf("%d out of %d artifacts are signed", totalSigned, totalReleases) - return checker.CreateProportionalScoreResult(CheckSignedReleases, reason, totalSigned, totalReleases) + // Return the score evaluation. + return evaluation.SignedReleases(CheckSignedReleases, c.Dlogger, &rawData) } diff --git a/pkg/json_raw_results.go b/pkg/json_raw_results.go index 100fcd75..c75eeab2 100644 --- a/pkg/json_raw_results.go +++ b/pkg/json_raw_results.go @@ -119,6 +119,19 @@ type jsonIssue struct { // TODO: add fields, e.g., state=[opened|closed] } +type jsonRelease struct { + Tag string `json:"tag"` + URL string `json:"url"` + Assets []jsonReleaseAsset `json:"assets"` + // TODO: add needed fields, e.g. Path. +} + +type jsonReleaseAsset struct { + Path string `json:"path"` + URL string `json:"url"` +} + +//nolint type jsonRawResults struct { // List of recent issues. RecentIssues []jsonIssue `json:"issues"` @@ -138,14 +151,33 @@ type jsonRawResults struct { DefaultBranchCommits []jsonDefaultBranchCommit `json:"default-branch-commits"` // Archived status of the repo. ArchivedStatus jsonArchivedStatus `json:"archived"` + // Releases. + Releases []jsonRelease `json:"releases"` +} + +//nolint:unparam +func (r *jsonScorecardRawResult) addSignedReleasesRawResults(sr *checker.SignedReleasesData) error { + r.Results.Releases = []jsonRelease{} + for i, release := range sr.Releases { + r.Results.Releases = append(r.Results.Releases, + jsonRelease{ + Tag: release.Tag, + URL: release.URL, + }) + for _, asset := range release.Assets { + r.Results.Releases[i].Assets = append(r.Results.Releases[i].Assets, + jsonReleaseAsset{ + Path: asset.Name, + URL: asset.URL, + }, + ) + } + } + return nil } func getRepoAssociation(author *checker.User) *string { - if author == nil { - return nil - } - - if author.RepoAssociation == nil { + if author == nil || author.RepoAssociation == nil { return nil } @@ -367,6 +399,11 @@ func (r *jsonScorecardRawResult) fillJSONRawResults(raw *checker.RawResults) err return sce.WithMessage(sce.ErrScorecardInternal, err.Error()) } + // Signed-Releases. + if err := r.addSignedReleasesRawResults(&raw.SignedReleasesResults); err != nil { + return sce.WithMessage(sce.ErrScorecardInternal, err.Error()) + } + return nil }