From c1d955c553a2c8cda88a0f7d91e253b73440d577 Mon Sep 17 00:00:00 2001 From: Carlos Panato Date: Thu, 24 Feb 2022 09:49:24 +0100 Subject: [PATCH] checks: add GitHub Webhook check Signed-off-by: Carlos Panato --- checker/raw_result.go | 13 ++ checks/raw/webhook.go | 45 ++++++ checks/raw/webhooks_test.go | 135 ++++++++++++++++++ checks/webhook.go | 66 +++++++++ checks/webhook_test.go | 126 +++++++++++++++++ clients/githubrepo/client.go | 12 ++ clients/githubrepo/webhook.go | 84 +++++++++++ clients/localdir/client.go | 5 + clients/mockclients/repo_client.go | 214 +++++++++++++++-------------- clients/repo_client.go | 1 + clients/webhook.go | 21 +++ cmd/root.go | 6 + docs/checks.md | 12 ++ docs/checks/internal/checks.yaml | 17 +++ 14 files changed, 657 insertions(+), 100 deletions(-) create mode 100644 checks/raw/webhook.go create mode 100644 checks/raw/webhooks_test.go create mode 100644 checks/webhook.go create mode 100644 checks/webhook_test.go create mode 100644 clients/githubrepo/webhook.go create mode 100644 clients/webhook.go diff --git a/checker/raw_result.go b/checker/raw_result.go index 8605af992f8d..d97261352e82 100644 --- a/checker/raw_result.go +++ b/checker/raw_result.go @@ -61,6 +61,19 @@ type DependencyUpdateToolData struct { Tools []Tool } +// WebhooksData contains the raw results +// for the Webhook check. +type WebhooksData struct { + Webhook []WebhookData +} + +// WebhookData contains the raw results +// for webhook check. +type WebhookData struct { + ID *int64 + HasSecret *bool +} + // BranchProtectionsData contains the raw results // for the Branch-Protection check. type BranchProtectionsData struct { diff --git a/checks/raw/webhook.go b/checks/raw/webhook.go new file mode 100644 index 000000000000..1db92abf61ef --- /dev/null +++ b/checks/raw/webhook.go @@ -0,0 +1,45 @@ +// Copyright 2022 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 ( + "github.com/ossf/scorecard/v4/checker" + sce "github.com/ossf/scorecard/v4/errors" +) + +// WebHook retrieves the raw data for the WebHooks check. +func WebHook(c *checker.CheckRequest) (checker.WebhooksData, error) { + hooksResp, err := c.RepoClient.ListWebhooks() + if err != nil { + return checker.WebhooksData{}, + sce.WithMessage(sce.ErrScorecardInternal, "Client.Repositories.ListWebhooks") + } + + if len(hooksResp) < 1 { + return checker.WebhooksData{}, nil + } + + hooks := []checker.WebhookData{} + for _, hook := range hooksResp { + v := checker.WebhookData{ + ID: &hook.ID, + HasSecret: &hook.HasSecret, + // Note: add fields if needed. + } + hooks = append(hooks, v) + } + + return checker.WebhooksData{Webhook: hooks}, nil +} diff --git a/checks/raw/webhooks_test.go b/checks/raw/webhooks_test.go new file mode 100644 index 000000000000..47f037d6a295 --- /dev/null +++ b/checks/raw/webhooks_test.go @@ -0,0 +1,135 @@ +// Copyright 2022 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 ( + "context" + "testing" + + "github.com/golang/mock/gomock" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/clients" + mockrepo "github.com/ossf/scorecard/v4/clients/mockclients" + sce "github.com/ossf/scorecard/v4/errors" + scut "github.com/ossf/scorecard/v4/utests" +) + +func TestWebhooks(t *testing.T) { + t.Parallel() + //nolint + tests := []struct { + name string + err error + wantErr bool + expectedHasSecret int + expected scut.TestReturn + webhookResponse []*clients.Webhook + }{ + { + name: "No Webhooks", + wantErr: false, + webhookResponse: []*clients.Webhook{}, + }, + { + name: "Error getting webhook", + wantErr: true, + err: sce.ErrScorecardInternal, + }, + { + name: "Webhook with no secret", + wantErr: false, + expectedHasSecret: 0, + webhookResponse: []*clients.Webhook{ + { + HasSecret: false, + }, + }, + }, + { + name: "Webhook with secrets", + wantErr: false, + expectedHasSecret: 2, + webhookResponse: []*clients.Webhook{ + { + HasSecret: true, + }, + { + HasSecret: true, + }, + }, + }, + { + name: "Webhook with secrets and some without defined secrets", + wantErr: false, + expectedHasSecret: 1, + webhookResponse: []*clients.Webhook{ + { + HasSecret: true, + }, + { + HasSecret: false, + }, + { + HasSecret: false, + }, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockRepo := mockrepo.NewMockRepoClient(ctrl) + + mockRepo.EXPECT().ListWebhooks().DoAndReturn(func() ([]*clients.Webhook, error) { + if tt.err != nil { + return nil, tt.err + } + + return tt.webhookResponse, nil + }).AnyTimes() + + dl := scut.TestDetailLogger{} + req := checker.CheckRequest{ + RepoClient: mockRepo, + Ctx: context.TODO(), + Dlogger: &dl, + } + got, err := WebHook(&req) + if (err != nil) != tt.wantErr { + t.Errorf("Webhooks() error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr { + gotHasSecret := 0 + for _, gotHook := range got.Webhook { + if *gotHook.HasSecret { + gotHasSecret++ + } + } + + if gotHasSecret != tt.expectedHasSecret { + t.Errorf("Webhooks() got = %v, want %v", gotHasSecret, tt.expectedHasSecret) + } + } + + if !scut.ValidateTestReturn(t, tt.name, &tt.expected, &checker.CheckResult{}, &dl) { + t.Fatalf("Test %s failed", tt.name) + } + }) + } +} diff --git a/checks/webhook.go b/checks/webhook.go new file mode 100644 index 000000000000..c08eba3dad83 --- /dev/null +++ b/checks/webhook.go @@ -0,0 +1,66 @@ +// Copyright 2022 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 ( + "fmt" + + "github.com/ossf/scorecard/v4/checker" + sce "github.com/ossf/scorecard/v4/errors" +) + +const ( + // CheckWebHooks is the registered name for WebHooks. + CheckWebHooks = "Webhooks" +) + +//nolint:gochecknoinits +func init() { + if err := registerCheck(CheckWebHooks, WebHooks, nil); err != nil { + // this should never happen + panic(err) + } +} + +// WebHooks run Contributors check. +func WebHooks(c *checker.CheckRequest) checker.CheckResult { + hooks, err := c.RepoClient.ListWebhooks() + if err != nil { + e := sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("Client.Repositories.ListWebhooks: %v", err)) + return checker.CreateRuntimeErrorResult(CheckWebHooks, e) + } + + if len(hooks) < 1 { + return checker.CreateMaxScoreResult(CheckWebHooks, "no webhooks defined") + } + + hasSecretToCount := 0 + for _, hook := range hooks { + if !hook.HasSecret { + hasSecretToCount++ + } + } + + if hasSecretToCount == 0 { + return checker.CreateMaxScoreResult(CheckWebHooks, "all webhooks have secrets defined") + } + + if len(hooks) == hasSecretToCount { + return checker.CreateMinScoreResult(CheckWebHooks, "webhooks with no secrets configured detected") + } + + return checker.CreateProportionalScoreResult(CheckWebHooks, + "webhooks with no secrets configured detected", hasSecretToCount, len(hooks)) +} diff --git a/checks/webhook_test.go b/checks/webhook_test.go new file mode 100644 index 000000000000..9f80aba66d3e --- /dev/null +++ b/checks/webhook_test.go @@ -0,0 +1,126 @@ +// Copyright 2022 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 ( + "context" + "testing" + + "github.com/golang/mock/gomock" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/clients" + mockrepo "github.com/ossf/scorecard/v4/clients/mockclients" +) + +func TestWebhooks(t *testing.T) { + t.Parallel() + tests := []struct { + expected checker.CheckResult + err error + name string + webhooks []*clients.Webhook + }{ + { + name: "No Webhooks", + expected: checker.CheckResult{ + Pass: true, + Score: 10, + }, + err: nil, + webhooks: []*clients.Webhook{}, + }, + { + name: "With Webhooks and secret set", + expected: checker.CheckResult{ + Pass: true, + Score: 10, + }, + err: nil, + webhooks: []*clients.Webhook{ + { + HasSecret: true, + }, + }, + }, + { + name: "With Webhooks and no secret set", + expected: checker.CheckResult{ + Pass: false, + Score: 0, + }, + err: nil, + webhooks: []*clients.Webhook{ + { + HasSecret: false, + }, + }, + }, + { + name: "With 2 Webhooks with and whitout secrets configured", + expected: checker.CheckResult{ + Pass: false, + Score: 5, + }, + err: nil, + webhooks: []*clients.Webhook{ + { + HasSecret: false, + }, + { + HasSecret: true, + }, + }, + }, + } + + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockRepo := mockrepo.NewMockRepoClient(ctrl) + + mockRepo.EXPECT().ListWebhooks().DoAndReturn(func() ([]*clients.Webhook, error) { + if tt.err != nil { + return nil, tt.err + } + return tt.webhooks, tt.err + }).MaxTimes(1) + + req := checker.CheckRequest{ + RepoClient: mockRepo, + Ctx: context.TODO(), + } + res := WebHooks(&req) + if tt.err != nil { + if res.Error2 == nil { + t.Errorf("Expected error %v, got nil", tt.err) + } + // return as we don't need to check the rest of the fields. + return + } + + if res.Score != tt.expected.Score { + t.Errorf("Expected score %d, got %d for %v", tt.expected.Score, res.Score, tt.name) + } + if res.Pass != tt.expected.Pass { + t.Errorf("Expected pass %t, got %t for %v", tt.expected.Pass, res.Pass, tt.name) + } + ctrl.Finish() + }) + } +} diff --git a/clients/githubrepo/client.go b/clients/githubrepo/client.go index a1ed069c4d67..6869b91887c3 100644 --- a/clients/githubrepo/client.go +++ b/clients/githubrepo/client.go @@ -45,6 +45,7 @@ type Client struct { checkruns *checkrunsHandler statuses *statusesHandler search *searchHandler + webhook *webhookHandler ctx context.Context tarball tarballHandler } @@ -99,6 +100,9 @@ func (client *Client) InitRepo(inputRepo clients.Repo, commitSHA string) error { // Setup searchHandler. client.search.init(client.ctx, client.repourl) + // Setup webhookHandler. + client.webhook.init(client.ctx, client.repourl) + return nil } @@ -152,6 +156,11 @@ func (client *Client) ListBranches() ([]*clients.BranchRef, error) { return client.branches.listBranches() } +// ListWebhooks implements RepoClient.ListWebhooks. +func (client *Client) ListWebhooks() ([]*clients.Webhook, error) { + return client.webhook.listWebhooks() +} + // ListSuccessfulWorkflowRuns implements RepoClient.WorkflowRunsByFilename. func (client *Client) ListSuccessfulWorkflowRuns(filename string) ([]clients.WorkflowRun, error) { return client.workflows.listSuccessfulWorkflowRuns(filename) @@ -213,6 +222,9 @@ func CreateGithubRepoClientWithTransport(ctx context.Context, rt http.RoundTripp search: &searchHandler{ ghClient: client, }, + webhook: &webhookHandler{ + ghClient: client, + }, } } diff --git a/clients/githubrepo/webhook.go b/clients/githubrepo/webhook.go new file mode 100644 index 000000000000..ebc194519b49 --- /dev/null +++ b/clients/githubrepo/webhook.go @@ -0,0 +1,84 @@ +// Copyright 2022 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 githubrepo + +import ( + "context" + "fmt" + "strings" + "sync" + + "github.com/google/go-github/v38/github" + + "github.com/ossf/scorecard/v4/clients" +) + +type webhookHandler struct { + ghClient *github.Client + once *sync.Once + ctx context.Context + errSetup error + repourl *repoURL + webhook []*clients.Webhook +} + +func (handler *webhookHandler) init(ctx context.Context, repourl *repoURL) { + handler.ctx = ctx + handler.repourl = repourl + handler.errSetup = nil + handler.once = new(sync.Once) +} + +func (handler *webhookHandler) setup() error { + handler.once.Do(func() { + if !strings.EqualFold(handler.repourl.commitSHA, clients.HeadSHA) { + handler.errSetup = fmt.Errorf("%w: ListWebHooks only supported for HEAD queries", clients.ErrUnsupportedFeature) + return + } + hooks, _, err := handler.ghClient.Repositories.ListHooks( + handler.ctx, handler.repourl.owner, handler.repourl.repo, &github.ListOptions{}) + if err != nil { + handler.errSetup = fmt.Errorf("error during ListHooks: %w", err) + return + } + + for _, hook := range hooks { + repoHook := &clients.Webhook{ + ID: hook.GetID(), + HasSecret: hasSecret(hook.Config), + } + handler.webhook = append(handler.webhook, repoHook) + } + handler.errSetup = nil + }) + return handler.errSetup +} + +func hasSecret(config map[string]interface{}) bool { + if val, ok := config["secret"]; ok { + if val != nil { + return true + } + } + + return false +} + +func (handler *webhookHandler) listWebhooks() ([]*clients.Webhook, error) { + if err := handler.setup(); err != nil { + return nil, fmt.Errorf("error during webhookHandler.setup: %w", err) + } + return handler.webhook, nil +} diff --git a/clients/localdir/client.go b/clients/localdir/client.go index 029e4531f50d..424f4d1288ae 100644 --- a/clients/localdir/client.go +++ b/clients/localdir/client.go @@ -198,6 +198,11 @@ func (client *localDirClient) ListStatuses(ref string) ([]clients.Status, error) return nil, fmt.Errorf("ListStatuses: %w", clients.ErrUnsupportedFeature) } +// ListWebhooks implements RepoClient.ListWebhooks. +func (client *localDirClient) ListWebhooks() ([]*clients.Webhook, error) { + return nil, fmt.Errorf("ListWebhooks: %w", clients.ErrUnsupportedFeature) +} + // Search implements RepoClient.Search. func (client *localDirClient) Search(request clients.SearchRequest) (clients.SearchResponse, error) { return clients.SearchResponse{}, fmt.Errorf("Search: %w", clients.ErrUnsupportedFeature) diff --git a/clients/mockclients/repo_client.go b/clients/mockclients/repo_client.go index 8f458bcb9587..3499279bce0e 100644 --- a/clients/mockclients/repo_client.go +++ b/clients/mockclients/repo_client.go @@ -20,109 +20,108 @@ package mockrepo import ( - reflect "reflect" - gomock "github.com/golang/mock/gomock" clients "github.com/ossf/scorecard/v4/clients" + reflect "reflect" ) -// MockRepoClient is a mock of RepoClient interface. +// MockRepoClient is a mock of RepoClient interface type MockRepoClient struct { ctrl *gomock.Controller recorder *MockRepoClientMockRecorder } -// MockRepoClientMockRecorder is the mock recorder for MockRepoClient. +// MockRepoClientMockRecorder is the mock recorder for MockRepoClient type MockRepoClientMockRecorder struct { mock *MockRepoClient } -// NewMockRepoClient creates a new mock instance. +// NewMockRepoClient creates a new mock instance func NewMockRepoClient(ctrl *gomock.Controller) *MockRepoClient { mock := &MockRepoClient{ctrl: ctrl} mock.recorder = &MockRepoClientMockRecorder{mock} return mock } -// EXPECT returns an object that allows the caller to indicate expected use. +// EXPECT returns an object that allows the caller to indicate expected use func (m *MockRepoClient) EXPECT() *MockRepoClientMockRecorder { return m.recorder } -// Close mocks base method. -func (m *MockRepoClient) Close() error { +// InitRepo mocks base method +func (m *MockRepoClient) InitRepo(repo clients.Repo, commitSHA string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Close") + ret := m.ctrl.Call(m, "InitRepo", repo, commitSHA) ret0, _ := ret[0].(error) return ret0 } -// Close indicates an expected call of Close. -func (mr *MockRepoClientMockRecorder) Close() *gomock.Call { +// InitRepo indicates an expected call of InitRepo +func (mr *MockRepoClientMockRecorder) InitRepo(repo, commitSHA interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockRepoClient)(nil).Close)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitRepo", reflect.TypeOf((*MockRepoClient)(nil).InitRepo), repo, commitSHA) } -// GetDefaultBranch mocks base method. -func (m *MockRepoClient) GetDefaultBranch() (*clients.BranchRef, error) { +// URI mocks base method +func (m *MockRepoClient) URI() string { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDefaultBranch") - ret0, _ := ret[0].(*clients.BranchRef) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret := m.ctrl.Call(m, "URI") + ret0, _ := ret[0].(string) + return ret0 } -// GetDefaultBranch indicates an expected call of GetDefaultBranch. -func (mr *MockRepoClientMockRecorder) GetDefaultBranch() *gomock.Call { +// URI indicates an expected call of URI +func (mr *MockRepoClientMockRecorder) URI() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultBranch", reflect.TypeOf((*MockRepoClient)(nil).GetDefaultBranch)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "URI", reflect.TypeOf((*MockRepoClient)(nil).URI)) } -// GetFileContent mocks base method. -func (m *MockRepoClient) GetFileContent(filename string) ([]byte, error) { +// IsArchived mocks base method +func (m *MockRepoClient) IsArchived() (bool, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetFileContent", filename) - ret0, _ := ret[0].([]byte) + ret := m.ctrl.Call(m, "IsArchived") + ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetFileContent indicates an expected call of GetFileContent. -func (mr *MockRepoClientMockRecorder) GetFileContent(filename interface{}) *gomock.Call { +// IsArchived indicates an expected call of IsArchived +func (mr *MockRepoClientMockRecorder) IsArchived() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFileContent", reflect.TypeOf((*MockRepoClient)(nil).GetFileContent), filename) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsArchived", reflect.TypeOf((*MockRepoClient)(nil).IsArchived)) } -// InitRepo mocks base method. -func (m *MockRepoClient) InitRepo(repo clients.Repo, commitSHA string) error { +// ListFiles mocks base method +func (m *MockRepoClient) ListFiles(predicate func(string) (bool, error)) ([]string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InitRepo", repo, commitSHA) - ret0, _ := ret[0].(error) - return ret0 + ret := m.ctrl.Call(m, "ListFiles", predicate) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 } -// InitRepo indicates an expected call of InitRepo. -func (mr *MockRepoClientMockRecorder) InitRepo(repo, commitSHA interface{}) *gomock.Call { +// ListFiles indicates an expected call of ListFiles +func (mr *MockRepoClientMockRecorder) ListFiles(predicate interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitRepo", reflect.TypeOf((*MockRepoClient)(nil).InitRepo), repo, commitSHA) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListFiles", reflect.TypeOf((*MockRepoClient)(nil).ListFiles), predicate) } -// IsArchived mocks base method. -func (m *MockRepoClient) IsArchived() (bool, error) { +// GetFileContent mocks base method +func (m *MockRepoClient) GetFileContent(filename string) ([]byte, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsArchived") - ret0, _ := ret[0].(bool) + ret := m.ctrl.Call(m, "GetFileContent", filename) + ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(error) return ret0, ret1 } -// IsArchived indicates an expected call of IsArchived. -func (mr *MockRepoClientMockRecorder) IsArchived() *gomock.Call { +// GetFileContent indicates an expected call of GetFileContent +func (mr *MockRepoClientMockRecorder) GetFileContent(filename interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsArchived", reflect.TypeOf((*MockRepoClient)(nil).IsArchived)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFileContent", reflect.TypeOf((*MockRepoClient)(nil).GetFileContent), filename) } -// ListBranches mocks base method. +// ListBranches mocks base method func (m *MockRepoClient) ListBranches() ([]*clients.BranchRef, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListBranches") @@ -131,28 +130,28 @@ func (m *MockRepoClient) ListBranches() ([]*clients.BranchRef, error) { return ret0, ret1 } -// ListBranches indicates an expected call of ListBranches. +// ListBranches indicates an expected call of ListBranches func (mr *MockRepoClientMockRecorder) ListBranches() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBranches", reflect.TypeOf((*MockRepoClient)(nil).ListBranches)) } -// ListCheckRunsForRef mocks base method. -func (m *MockRepoClient) ListCheckRunsForRef(ref string) ([]clients.CheckRun, error) { +// GetDefaultBranch mocks base method +func (m *MockRepoClient) GetDefaultBranch() (*clients.BranchRef, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListCheckRunsForRef", ref) - ret0, _ := ret[0].([]clients.CheckRun) + ret := m.ctrl.Call(m, "GetDefaultBranch") + ret0, _ := ret[0].(*clients.BranchRef) ret1, _ := ret[1].(error) return ret0, ret1 } -// ListCheckRunsForRef indicates an expected call of ListCheckRunsForRef. -func (mr *MockRepoClientMockRecorder) ListCheckRunsForRef(ref interface{}) *gomock.Call { +// GetDefaultBranch indicates an expected call of GetDefaultBranch +func (mr *MockRepoClientMockRecorder) GetDefaultBranch() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCheckRunsForRef", reflect.TypeOf((*MockRepoClient)(nil).ListCheckRunsForRef), ref) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultBranch", reflect.TypeOf((*MockRepoClient)(nil).GetDefaultBranch)) } -// ListCommits mocks base method. +// ListCommits mocks base method func (m *MockRepoClient) ListCommits() ([]clients.Commit, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListCommits") @@ -161,73 +160,88 @@ func (m *MockRepoClient) ListCommits() ([]clients.Commit, error) { return ret0, ret1 } -// ListCommits indicates an expected call of ListCommits. +// ListCommits indicates an expected call of ListCommits func (mr *MockRepoClientMockRecorder) ListCommits() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCommits", reflect.TypeOf((*MockRepoClient)(nil).ListCommits)) } -// ListContributors mocks base method. -func (m *MockRepoClient) ListContributors() ([]clients.Contributor, error) { +// ListIssues mocks base method +func (m *MockRepoClient) ListIssues() ([]clients.Issue, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListContributors") - ret0, _ := ret[0].([]clients.Contributor) + ret := m.ctrl.Call(m, "ListIssues") + ret0, _ := ret[0].([]clients.Issue) ret1, _ := ret[1].(error) return ret0, ret1 } -// ListContributors indicates an expected call of ListContributors. -func (mr *MockRepoClientMockRecorder) ListContributors() *gomock.Call { +// ListIssues indicates an expected call of ListIssues +func (mr *MockRepoClientMockRecorder) ListIssues() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListContributors", reflect.TypeOf((*MockRepoClient)(nil).ListContributors)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListIssues", reflect.TypeOf((*MockRepoClient)(nil).ListIssues)) } -// ListFiles mocks base method. -func (m *MockRepoClient) ListFiles(predicate func(string) (bool, error)) ([]string, error) { +// ListReleases mocks base method +func (m *MockRepoClient) ListReleases() ([]clients.Release, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListFiles", predicate) - ret0, _ := ret[0].([]string) + ret := m.ctrl.Call(m, "ListReleases") + ret0, _ := ret[0].([]clients.Release) ret1, _ := ret[1].(error) return ret0, ret1 } -// ListFiles indicates an expected call of ListFiles. -func (mr *MockRepoClientMockRecorder) ListFiles(predicate interface{}) *gomock.Call { +// ListReleases indicates an expected call of ListReleases +func (mr *MockRepoClientMockRecorder) ListReleases() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListFiles", reflect.TypeOf((*MockRepoClient)(nil).ListFiles), predicate) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListReleases", reflect.TypeOf((*MockRepoClient)(nil).ListReleases)) } -// ListIssues mocks base method. -func (m *MockRepoClient) ListIssues() ([]clients.Issue, error) { +// ListContributors mocks base method +func (m *MockRepoClient) ListContributors() ([]clients.Contributor, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListIssues") - ret0, _ := ret[0].([]clients.Issue) + ret := m.ctrl.Call(m, "ListContributors") + ret0, _ := ret[0].([]clients.Contributor) ret1, _ := ret[1].(error) return ret0, ret1 } -// ListIssues indicates an expected call of ListIssues. -func (mr *MockRepoClientMockRecorder) ListIssues() *gomock.Call { +// ListContributors indicates an expected call of ListContributors +func (mr *MockRepoClientMockRecorder) ListContributors() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListIssues", reflect.TypeOf((*MockRepoClient)(nil).ListIssues)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListContributors", reflect.TypeOf((*MockRepoClient)(nil).ListContributors)) } -// ListReleases mocks base method. -func (m *MockRepoClient) ListReleases() ([]clients.Release, error) { +// ListSuccessfulWorkflowRuns mocks base method +func (m *MockRepoClient) ListSuccessfulWorkflowRuns(filename string) ([]clients.WorkflowRun, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListReleases") - ret0, _ := ret[0].([]clients.Release) + ret := m.ctrl.Call(m, "ListSuccessfulWorkflowRuns", filename) + ret0, _ := ret[0].([]clients.WorkflowRun) ret1, _ := ret[1].(error) return ret0, ret1 } -// ListReleases indicates an expected call of ListReleases. -func (mr *MockRepoClientMockRecorder) ListReleases() *gomock.Call { +// ListSuccessfulWorkflowRuns indicates an expected call of ListSuccessfulWorkflowRuns +func (mr *MockRepoClientMockRecorder) ListSuccessfulWorkflowRuns(filename interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListReleases", reflect.TypeOf((*MockRepoClient)(nil).ListReleases)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSuccessfulWorkflowRuns", reflect.TypeOf((*MockRepoClient)(nil).ListSuccessfulWorkflowRuns), filename) } -// ListStatuses mocks base method. +// ListCheckRunsForRef mocks base method +func (m *MockRepoClient) ListCheckRunsForRef(ref string) ([]clients.CheckRun, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListCheckRunsForRef", ref) + ret0, _ := ret[0].([]clients.CheckRun) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListCheckRunsForRef indicates an expected call of ListCheckRunsForRef +func (mr *MockRepoClientMockRecorder) ListCheckRunsForRef(ref interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCheckRunsForRef", reflect.TypeOf((*MockRepoClient)(nil).ListCheckRunsForRef), ref) +} + +// ListStatuses mocks base method func (m *MockRepoClient) ListStatuses(ref string) ([]clients.Status, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListStatuses", ref) @@ -236,28 +250,28 @@ func (m *MockRepoClient) ListStatuses(ref string) ([]clients.Status, error) { return ret0, ret1 } -// ListStatuses indicates an expected call of ListStatuses. +// ListStatuses indicates an expected call of ListStatuses func (mr *MockRepoClientMockRecorder) ListStatuses(ref interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListStatuses", reflect.TypeOf((*MockRepoClient)(nil).ListStatuses), ref) } -// ListSuccessfulWorkflowRuns mocks base method. -func (m *MockRepoClient) ListSuccessfulWorkflowRuns(filename string) ([]clients.WorkflowRun, error) { +// ListWebhooks mocks base method +func (m *MockRepoClient) ListWebhooks() ([]*clients.Webhook, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListSuccessfulWorkflowRuns", filename) - ret0, _ := ret[0].([]clients.WorkflowRun) + ret := m.ctrl.Call(m, "ListWebhooks") + ret0, _ := ret[0].([]*clients.Webhook) ret1, _ := ret[1].(error) return ret0, ret1 } -// ListSuccessfulWorkflowRuns indicates an expected call of ListSuccessfulWorkflowRuns. -func (mr *MockRepoClientMockRecorder) ListSuccessfulWorkflowRuns(filename interface{}) *gomock.Call { +// ListWebhooks indicates an expected call of ListWebhooks +func (mr *MockRepoClientMockRecorder) ListWebhooks() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSuccessfulWorkflowRuns", reflect.TypeOf((*MockRepoClient)(nil).ListSuccessfulWorkflowRuns), filename) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListWebhooks", reflect.TypeOf((*MockRepoClient)(nil).ListWebhooks)) } -// Search mocks base method. +// Search mocks base method func (m *MockRepoClient) Search(request clients.SearchRequest) (clients.SearchResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Search", request) @@ -266,22 +280,22 @@ func (m *MockRepoClient) Search(request clients.SearchRequest) (clients.SearchRe return ret0, ret1 } -// Search indicates an expected call of Search. +// Search indicates an expected call of Search func (mr *MockRepoClientMockRecorder) Search(request interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Search", reflect.TypeOf((*MockRepoClient)(nil).Search), request) } -// URI mocks base method. -func (m *MockRepoClient) URI() string { +// Close mocks base method +func (m *MockRepoClient) Close() error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "URI") - ret0, _ := ret[0].(string) + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) return ret0 } -// URI indicates an expected call of URI. -func (mr *MockRepoClientMockRecorder) URI() *gomock.Call { +// Close indicates an expected call of Close +func (mr *MockRepoClientMockRecorder) Close() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "URI", reflect.TypeOf((*MockRepoClient)(nil).URI)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockRepoClient)(nil).Close)) } diff --git a/clients/repo_client.go b/clients/repo_client.go index 6b2e88904a3f..d743fdfbfbd9 100644 --- a/clients/repo_client.go +++ b/clients/repo_client.go @@ -39,6 +39,7 @@ type RepoClient interface { ListSuccessfulWorkflowRuns(filename string) ([]WorkflowRun, error) ListCheckRunsForRef(ref string) ([]CheckRun, error) ListStatuses(ref string) ([]Status, error) + ListWebhooks() ([]*Webhook, error) Search(request SearchRequest) (SearchResponse, error) Close() error } diff --git a/clients/webhook.go b/clients/webhook.go new file mode 100644 index 000000000000..2e65b496f522 --- /dev/null +++ b/clients/webhook.go @@ -0,0 +1,21 @@ +// Copyright 2022 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 clients + +// Webhook represents VCS Webhook. +type Webhook struct { + ID int64 + HasSecret bool +} diff --git a/cmd/root.go b/cmd/root.go index a2a6d363dc74..45ab212b7c6f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -302,6 +302,12 @@ func checksHavePolicies(sp *spol.ScorecardPolicy, enabledChecks checker.CheckNam } func isSupportedCheck(checkName string, requiredRequestTypes []checker.RequestType) bool { + // TODO: remove this validation when this check is ready to be released + if checkName == checks.CheckWebHooks && !isV6Enabled() { + log.Println("Webhook check not enabled, please set the `SCORECARD_V6` environment variable if you want to use it.") + return false + } + unsupported := checker.ListUnsupported( requiredRequestTypes, checks.AllChecks[checkName].SupportedRequestTypes) diff --git a/docs/checks.md b/docs/checks.md index dd12196e6a79..02366e296a08 100644 --- a/docs/checks.md +++ b/docs/checks.md @@ -588,3 +588,15 @@ possible. **Remediation steps** - Fix the vulnerabilities. The details of each vulnerability can be found on . +## Webhooks + +Risk: `High` (possible service can be accessed by third-party) + +This check validates if the webhook defined in the repository have a token configured to authenticate the origin of the request to make sure that is a trusted request. + + +**Remediation steps** +- Check if your service supports the token authentication. +- If that supports set the secret in the Webhook configuration. +- If does not support, consider implementing this support, more information can be found on . + diff --git a/docs/checks/internal/checks.yaml b/docs/checks/internal/checks.yaml index 67e1deeded1f..64252bf3dc16 100644 --- a/docs/checks/internal/checks.yaml +++ b/docs/checks/internal/checks.yaml @@ -717,3 +717,20 @@ checks: - >- Alternately, create a `LICENSE` directory and add license files with a name that matches your [SPDX license identifier](https://spdx.dev/ids/). + + Webhooks: + risk: High + tags: security, infrastructure + repos: GitHub + short: This check validate if the webhook defined in the repository have a token configured. + description: | + Risk: `High` (possible service can be accessed by third-party) + + This check validates if the webhook defined in the repository have a token configured to authenticate the origin of the request to make sure that is a trusted request. + remediation: + - >- + Check if your service supports the token authentication. + - >- + If that supports set the secret in the Webhook configuration. + - >- + If does not support, consider implementing this support, more information can be found on .