From 1a336d80870ceccc8e7c36243ecffef6ce190f17 Mon Sep 17 00:00:00 2001 From: laurentsimon <64505099+laurentsimon@users.noreply.github.com> Date: Mon, 22 May 2023 18:13:24 -0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=20[experimental]=20Add=20probe=20c?= =?UTF-8?q?ode=20and=20support=20for=20Tool-Update-Dependency=20(#2944)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * update Signed-off-by: laurentsimon * update Signed-off-by: laurentsimon * update Signed-off-by: laurentsimon * update Signed-off-by: laurentsimon * update Signed-off-by: laurentsimon * update Signed-off-by: laurentsimon * update Signed-off-by: laurentsimon * update Signed-off-by: laurentsimon --------- Signed-off-by: laurentsimon --- checker/check_result.go | 23 +++ checker/raw_result.go | 22 ++- checks/dependency_update_tool.go | 12 +- checks/dependency_update_tool_test.go | 18 +- checks/evaluation/dependency_update_tool.go | 49 +---- .../evaluation/dependency_update_tool_test.go | 176 +++++++----------- checks/raw/dependency_update_tool.go | 8 +- checks/raw/dependency_update_tool_test.go | 125 +++++-------- checks/run_probes.go | 41 ++++ e2e/dependency_update_tool_test.go | 8 +- probes/entries.go | 59 ++++++ probes/toolDependabotInstalled/def.yml | 32 ++++ probes/toolDependabotInstalled/impl.go | 53 ++++++ probes/toolPyUpInstalled/def.yml | 32 ++++ probes/toolPyUpInstalled/impl.go | 53 ++++++ probes/toolRenovateInstalled/def.yml | 32 ++++ probes/toolRenovateInstalled/impl.go | 53 ++++++ probes/toolSonatypeLiftInstalled/def.yml | 32 ++++ probes/toolSonatypeLiftInstalled/impl.go | 53 ++++++ probes/utils/tools.go | 69 +++++++ probes/zrunner/runner.go | 51 +++++ 21 files changed, 757 insertions(+), 244 deletions(-) create mode 100644 checks/run_probes.go create mode 100644 probes/entries.go create mode 100644 probes/toolDependabotInstalled/def.yml create mode 100644 probes/toolDependabotInstalled/impl.go create mode 100644 probes/toolPyUpInstalled/def.yml create mode 100644 probes/toolPyUpInstalled/impl.go create mode 100644 probes/toolRenovateInstalled/def.yml create mode 100644 probes/toolRenovateInstalled/impl.go create mode 100644 probes/toolSonatypeLiftInstalled/def.yml create mode 100644 probes/toolSonatypeLiftInstalled/impl.go create mode 100644 probes/utils/tools.go create mode 100644 probes/zrunner/runner.go diff --git a/checker/check_result.go b/checker/check_result.go index 8491bdd3..5e0dd219 100644 --- a/checker/check_result.go +++ b/checker/check_result.go @@ -193,3 +193,26 @@ func CreateRuntimeErrorResult(name string, e error) CheckResult { Reason: e.Error(), // Note: message already accessible by caller thru `Error`. } } + +// LogFindings logs the list of findings. +func LogFindings(findings []finding.Finding, dl DetailLogger) error { + for i := range findings { + f := &findings[i] + switch f.Outcome { + case finding.OutcomeNegative: + dl.Warn(&LogMessage{ + Finding: f, + }) + case finding.OutcomePositive: + dl.Info(&LogMessage{ + Finding: f, + }) + default: + dl.Debug(&LogMessage{ + Finding: f, + }) + } + } + + return nil +} diff --git a/checker/raw_result.go b/checker/raw_result.go index 8b991d18..37dfcbd3 100644 --- a/checker/raw_result.go +++ b/checker/raw_result.go @@ -242,7 +242,6 @@ type SignedReleasesData struct { // 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 } @@ -375,3 +374,24 @@ type TokenPermission struct { Msg *string Type PermissionLevel } + +// Location generates location from a file. +func (f *File) Location() *finding.Location { + // TODO(2626): merge location and path. + if f == nil { + return nil + } + loc := &finding.Location{ + Type: f.Type, + Path: f.Path, + LineStart: &f.Offset, + } + if f.EndOffset != 0 { + loc.LineEnd = &f.EndOffset + } + if f.Snippet != "" { + loc.Snippet = &f.Snippet + } + + return loc +} diff --git a/checks/dependency_update_tool.go b/checks/dependency_update_tool.go index fee37c19..54f1954f 100644 --- a/checks/dependency_update_tool.go +++ b/checks/dependency_update_tool.go @@ -19,12 +19,13 @@ import ( "github.com/ossf/scorecard/v4/checks/evaluation" "github.com/ossf/scorecard/v4/checks/raw" sce "github.com/ossf/scorecard/v4/errors" + "github.com/ossf/scorecard/v4/probes" ) // CheckDependencyUpdateTool is the exported name for Automatic-Depdendency-Update. const CheckDependencyUpdateTool = "Dependency-Update-Tool" -//nolint +// nolint func init() { supportedRequestTypes := []checker.RequestType{ checker.FileBased, @@ -48,6 +49,13 @@ func DependencyUpdateTool(c *checker.CheckRequest) checker.CheckResult { c.RawResults.DependencyUpdateToolResults = rawData } + // Evaluate the probes. + findings, err := evaluateProbes(c, CheckDependencyUpdateTool, probes.DependencyToolUpdates) + if err != nil { + e := sce.WithMessage(sce.ErrScorecardInternal, err.Error()) + return checker.CreateRuntimeErrorResult(CheckDependencyUpdateTool, e) + } + // Return the score evaluation. - return evaluation.DependencyUpdateTool(CheckDependencyUpdateTool, c.Dlogger, &rawData) + return evaluation.DependencyUpdateTool(CheckDependencyUpdateTool, findings) } diff --git a/checks/dependency_update_tool_test.go b/checks/dependency_update_tool_test.go index 2b21e6f9..173f4e51 100644 --- a/checks/dependency_update_tool_test.go +++ b/checks/dependency_update_tool_test.go @@ -51,6 +51,7 @@ func TestDependencyUpdateTool(t *testing.T) { CallSearchCommits: 0, expected: scut.TestReturn{ NumberOfInfo: 1, + NumberOfWarn: 3, Score: 10, }, }, @@ -63,6 +64,7 @@ func TestDependencyUpdateTool(t *testing.T) { CallSearchCommits: 0, expected: scut.TestReturn{ NumberOfInfo: 1, + NumberOfWarn: 3, Score: 10, }, }, @@ -75,7 +77,7 @@ func TestDependencyUpdateTool(t *testing.T) { SearchCommits: []clients.Commit{{Committer: clients.User{ID: 111111111}}}, CallSearchCommits: 1, expected: scut.TestReturn{ - NumberOfWarn: 1, + NumberOfWarn: 4, }, }, { @@ -87,7 +89,7 @@ func TestDependencyUpdateTool(t *testing.T) { SearchCommits: []clients.Commit{}, CallSearchCommits: 1, expected: scut.TestReturn{ - NumberOfWarn: 1, + NumberOfWarn: 4, }, }, @@ -101,6 +103,7 @@ func TestDependencyUpdateTool(t *testing.T) { CallSearchCommits: 1, expected: scut.TestReturn{ NumberOfInfo: 1, + NumberOfWarn: 3, Score: 10, }, }, @@ -108,13 +111,14 @@ func TestDependencyUpdateTool(t *testing.T) { name: "found in commits 2", wantErr: false, files: []string{}, - SearchCommits: []clients.Commit{{Committer: clients.User{ID: 111111111}}, + SearchCommits: []clients.Commit{ + {Committer: clients.User{ID: 111111111}}, {Committer: clients.User{ID: dependabotID}}, }, - CallSearchCommits: 1, expected: scut.TestReturn{ NumberOfInfo: 1, + NumberOfWarn: 3, Score: 10, }, }, @@ -125,12 +129,14 @@ func TestDependencyUpdateTool(t *testing.T) { files: []string{ ".github/foobar.yml", }, - SearchCommits: []clients.Commit{{Committer: clients.User{ID: 111111111}}, + SearchCommits: []clients.Commit{ + {Committer: clients.User{ID: 111111111}}, {Committer: clients.User{ID: dependabotID}}, }, CallSearchCommits: 1, expected: scut.TestReturn{ NumberOfInfo: 1, + NumberOfWarn: 3, Score: 10, }, }, @@ -144,9 +150,11 @@ func TestDependencyUpdateTool(t *testing.T) { mockRepo.EXPECT().ListFiles(gomock.Any()).Return(tt.files, nil) mockRepo.EXPECT().SearchCommits(gomock.Any()).Return(tt.SearchCommits, nil).Times(tt.CallSearchCommits) dl := scut.TestDetailLogger{} + raw := checker.RawResults{} c := &checker.CheckRequest{ RepoClient: mockRepo, Dlogger: &dl, + RawResults: &raw, } res := DependencyUpdateTool(c) diff --git a/checks/evaluation/dependency_update_tool.go b/checks/evaluation/dependency_update_tool.go index c527b82b..239167a4 100644 --- a/checks/evaluation/dependency_update_tool.go +++ b/checks/evaluation/dependency_update_tool.go @@ -15,51 +15,20 @@ package evaluation import ( - "fmt" - "github.com/ossf/scorecard/v4/checker" - sce "github.com/ossf/scorecard/v4/errors" + "github.com/ossf/scorecard/v4/finding" ) // DependencyUpdateTool applies the score policy for the Dependency-Update-Tool check. -func DependencyUpdateTool(name string, dl checker.DetailLogger, - r *checker.DependencyUpdateToolData, +func DependencyUpdateTool(name string, + findings []finding.Finding, ) checker.CheckResult { - if r == nil { - e := sce.WithMessage(sce.ErrScorecardInternal, "empty raw data") - return checker.CreateRuntimeErrorResult(name, e) + for i := range findings { + f := &findings[i] + if f.Outcome == finding.OutcomePositive { + return checker.CreateMaxScoreResult(name, "update tool detected") + } } - // Apply the policy evaluation. - if r.Tools == nil || len(r.Tools) == 0 { - dl.Warn(&checker.LogMessage{ - Text: `Config file not detected in source location for dependabot, renovatebot, Sonatype Lift, or - PyUp (Python). 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 r.Tools[0].Files == nil { - e := sce.WithMessage(sce.ErrScorecardInternal, "Files are nil") - return checker.CreateRuntimeErrorResult(name, e) - } - - // Iterate over all the files, since a Tool can contain multiple files. - for _, file := range r.Tools[0].Files { - dl.Info(&checker.LogMessage{ - Path: file.Path, - Type: file.Type, - Offset: file.Offset, - Text: fmt.Sprintf("%s detected", r.Tools[0].Name), - }) - } - - // High score result. - return checker.CreateMaxScoreResult(name, "update tool detected") + return checker.CreateMinScoreResult(name, "no update tool detected") } diff --git a/checks/evaluation/dependency_update_tool_test.go b/checks/evaluation/dependency_update_tool_test.go index 68d50f61..62667d6c 100644 --- a/checks/evaluation/dependency_update_tool_test.go +++ b/checks/evaluation/dependency_update_tool_test.go @@ -18,7 +18,6 @@ import ( "testing" "github.com/ossf/scorecard/v4/checker" - sce "github.com/ossf/scorecard/v4/errors" "github.com/ossf/scorecard/v4/finding" scut "github.com/ossf/scorecard/v4/utests" ) @@ -26,135 +25,91 @@ import ( func TestDependencyUpdateTool(t *testing.T) { t.Parallel() //nolint - type args struct { - name string - dl checker.DetailLogger - r *checker.DependencyUpdateToolData - } - //nolint tests := []struct { name string - args args - want checker.CheckResult + findings []finding.Finding err bool + want checker.CheckResult expected scut.TestReturn }{ { - name: "DependencyUpdateTool", - args: args{ - name: "DependencyUpdateTool", - dl: &scut.TestDetailLogger{}, - r: &checker.DependencyUpdateToolData{ - Tools: []checker.Tool{ - { - Name: "DependencyUpdateTool", - }, - }, - }, - }, - want: checker.CheckResult{ - Score: -1, - }, - err: false, - expected: scut.TestReturn{ - Error: sce.ErrScorecardInternal, - Score: -1, - }, - }, - { - name: "empty tool list", - args: args{ - name: "DependencyUpdateTool", - dl: &scut.TestDetailLogger{}, - r: &checker.DependencyUpdateToolData{ - Tools: []checker.Tool{}, - }, - }, - want: checker.CheckResult{ - Score: 0, - Error: nil, - }, - err: false, - expected: scut.TestReturn{ - Score: 0, - NumberOfWarn: 1, - }, - }, - { - name: "Valid tool", - args: args{ - name: "DependencyUpdateTool", - dl: &scut.TestDetailLogger{}, - r: &checker.DependencyUpdateToolData{ - Tools: []checker.Tool{ - { - Name: "DependencyUpdateTool", - Files: []checker.File{ - { - Path: "/etc/dependency-update-tool.conf", - Snippet: ` - [dependency-update-tool] - enabled = true - `, - Type: finding.FileTypeSource, - }, - }, - }, - }, + name: "dependabot", + findings: []finding.Finding{ + { + Probe: "toolDependabotInstalled", + Outcome: finding.OutcomePositive, }, }, want: checker.CheckResult{ Score: 10, - Error: nil, }, - expected: scut.TestReturn{ - Error: nil, - Score: 10, - NumberOfInfo: 1, - }, - err: false, }, { - name: "more than one tool in the list", - args: args{ - name: "DependencyUpdateTool", - dl: &scut.TestDetailLogger{}, - r: &checker.DependencyUpdateToolData{ - Tools: []checker.Tool{ - { - Name: "DependencyUpdateTool", - }, - { - Name: "DependencyUpdateTool", - }, - }, + name: "renovate", + findings: []finding.Finding{ + { + Probe: "toolRenovateInstalled", + Outcome: finding.OutcomePositive, }, }, want: checker.CheckResult{ - Score: -1, - Error: nil, + Score: 10, }, - expected: scut.TestReturn{ - Error: sce.ErrScorecardInternal, - Score: -1, - }, - err: false, }, { - name: "Nil r", - args: args{ - name: "nil r", - dl: &scut.TestDetailLogger{}, + name: "pyup", + findings: []finding.Finding{ + { + Probe: "toolPyUpInstalled", + Outcome: finding.OutcomePositive, + }, }, want: checker.CheckResult{ - Score: -1, + Score: 10, + }, + }, + { + name: "sonatype", + findings: []finding.Finding{ + { + Probe: "toolSonatypeInstalled", + Outcome: finding.OutcomePositive, + }, + }, + want: checker.CheckResult{ + Score: 10, + }, + }, + { + name: "none", + findings: []finding.Finding{ + { + Probe: "toolDependabotInstalled", + Outcome: finding.OutcomeNegative, + }, + { + Probe: "toolRenovateInstalled", + Outcome: finding.OutcomeNegative, + }, + { + Probe: "toolPyUpInstalled", + Outcome: finding.OutcomeNegative, + }, + { + Probe: "toolSonatypeInstalled", + Outcome: finding.OutcomeNegative, + }, + }, + want: checker.CheckResult{ + Score: 0, + }, + }, + { + name: "empty tool list", + want: checker.CheckResult{ + Score: 0, Error: nil, }, - expected: scut.TestReturn{ - Error: sce.ErrScorecardInternal, - Score: -1, - }, - err: false, }, } for _, tt := range tests { @@ -162,8 +117,7 @@ func TestDependencyUpdateTool(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - dl := scut.TestDetailLogger{} - got := DependencyUpdateTool(tt.args.name, &dl, tt.args.r) + got := DependencyUpdateTool(tt.name, tt.findings) if tt.want.Score != got.Score { t.Errorf("DependencyUpdateTool() got Score = %v, want %v for %v", got.Score, tt.want.Score, tt.name) } @@ -171,10 +125,6 @@ func TestDependencyUpdateTool(t *testing.T) { t.Errorf("DependencyUpdateTool() error = %v, want %v for %v", got.Error, tt.want.Error, tt.name) return } - - if !scut.ValidateTestReturn(t, tt.name, &tt.expected, &got, &dl) { - t.Fatalf(tt.name) - } }) } } diff --git a/checks/raw/dependency_update_tool.go b/checks/raw/dependency_update_tool.go index 68fc4a1e..3af013b4 100644 --- a/checks/raw/dependency_update_tool.go +++ b/checks/raw/dependency_update_tool.go @@ -126,13 +126,11 @@ var checkDependencyFileExists fileparser.DoWhileTrueOnFilename = func(name strin }, }, }) - default: - // Continue iterating. - return true, nil } - // We found a file, no need to continue iterating. - return false, nil + // Continue iterating, even if we have found a tool. + // It's needed for all probes results to be populated. + return true, nil } func asPointer(s string) *string { diff --git a/checks/raw/dependency_update_tool_test.go b/checks/raw/dependency_update_tool_test.go index 87fb21a7..02a31286 100644 --- a/checks/raw/dependency_update_tool_test.go +++ b/checks/raw/dependency_update_tool_test.go @@ -26,115 +26,84 @@ import ( func Test_checkDependencyFileExists(t *testing.T) { t.Parallel() - //nolint - type args struct { - name string - data *[]checker.Tool - } + //nolint tests := []struct { name string - args args + path string want bool wantErr bool }{ { - name: "check dependency file exists", - args: args{ - name: ".github/dependabot.yml", - data: &[]checker.Tool{}, - }, - want: false, - wantErr: false, - }, - { - name: ".other", - args: args{ - name: ".other", - data: &[]checker.Tool{}, - }, + name: ".github/dependabot.yml", + path: ".github/dependabot.yml", want: true, wantErr: false, }, { - name: ".github/renovate.json", - args: args{ - name: ".github/renovate.json", - data: &[]checker.Tool{}, - }, + name: ".github/dependabot.yaml", + path: ".github/dependabot.yaml", + want: true, + wantErr: false, + }, + { + name: ".other", + path: ".other", want: false, wantErr: false, }, { - name: ".github/renovate.json5", - args: args{ - name: ".github/renovate.json5", - data: &[]checker.Tool{}, - }, - want: false, + name: ".github/renovate.json", + path: ".github/renovate.json", + want: true, wantErr: false, }, { - name: ".renovaterc.json", - args: args{ - name: ".renovaterc.json", - data: &[]checker.Tool{}, - }, - want: false, + name: ".github/renovate.json5", + path: ".github/renovate.json5", + want: true, wantErr: false, }, { - name: "renovate.json", - args: args{ - name: "renovate.json", - data: &[]checker.Tool{}, - }, - want: false, + name: ".renovaterc.json", + path: ".renovaterc.json", + want: true, wantErr: false, }, { - name: "renovate.json5", - args: args{ - name: "renovate.json5", - data: &[]checker.Tool{}, - }, - want: false, + name: "renovate.json", + path: "renovate.json", + want: true, wantErr: false, }, { - name: ".renovaterc", - args: args{ - name: ".renovaterc", - data: &[]checker.Tool{}, - }, - want: false, + name: "renovate.json5", + path: "renovate.json5", + want: true, wantErr: false, }, { - name: ".pyup.yml", - args: args{ - name: ".pyup.yml", - data: &[]checker.Tool{}, - }, - want: false, + name: ".renovaterc", + path: ".renovaterc", + want: true, wantErr: false, }, { - name: ".lift.toml", - args: args{ - name: ".lift.toml", - data: &[]checker.Tool{}, - }, - want: false, + name: ".pyup.yml", + path: ".pyup.yml", + want: true, wantErr: false, }, { - name: ".lift/config.toml", - args: args{ - name: ".lift/config.toml", - data: &[]checker.Tool{}, - }, - want: false, + name: ".lift.toml", + path: ".lift.toml", + want: true, + wantErr: false, + }, + { + name: ".lift/config.toml", + path: ".lift/config.toml", + want: true, wantErr: false, }, } @@ -142,13 +111,17 @@ func Test_checkDependencyFileExists(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - got, err := checkDependencyFileExists(tt.args.name, tt.args.data) + results := []checker.Tool{} + cont, err := checkDependencyFileExists(tt.path, &results) if (err != nil) != tt.wantErr { t.Errorf("checkDependencyFileExists() error = %v, wantErr %v", err, tt.wantErr) return } - if got != tt.want { - t.Errorf("checkDependencyFileExists() = %v, want %v for test %v", got, tt.want, tt.name) + if !cont { + t.Errorf("continue is false for %v", tt.name) + } + if tt.want != (len(results) == 1) { + t.Errorf("checkDependencyFileExists() = %v, want %v for test %v", len(results), tt.want, tt.name) } }) } diff --git a/checks/run_probes.go b/checks/run_probes.go new file mode 100644 index 00000000..5af28ac7 --- /dev/null +++ b/checks/run_probes.go @@ -0,0 +1,41 @@ +// Copyright 2023 OpenSSF 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 ( + "fmt" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes" + "github.com/ossf/scorecard/v4/probes/zrunner" +) + +// evaluateProbes runs the probes in probesToRun and logs its findings. +func evaluateProbes(c *checker.CheckRequest, checkName string, + probesToRun []probes.ProbeImpl, +) ([]finding.Finding, error) { + // Run the probes. + findings, err := zrunner.Run(c.RawResults, probesToRun) + if err != nil { + return nil, fmt.Errorf("zrunner.Run: %w", err) + } + + // Log the findings. + if err := checker.LogFindings(findings, c.Dlogger); err != nil { + return nil, fmt.Errorf("LogFindings: %w", err) + } + return findings, nil +} diff --git a/e2e/dependency_update_tool_test.go b/e2e/dependency_update_tool_test.go index a580a6bb..244c849f 100644 --- a/e2e/dependency_update_tool_test.go +++ b/e2e/dependency_update_tool_test.go @@ -39,16 +39,18 @@ var _ = Describe("E2E TEST:"+checks.CheckDependencyUpdateTool, func() { err = repoClient.InitRepo(repo, clients.HeadSHA, 0) Expect(err).Should(BeNil()) + raw := checker.RawResults{} req := checker.CheckRequest{ Ctx: context.Background(), RepoClient: repoClient, Repo: repo, Dlogger: &dl, + RawResults: &raw, } expected := scut.TestReturn{ Error: nil, Score: checker.MaxResultScore, - NumberOfWarn: 0, + NumberOfWarn: 3, NumberOfInfo: 1, NumberOfDebug: 0, } @@ -66,16 +68,18 @@ var _ = Describe("E2E TEST:"+checks.CheckDependencyUpdateTool, func() { err = repoClient.InitRepo(repo, clients.HeadSHA, 0) Expect(err).Should(BeNil()) + raw := checker.RawResults{} req := checker.CheckRequest{ Ctx: context.Background(), RepoClient: repoClient, Repo: repo, Dlogger: &dl, + RawResults: &raw, } expected := scut.TestReturn{ Error: nil, Score: checker.MaxResultScore, - NumberOfWarn: 0, + NumberOfWarn: 3, NumberOfInfo: 1, NumberOfDebug: 0, } diff --git a/probes/entries.go b/probes/entries.go new file mode 100644 index 00000000..84be2c51 --- /dev/null +++ b/probes/entries.go @@ -0,0 +1,59 @@ +// Copyright 2023 OpenSSF 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 probes + +import ( + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/toolDependabotInstalled" + "github.com/ossf/scorecard/v4/probes/toolPyUpInstalled" + "github.com/ossf/scorecard/v4/probes/toolRenovateInstalled" + "github.com/ossf/scorecard/v4/probes/toolSonatypeLiftInstalled" +) + +// ProbeImpl is the implementation of a probe. +type ProbeImpl func(*checker.RawResults) ([]finding.Finding, string, error) + +var ( + // All represents all the probes. + All []ProbeImpl + // DependencyToolUpdates is all the probes for the + // DpendencyUpdateTool check. + DependencyToolUpdates = []ProbeImpl{ + toolRenovateInstalled.Run, + toolDependabotInstalled.Run, + toolPyUpInstalled.Run, + toolSonatypeLiftInstalled.Run, + } +) + +//nolint:gochecknoinits +func init() { + All = concatMultipleProbes([][]ProbeImpl{ + DependencyToolUpdates, + }) +} + +func concatMultipleProbes(slices [][]ProbeImpl) []ProbeImpl { + var totalLen int + for _, s := range slices { + totalLen += len(s) + } + tmp := make([]ProbeImpl, 0, totalLen) + for _, s := range slices { + tmp = append(tmp, s...) + } + return tmp +} diff --git a/probes/toolDependabotInstalled/def.yml b/probes/toolDependabotInstalled/def.yml new file mode 100644 index 00000000..e58d6e14 --- /dev/null +++ b/probes/toolDependabotInstalled/def.yml @@ -0,0 +1,32 @@ +# Copyright 2023 OpenSSF 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. + +id: toolDependabotInstalled +short: Check that Dependabot is enabled +motivation: > + Out-of-date dependencies make a project vulnerable to known flaws and prone to attacks. + Dependabot automates the process of updating dependencies by scanning for outdated or insecure requirements, and opening a pull request to update them if found. +implementation: > + The implemtation looks for the presence of files named ".github/dependabot.yml" or ".github/dependabot.yaml". If none of these files are found, + the implementation checks whether commits are authored by Dependabot. If none of these succeed, Dependabot is not installed. + NOTE: if the configuration files are found, the probe does not ensure that the Dependabot is run or that the Dependabot's pull requests are merged. +outcome: + - If dependendabot is installed, the probe returns OutcomePositive (1) + - If dependendabot is not installed, the probe returns OutcomeNegative (0) +remediation: + effort: Low + text: + - Follow the instructions from https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates. + markdown: + - Follow the instructions from [the official documentation](https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates). \ No newline at end of file diff --git a/probes/toolDependabotInstalled/impl.go b/probes/toolDependabotInstalled/impl.go new file mode 100644 index 00000000..1ca92087 --- /dev/null +++ b/probes/toolDependabotInstalled/impl.go @@ -0,0 +1,53 @@ +// Copyright 2022 OpenSSF 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:stylecheck +package toolDependabotInstalled + +import ( + "embed" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/utils" +) + +//go:embed *.yml +var fs embed.FS + +const probe = "toolDependabotInstalled" + +type dependabot struct{} + +func (t dependabot) Name() string { + return "Dependabot" +} + +func (t dependabot) Matches(tool *checker.Tool) bool { + return t.Name() == tool.Name +} + +func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { + tools := raw.DependencyUpdateToolResults.Tools + var matcher dependabot + // Check whether Dependabot tool is installed on the repo, + // and create the corresponding findings. + //nolint:wrapcheck + return utils.ToolsRun(tools, fs, probe, + // Tool found will generate a positive result. + finding.OutcomePositive, + // Tool not found will generate a negative result. + finding.OutcomeNegative, + matcher) +} diff --git a/probes/toolPyUpInstalled/def.yml b/probes/toolPyUpInstalled/def.yml new file mode 100644 index 00000000..9529194c --- /dev/null +++ b/probes/toolPyUpInstalled/def.yml @@ -0,0 +1,32 @@ +# Copyright 2023 OpenSSF 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. + +id: toolPyUpInstalled +short: Check that PyUp is installed. +motivation: > + Out-of-date dependencies make a project vulnerable to known flaws and prone to attacks. + PyUp automates the process of updating dependencies by scanning for outdated or insecure requirements, and opening a pull request to update them if found. +implementation: > + The implementation looks for the presence of a file named ".pyup.yml". + If the file is not found, PyUp is not installed. + NOTE: the implementation does not ensure that PyUp is run or that PyUp's pull requests are merged. +outcome: + - If PyUp is installed, the probe returns OutcomePositive (1) + - If PyUp is not installed, the probe returns OutcomeNegative (0) +remediation: + effort: Low + text: + - Follow the instructions from https://docs.pyup.io/docs. + markdown: + - Follow the instructions from [the official documentation](https://docs.pyup.io/docs). \ No newline at end of file diff --git a/probes/toolPyUpInstalled/impl.go b/probes/toolPyUpInstalled/impl.go new file mode 100644 index 00000000..41c58db8 --- /dev/null +++ b/probes/toolPyUpInstalled/impl.go @@ -0,0 +1,53 @@ +// Copyright 2022 OpenSSF 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:stylecheck +package toolPyUpInstalled + +import ( + "embed" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/utils" +) + +//go:embed *.yml +var fs embed.FS + +const probe = "toolPyUpInstalled" + +type pyup struct{} + +func (t pyup) Name() string { + return "PyUp" +} + +func (t pyup) Matches(tool *checker.Tool) bool { + return t.Name() == tool.Name +} + +func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { + tools := raw.DependencyUpdateToolResults.Tools + var matcher pyup + // Check whether PyUp tool is installed on the repo, + // and create the corresponding findings. + //nolint:wrapcheck + return utils.ToolsRun(tools, fs, probe, + // Tool found will generate a positive result. + finding.OutcomePositive, + // Tool not found will generate a negative result. + finding.OutcomeNegative, + matcher) +} diff --git a/probes/toolRenovateInstalled/def.yml b/probes/toolRenovateInstalled/def.yml new file mode 100644 index 00000000..72a9f106 --- /dev/null +++ b/probes/toolRenovateInstalled/def.yml @@ -0,0 +1,32 @@ +# Copyright 2023 OpenSSF 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. + +id: toolRenovateInstalled +short: Check that Renovate bot is installed. +motivation: > + Out-of-date dependencies make a project vulnerable to known flaws and prone to attacks. + Renovate automates the process of updating dependencies by scanning for outdated or insecure requirements, and opening a pull request to update them if found. +implementation: > + The implementation looks for the presence of files named ".github/renovate.json", ".github/renovate.json5", ".renovaterc.json" or. "renovate.json". + If none of these files are found, Renovate is not installed. + NOTE: the implementation does not ensure that Renovate is run or that Renovate's pull requests are merged. +outcome: + - If Renovate is installed, the probe returns OutcomePositive (1) + - If Renovate is not installed, the probe returns OutcomeNegative (0) +remediation: + effort: Low + text: + - Follow the instructions from https://docs.renovatebot.com/configuration-options/. + markdown: + - Follow the instructions from [the official documentation](https://docs.renovatebot.com/configuration-options/). \ No newline at end of file diff --git a/probes/toolRenovateInstalled/impl.go b/probes/toolRenovateInstalled/impl.go new file mode 100644 index 00000000..a35f9166 --- /dev/null +++ b/probes/toolRenovateInstalled/impl.go @@ -0,0 +1,53 @@ +// Copyright 2022 OpenSSF 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:stylecheck +package toolRenovateInstalled + +import ( + "embed" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/utils" +) + +//go:embed *.yml +var fs embed.FS + +const probe = "toolRenovateInstalled" + +type renovate struct{} + +func (t renovate) Name() string { + return "RenovateBot" +} + +func (t renovate) Matches(tool *checker.Tool) bool { + return t.Name() == tool.Name +} + +func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { + tools := raw.DependencyUpdateToolResults.Tools + var matcher renovate + // Check whether Renovate tool is installed on the repo, + // and create the corresponding findings. + //nolint:wrapcheck + return utils.ToolsRun(tools, fs, probe, + // Tool found will generate a positive result. + finding.OutcomePositive, + // Tool not found will generate a negative result. + finding.OutcomeNegative, + matcher) +} diff --git a/probes/toolSonatypeLiftInstalled/def.yml b/probes/toolSonatypeLiftInstalled/def.yml new file mode 100644 index 00000000..e2d38e1c --- /dev/null +++ b/probes/toolSonatypeLiftInstalled/def.yml @@ -0,0 +1,32 @@ +# Copyright 2023 OpenSSF 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. + +id: toolSonatypeLiftInstalled +short: Check that Sonatype Lyft is installed. +motivation: > + Out-of-date dependencies make a project vulnerable to known flaws and prone to attacks. + Sonatype Lyft automates the process of updating dependencies by scanning for outdated or insecure requirements, and opening a pull request to update them if found. +implementation: > + The implementation looks for the presence of files named ".lift.toml" or ".lift/config.toml". + If none of these files are found, Sonatype Lyft is not installed. + NOTE: the implementation does not ensure that Sonatype Lyft is run or that Sonatype Lyft's pull requests are merged. +outcome: + - If Sonatype Lyft is installed, the probe returns OutcomePositive (1) + - If Sonatype Lyft is not installed, the probe returns OutcomeNegative (0) +remediation: + effort: Low + text: + - Follow the instructions from https://help.sonatype.com/lift/getting-started. + markdown: + - Follow the instructions from [the official documentation](https://help.sonatype.com/lift/getting-started). \ No newline at end of file diff --git a/probes/toolSonatypeLiftInstalled/impl.go b/probes/toolSonatypeLiftInstalled/impl.go new file mode 100644 index 00000000..a7c258fb --- /dev/null +++ b/probes/toolSonatypeLiftInstalled/impl.go @@ -0,0 +1,53 @@ +// Copyright 2022 OpenSSF 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:stylecheck +package toolSonatypeLiftInstalled + +import ( + "embed" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/utils" +) + +//go:embed *.yml +var fs embed.FS + +const probe = "toolSonatypeLiftInstalled" + +type sonatypeLyft struct{} + +func (t sonatypeLyft) Name() string { + return "Sonatype Lift" +} + +func (t sonatypeLyft) Matches(tool *checker.Tool) bool { + return t.Name() == tool.Name +} + +func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { + tools := raw.DependencyUpdateToolResults.Tools + var matcher sonatypeLyft + // Check whether Sona Lyft tool is installed on the repo, + // and create the corresponding findings. + //nolint:wrapcheck + return utils.ToolsRun(tools, fs, probe, + // Tool found will generate a positive result. + finding.OutcomePositive, + // Tool not found will generate a negative result. + finding.OutcomeNegative, + matcher) +} diff --git a/probes/utils/tools.go b/probes/utils/tools.go new file mode 100644 index 00000000..c802126f --- /dev/null +++ b/probes/utils/tools.go @@ -0,0 +1,69 @@ +// Copyright 2023 OpenSSF 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 utils + +import ( + "embed" + "fmt" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" +) + +type toolMatcher interface { + Name() string + Matches(*checker.Tool) bool +} + +// ToolsRun runs the probe for a tool. +// The function iterates thru the raw results and searches for a tool of interest that is used on a repository. +// The function uses 'matcher' to identify the tool of interest. +// If a tool is used in the repository, it creates a finding with the 'foundOutcome'. +// If not, it returns a finding with outcome 'notFoundOutcome'. +func ToolsRun(tools []checker.Tool, fs embed.FS, probeID string, + foundOutcome, notFoundOutcome finding.Outcome, matcher toolMatcher, +) ([]finding.Finding, string, error) { + var findings []finding.Finding + for i := range tools { + tool := &tools[i] + if !matcher.Matches(tool) { + continue + } + + var loc *finding.Location + if len(tool.Files) > 0 { + loc = tool.Files[0].Location() + } + + f, err := finding.NewWith(fs, probeID, fmt.Sprintf("tool '%s' is used", tool.Name), + loc, foundOutcome) + if err != nil { + return nil, probeID, fmt.Errorf("create finding: %w", err) + } + findings = append(findings, *f) + } + + // No tools found. + if len(findings) == 0 { + f, err := finding.NewWith(fs, probeID, fmt.Sprintf("tool '%s' is not used", matcher.Name()), + nil, notFoundOutcome) + if err != nil { + return nil, probeID, fmt.Errorf("create finding: %w", err) + } + findings = append(findings, *f) + } + + return findings, probeID, nil +} diff --git a/probes/zrunner/runner.go b/probes/zrunner/runner.go new file mode 100644 index 00000000..e8c837bb --- /dev/null +++ b/probes/zrunner/runner.go @@ -0,0 +1,51 @@ +// Copyright 2023 OpenSSF 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 zrunner + +import ( + "errors" + "fmt" + + "github.com/ossf/scorecard/v4/checker" + serrors "github.com/ossf/scorecard/v4/errors" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes" +) + +var errProbeRun = errors.New("probe run failure") + +// Run runs the probes in probesToRun. +func Run(raw *checker.RawResults, probesToRun []probes.ProbeImpl) ([]finding.Finding, error) { + var results []finding.Finding + var errs []error + for _, probeFunc := range probesToRun { + findings, probeID, err := probeFunc(raw) + if err != nil { + errs = append(errs, err) + results = append(results, + finding.Finding{ + Probe: probeID, + Outcome: finding.OutcomeError, + Message: serrors.WithMessage(serrors.ErrScorecardInternal, err.Error()).Error(), + }) + continue + } + results = append(results, findings...) + } + if len(errs) > 0 { + return results, fmt.Errorf("%w: %v", errProbeRun, errs) + } + return results, nil +}