diff --git a/checker/check_result.go b/checker/check_result.go index a4a3d2c0..bfd90b3f 100644 --- a/checker/check_result.go +++ b/checker/check_result.go @@ -139,6 +139,38 @@ type SecurityPolicyData struct { Files []File } +// Run represents a run. +type Run struct { + URL string + // TODO: add fields, e.g., Result=["success", "failure"] +} + +// Issue represents an issue. +type Issue struct { + URL string + // TODO: add fields, e.g., state=[opened|closed] +} + +// MergeRequest represents a merge request. +type MergeRequest struct { + URL string + // TODO: add fields, e.g., State=["merged"|"closed"] +} + +// Tool represents a tool. +type Tool struct { + // Runs of the tool. + Runs []Run + // Issues created by the tool. + Issues []Issue + // Merges requests created by the tool. + MergeRequests []MergeRequest + Name string + URL string + Desc string + ConfigFiles []File +} + // BinaryArtifactData contains the raw results // for the Binary-Artifact check. type BinaryArtifactData struct { @@ -146,11 +178,20 @@ type BinaryArtifactData struct { Files []File } +// DependencyUpdateToolData contains the raw results +// for the Dependency-Update-Tool check. +type DependencyUpdateToolData struct { + // Tools contains a list of tools. + // Note: we only populate one entry at most. + Tools []Tool +} + // RawResults contains results before a policy // is applied. type RawResults struct { - BinaryArtifactResults BinaryArtifactData - SecurityPolicyResults SecurityPolicyData + BinaryArtifactResults BinaryArtifactData + SecurityPolicyResults SecurityPolicyData + DependencyUpdateToolResults DependencyUpdateToolData } // CreateProportionalScore creates a proportional score. diff --git a/checks/dependency_update_tool.go b/checks/dependency_update_tool.go index 4357d6d3..96e33292 100644 --- a/checks/dependency_update_tool.go +++ b/checks/dependency_update_tool.go @@ -15,10 +15,9 @@ package checks import ( - "strings" - "github.com/ossf/scorecard/v3/checker" - "github.com/ossf/scorecard/v3/checks/fileparser" + "github.com/ossf/scorecard/v3/checks/evaluation" + "github.com/ossf/scorecard/v3/checks/raw" sce "github.com/ossf/scorecard/v3/errors" ) @@ -27,60 +26,23 @@ const CheckDependencyUpdateTool = "Dependency-Update-Tool" //nolint func init() { - registerCheck(CheckDependencyUpdateTool, UsesDependencyUpdateTool) + registerCheck(CheckDependencyUpdateTool, DependencyUpdateTool) } -// UsesDependencyUpdateTool will check the repository uses a dependency update tool. -func UsesDependencyUpdateTool(c *checker.CheckRequest) checker.CheckResult { - var r bool - err := fileparser.CheckIfFileExists(c, fileExists, &r) +// DependencyUpdateTool checks if the repository uses a dependency update tool. +func DependencyUpdateTool(c *checker.CheckRequest) checker.CheckResult { + rawData, err := raw.DependencyUpdateTool(c.RepoClient) if err != nil { e := sce.WithMessage(sce.ErrScorecardInternal, err.Error()) return checker.CreateRuntimeErrorResult(CheckDependencyUpdateTool, e) } - if !r { - c.Dlogger.Warn3(&checker.LogMessage{ - Text: `dependabot config file not detected in source location. - We recommend setting this configuration in code so it can be easily verified by others.`, - }) - c.Dlogger.Warn3(&checker.LogMessage{ - Text: `renovatebot config file not detected in source location. - We recommend setting this configuration in code so it can be easily verified by others.`, - }) - return checker.CreateMinScoreResult(CheckDependencyUpdateTool, "no update tool detected") + + // Return raw results. + if c.RawResults != nil { + c.RawResults.DependencyUpdateToolResults = rawData + return checker.CheckResult{} } - // High score result. - return checker.CreateMaxScoreResult(CheckDependencyUpdateTool, "update tool detected") -} - -// fileExists will validate the if frozen dependencies file name exists. -func fileExists(name string, dl checker.DetailLogger, data fileparser.FileCbData) (bool, error) { - pdata := fileparser.FileGetCbDataAsBoolPointer(data) - - switch strings.ToLower(name) { - case ".github/dependabot.yml": - dl.Info3(&checker.LogMessage{ - Path: name, - Type: checker.FileTypeSource, - Offset: checker.OffsetDefault, - Text: "dependabot detected", - }) - // https://docs.renovatebot.com/configuration-options/ - case ".github/renovate.json", ".github/renovate.json5", ".renovaterc.json", "renovate.json", - "renovate.json5", ".renovaterc": - dl.Info3(&checker.LogMessage{ - Path: name, - Type: checker.FileTypeSource, - Offset: checker.OffsetDefault, - Text: "renovate detected", - }) - default: - // Continue iterating. - return true, nil - } - - *pdata = true - // We found the file, no need to continue iterating. - return false, nil + // Return the score evaluation. + return evaluation.DependencyUpdateTool(CheckDependencyUpdateTool, c.Dlogger, &rawData) } diff --git a/checks/evaluation/binary_artifacts.go b/checks/evaluation/binary_artifacts.go index 5a3f6648..4630771a 100644 --- a/checks/evaluation/binary_artifacts.go +++ b/checks/evaluation/binary_artifacts.go @@ -19,7 +19,7 @@ import ( sce "github.com/ossf/scorecard/v3/errors" ) -// BinaryArtifacts applies the score policy for the Binary-Artiacts check. +// BinaryArtifacts applies the score policy for the Binary-Artifacts check. func BinaryArtifacts(name string, dl checker.DetailLogger, r *checker.BinaryArtifactData) checker.CheckResult { if r == nil { diff --git a/checks/evaluation/dependency_update_tool.go b/checks/evaluation/dependency_update_tool.go new file mode 100644 index 00000000..23dfd01e --- /dev/null +++ b/checks/evaluation/dependency_update_tool.go @@ -0,0 +1,68 @@ +// 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 evaluation + +import ( + "fmt" + + "github.com/ossf/scorecard/v3/checker" + sce "github.com/ossf/scorecard/v3/errors" +) + +// DependencyUpdateTool applies the score policy for the Dependency-Update-Tool check. +func DependencyUpdateTool(name string, dl checker.DetailLogger, + r *checker.DependencyUpdateToolData) checker.CheckResult { + if r == nil { + e := sce.WithMessage(sce.ErrScorecardInternal, "empty raw data") + return checker.CreateRuntimeErrorResult(name, e) + } + + // Apply the policy evaluation. + if r.Tools == nil || len(r.Tools) == 0 { + dl.Warn3(&checker.LogMessage{ + Text: `dependabot config file not detected in source location. + We recommend setting this configuration in code so it can be easily verified by others.`, + }) + dl.Warn3(&checker.LogMessage{ + Text: `renovatebot config file not detected in source location. + We recommend setting this configuration in code so it can be easily verified by others.`, + }) + return checker.CreateMinScoreResult(name, "no update tool detected") + } + + // Validate the input. + if len(r.Tools) != 1 { + e := sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("found %d tools, expected 1", len(r.Tools))) + return checker.CreateRuntimeErrorResult(name, e) + } + + if len(r.Tools[0].ConfigFiles) != 1 { + e := sce.WithMessage(sce.ErrScorecardInternal, + fmt.Sprintf("found %d config files, expected 1", len(r.Tools[0].ConfigFiles))) + return checker.CreateRuntimeErrorResult(name, e) + } + + // Note: only one file per tool is present, + // so we do not iterate thru all entries. + dl.Info3(&checker.LogMessage{ + Path: r.Tools[0].ConfigFiles[0].Path, + Type: r.Tools[0].ConfigFiles[0].Type, + Offset: r.Tools[0].ConfigFiles[0].Offset, + Text: fmt.Sprintf("%s detected", r.Tools[0].Name), + }) + + // High score result. + return checker.CreateMaxScoreResult(name, "update tool detected") +} diff --git a/checks/fileparser/listing.go b/checks/fileparser/listing.go index 61119f84..c532871d 100644 --- a/checks/fileparser/listing.go +++ b/checks/fileparser/listing.go @@ -168,6 +168,34 @@ func CheckFilesContentV6(shellPathFnPattern string, return nil } +// FileCbV6 is the callback. +// The bool returned indicates whether the FileCbData +// should continue iterating over files or not. +type FileCbV6 func(path string, data FileCbData) (bool, error) + +// CheckIfFileExistsV6 downloads the tar of the repository and calls the onFile() to check +// for the occurrence. +func CheckIfFileExistsV6(repoClient clients.RepoClient, + onFile FileCbV6, data FileCbData) error { + matchedFiles, err := repoClient.ListFiles(func(string) (bool, error) { return true, nil }) + if err != nil { + // nolint: wrapcheck + return err + } + for _, filename := range matchedFiles { + continueIter, err := onFile(filename, data) + if err != nil { + return err + } + + if !continueIter { + break + } + } + + return nil +} + // FileCb represents a callback fn. type FileCb func(path string, dl checker.DetailLogger, data FileCbData) (bool, error) diff --git a/checks/raw/dependency_update_tool.go b/checks/raw/dependency_update_tool.go new file mode 100644 index 00000000..bf761f63 --- /dev/null +++ b/checks/raw/dependency_update_tool.go @@ -0,0 +1,82 @@ +// 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" + "strings" + + "github.com/ossf/scorecard/v3/checker" + "github.com/ossf/scorecard/v3/checks/fileparser" + "github.com/ossf/scorecard/v3/clients" +) + +// DependencyUpdateTool is the exported name for Depdendency-Update-Tool. +func DependencyUpdateTool(c clients.RepoClient) (checker.DependencyUpdateToolData, error) { + var tools []checker.Tool + err := fileparser.CheckIfFileExistsV6(c, checkDependencyFileExists, &tools) + if err != nil { + return checker.DependencyUpdateToolData{}, fmt.Errorf("%w", err) + } + + // No error, return the files. + return checker.DependencyUpdateToolData{Tools: tools}, nil +} + +func checkDependencyFileExists(name string, data fileparser.FileCbData) (bool, error) { + ptools, ok := data.(*[]checker.Tool) + if !ok { + // This never happens. + panic("invalid type") + } + + switch strings.ToLower(name) { + case ".github/dependabot.yml": + *ptools = append(*ptools, checker.Tool{ + Name: "Dependabot", + URL: "https://github.com/dependabot", + Desc: "Automated dependency updates built into GitHub", + ConfigFiles: []checker.File{ + { + Path: name, + Type: checker.FileTypeSource, + Offset: checker.OffsetDefault, + }, + }, + }) + + // https://docs.renovatebot.com/configuration-options/ + case ".github/renovate.json", ".github/renovate.json5", ".renovaterc.json", "renovate.json", + "renovate.json5", ".renovaterc": + *ptools = append(*ptools, checker.Tool{ + Name: "Renovabot", + URL: "https://github.com/renovatebot/renovate", + Desc: "Automated dependency updates. Multi-platform and multi-language.", + ConfigFiles: []checker.File{ + { + Path: name, + Type: checker.FileTypeSource, + Offset: checker.OffsetDefault, + }, + }, + }) + default: + // Continue iterating. + return true, nil + } + + // We found a file, no need to continue iterating. + return false, nil +} diff --git a/e2e/dependency_update_tool_test.go b/e2e/dependency_update_tool_test.go index c1d05340..6f9582d9 100644 --- a/e2e/dependency_update_tool_test.go +++ b/e2e/dependency_update_tool_test.go @@ -52,7 +52,7 @@ var _ = Describe("E2E TEST:"+checks.CheckDependencyUpdateTool, func() { NumberOfDebug: 0, } - result := checks.UsesDependencyUpdateTool(&req) + result := checks.DependencyUpdateTool(&req) // UPGRADEv2: to remove. // Old version. Expect(result.Error).Should(BeNil()) @@ -82,7 +82,7 @@ var _ = Describe("E2E TEST:"+checks.CheckDependencyUpdateTool, func() { NumberOfInfo: 1, NumberOfDebug: 0, } - result := checks.UsesDependencyUpdateTool(&req) + result := checks.DependencyUpdateTool(&req) // UPGRADEv2: to remove. // Old version. Expect(result.Error).Should(BeNil()) diff --git a/pkg/json_raw_results.go b/pkg/json_raw_results.go index 45e16d66..e778a928 100644 --- a/pkg/json_raw_results.go +++ b/pkg/json_raw_results.go @@ -33,24 +33,35 @@ type jsonScorecardRawResult struct { } // TODO: separate each check extraction into its own file. -type jsonFiles struct { +type jsonFile struct { Path string `json:"path"` Offset int `json:"offset,omitempty"` } +type jsonTool struct { + Name string `json:"name"` + URL string `json:"url"` + Desc string `json:"desc"` + ConfigFiles []jsonFile `json:"files"` + // TODO: Runs, Issues, Merge requests. +} + type jsonRawResults struct { // List of binaries found in the repo. - Binaries []jsonFiles `json:"binaries"` + Binaries []jsonFile `json:"binaries"` // List of security policy files found in the repo. // Note: we return one at most. - SecurityPolicies []jsonFiles `json:"security-policies"` + SecurityPolicies []jsonFile `json:"security-policies"` + // List of update tools. + // Note: we return one at most. + DependencyUpdateTools []jsonTool `json:"dependency-update-tools"` } //nolint:unparam func (r *jsonScorecardRawResult) addBinaryArtifactRawResults(ba *checker.BinaryArtifactData) error { - r.Results.Binaries = []jsonFiles{} + r.Results.Binaries = []jsonFile{} for _, v := range ba.Files { - r.Results.Binaries = append(r.Results.Binaries, jsonFiles{ + r.Results.Binaries = append(r.Results.Binaries, jsonFile{ Path: v.Path, }) } @@ -58,16 +69,38 @@ func (r *jsonScorecardRawResult) addBinaryArtifactRawResults(ba *checker.BinaryA } //nolint:unparam -func (r *jsonScorecardRawResult) addSecurityPolicyRawResults(ba *checker.SecurityPolicyData) error { - r.Results.SecurityPolicies = []jsonFiles{} - for _, v := range ba.Files { - r.Results.SecurityPolicies = append(r.Results.SecurityPolicies, jsonFiles{ +func (r *jsonScorecardRawResult) addSecurityPolicyRawResults(sp *checker.SecurityPolicyData) error { + r.Results.SecurityPolicies = []jsonFile{} + for _, v := range sp.Files { + r.Results.SecurityPolicies = append(r.Results.SecurityPolicies, jsonFile{ Path: v.Path, }) } return nil } +//nolint:unparam +func (r *jsonScorecardRawResult) addDependencyUpdateToolRawResults(dut *checker.DependencyUpdateToolData) error { + r.Results.DependencyUpdateTools = []jsonTool{} + for i := range dut.Tools { + t := dut.Tools[i] + offset := len(r.Results.DependencyUpdateTools) + r.Results.DependencyUpdateTools = append(r.Results.DependencyUpdateTools, jsonTool{ + Name: t.Name, + URL: t.URL, + Desc: t.Desc, + }) + for _, f := range t.ConfigFiles { + r.Results.DependencyUpdateTools[offset].ConfigFiles = + append(r.Results.DependencyUpdateTools[offset].ConfigFiles, jsonFile{ + Path: f.Path, + Offset: f.Offset, + }) + } + } + return nil +} + func (r *jsonScorecardRawResult) fillJSONRawResults(raw *checker.RawResults) error { // Binary-Artifacts. if err := r.addBinaryArtifactRawResults(&raw.BinaryArtifactResults); err != nil { @@ -78,6 +111,12 @@ func (r *jsonScorecardRawResult) fillJSONRawResults(raw *checker.RawResults) err if err := r.addSecurityPolicyRawResults(&raw.SecurityPolicyResults); err != nil { return sce.WithMessage(sce.ErrScorecardInternal, err.Error()) } + + // Dependecy-Update-Tool. + if err := r.addDependencyUpdateToolRawResults(&raw.DependencyUpdateToolResults); err != nil { + return sce.WithMessage(sce.ErrScorecardInternal, err.Error()) + } + return nil }