mirror of
https://github.com/ossf/scorecard.git
synced 2024-09-17 11:57:12 +03:00
✨ checks: add GitHub Webhook check (#1675)
* checks: add GitHub Webhook check Signed-off-by: Carlos Panato <ctadeu@gmail.com> * update per feedback Signed-off-by: cpanato <ctadeu@gmail.com> * add evaluation code Signed-off-by: cpanato <ctadeu@gmail.com> * add feature gate check Signed-off-by: cpanato <ctadeu@gmail.com> * fix lint Signed-off-by: cpanato <ctadeu@gmail.com>
This commit is contained in:
parent
93889a8e70
commit
7dcb3cb3e2
@ -25,6 +25,7 @@ type RawResults struct {
|
|||||||
DependencyUpdateToolResults DependencyUpdateToolData
|
DependencyUpdateToolResults DependencyUpdateToolData
|
||||||
BranchProtectionResults BranchProtectionsData
|
BranchProtectionResults BranchProtectionsData
|
||||||
CodeReviewResults CodeReviewData
|
CodeReviewResults CodeReviewData
|
||||||
|
WebhookResults WebhooksData
|
||||||
MaintainedResults MaintainedData
|
MaintainedResults MaintainedData
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,6 +71,20 @@ type DependencyUpdateToolData struct {
|
|||||||
Tools []Tool
|
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 {
|
||||||
|
Path string
|
||||||
|
ID int64
|
||||||
|
UsesAuthSecret bool
|
||||||
|
}
|
||||||
|
|
||||||
// BranchProtectionsData contains the raw results
|
// BranchProtectionsData contains the raw results
|
||||||
// for the Branch-Protection check.
|
// for the Branch-Protection check.
|
||||||
type BranchProtectionsData struct {
|
type BranchProtectionsData struct {
|
||||||
|
60
checks/evaluation/webhooks.go
Normal file
60
checks/evaluation/webhooks.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// Copyright 2021 Security Scorecard Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package evaluation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ossf/scorecard/v4/checker"
|
||||||
|
sce "github.com/ossf/scorecard/v4/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Webhooks applies the score policy for the Webhooks check.
|
||||||
|
func Webhooks(name string, dl checker.DetailLogger,
|
||||||
|
r *checker.WebhooksData,
|
||||||
|
) checker.CheckResult {
|
||||||
|
if r == nil {
|
||||||
|
e := sce.WithMessage(sce.ErrScorecardInternal, "empty raw data")
|
||||||
|
return checker.CreateRuntimeErrorResult(name, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.Webhook) < 1 {
|
||||||
|
return checker.CreateMaxScoreResult(name, "no webhooks defined")
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNoSecretCount := 0
|
||||||
|
for _, hook := range r.Webhook {
|
||||||
|
if !hook.UsesAuthSecret {
|
||||||
|
dl.Warn(&checker.LogMessage{
|
||||||
|
Path: hook.Path,
|
||||||
|
Type: checker.FileTypeURL,
|
||||||
|
Text: "Webhook with no secret configured",
|
||||||
|
})
|
||||||
|
hasNoSecretCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasNoSecretCount == 0 {
|
||||||
|
return checker.CreateMaxScoreResult(name, fmt.Sprintf("all %d hook(s) have a secret configured", len(r.Webhook)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.Webhook) == hasNoSecretCount {
|
||||||
|
return checker.CreateMinScoreResult(name, fmt.Sprintf("%d hook(s) do not have a secret configured", len(r.Webhook)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return checker.CreateProportionalScoreResult(name,
|
||||||
|
fmt.Sprintf("%d/%d hook(s) with no secrets configured detected",
|
||||||
|
hasNoSecretCount, len(r.Webhook)), hasNoSecretCount, len(r.Webhook))
|
||||||
|
}
|
152
checks/evaluation/webhooks_test.go
Normal file
152
checks/evaluation/webhooks_test.go
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
// 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 evaluation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ossf/scorecard/v4/checker"
|
||||||
|
scut "github.com/ossf/scorecard/v4/utests"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestWebhooks tests the webhooks check.
|
||||||
|
func TestWebhooks(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
//nolint
|
||||||
|
type args struct {
|
||||||
|
name string
|
||||||
|
dl checker.DetailLogger
|
||||||
|
r *checker.WebhooksData
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want checker.CheckResult
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "r nil",
|
||||||
|
args: args{
|
||||||
|
name: "test_webhook_check_pass",
|
||||||
|
dl: &scut.TestDetailLogger{},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no webhooks",
|
||||||
|
args: args{
|
||||||
|
name: "no webhooks",
|
||||||
|
dl: &scut.TestDetailLogger{},
|
||||||
|
r: &checker.WebhooksData{},
|
||||||
|
},
|
||||||
|
want: checker.CheckResult{
|
||||||
|
Score: checker.MaxResultScore,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "1 webhook with secret",
|
||||||
|
args: args{
|
||||||
|
name: "1 webhook with secret",
|
||||||
|
dl: &scut.TestDetailLogger{},
|
||||||
|
r: &checker.WebhooksData{
|
||||||
|
Webhook: []checker.WebhookData{
|
||||||
|
{
|
||||||
|
Path: "https://github.com/owner/repo/settings/hooks/1234",
|
||||||
|
ID: 1234,
|
||||||
|
UsesAuthSecret: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: checker.CheckResult{
|
||||||
|
Score: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "1 webhook with no secret",
|
||||||
|
args: args{
|
||||||
|
name: "1 webhook with no secret",
|
||||||
|
dl: &scut.TestDetailLogger{},
|
||||||
|
r: &checker.WebhooksData{
|
||||||
|
Webhook: []checker.WebhookData{
|
||||||
|
{
|
||||||
|
Path: "https://github.com/owner/repo/settings/hooks/1234",
|
||||||
|
ID: 1234,
|
||||||
|
UsesAuthSecret: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: checker.CheckResult{
|
||||||
|
Score: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "many webhooks with no secret and with secret",
|
||||||
|
args: args{
|
||||||
|
name: "many webhooks with no secret and with secret",
|
||||||
|
dl: &scut.TestDetailLogger{},
|
||||||
|
r: &checker.WebhooksData{
|
||||||
|
Webhook: []checker.WebhookData{
|
||||||
|
{
|
||||||
|
Path: "https://github.com/owner/repo/settings/hooks/1234",
|
||||||
|
ID: 1234,
|
||||||
|
UsesAuthSecret: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "https://github.com/owner/repo/settings/hooks/1111",
|
||||||
|
ID: 1111,
|
||||||
|
UsesAuthSecret: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "https://github.com/owner/repo/settings/hooks/4444",
|
||||||
|
ID: 4444,
|
||||||
|
UsesAuthSecret: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "https://github.com/owner/repo/settings/hooks/3333",
|
||||||
|
ID: 3333,
|
||||||
|
UsesAuthSecret: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "https://github.com/owner/repo/settings/hooks/2222",
|
||||||
|
ID: 2222,
|
||||||
|
UsesAuthSecret: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: checker.CheckResult{
|
||||||
|
Score: 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
got := Webhooks(tt.args.name, tt.args.dl, tt.args.r)
|
||||||
|
if tt.wantErr {
|
||||||
|
if got.Error2 == nil {
|
||||||
|
t.Errorf("Webhooks() error = %v, wantErr %v", got.Error2, tt.wantErr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if got.Score != tt.want.Score {
|
||||||
|
t.Errorf("Webhooks() = %v, want %v", got.Score, tt.want.Score)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
48
checks/raw/webhook.go
Normal file
48
checks/raw/webhook.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"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,
|
||||||
|
UsesAuthSecret: hook.UsesAuthSecret,
|
||||||
|
Path: fmt.Sprintf("https://%s/settings/hooks/%d", c.RepoClient.URI(), hook.ID),
|
||||||
|
// Note: add fields if needed.
|
||||||
|
}
|
||||||
|
hooks = append(hooks, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return checker.WebhooksData{Webhook: hooks}, nil
|
||||||
|
}
|
138
checks/raw/webhooks_test.go
Normal file
138
checks/raw/webhooks_test.go
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
// 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
|
||||||
|
uri string
|
||||||
|
wantErr bool
|
||||||
|
expectedUsesAuthSecret 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,
|
||||||
|
expectedUsesAuthSecret: 0,
|
||||||
|
webhookResponse: []*clients.Webhook{
|
||||||
|
{
|
||||||
|
UsesAuthSecret: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Webhook with secrets",
|
||||||
|
wantErr: false,
|
||||||
|
expectedUsesAuthSecret: 2,
|
||||||
|
webhookResponse: []*clients.Webhook{
|
||||||
|
{
|
||||||
|
UsesAuthSecret: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UsesAuthSecret: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Webhook with secrets and some without defined secrets",
|
||||||
|
wantErr: false,
|
||||||
|
expectedUsesAuthSecret: 1,
|
||||||
|
webhookResponse: []*clients.Webhook{
|
||||||
|
{
|
||||||
|
UsesAuthSecret: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UsesAuthSecret: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UsesAuthSecret: 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().URI().Return(tt.uri).AnyTimes()
|
||||||
|
|
||||||
|
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.UsesAuthSecret {
|
||||||
|
gotHasSecret++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotHasSecret != tt.expectedUsesAuthSecret {
|
||||||
|
t.Errorf("Webhooks() got = %v, want %v", gotHasSecret, tt.expectedUsesAuthSecret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !scut.ValidateTestReturn(t, tt.name, &tt.expected, &checker.CheckResult{}, &dl) {
|
||||||
|
t.Fatalf("Test %s failed", tt.name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
65
checks/webhook.go
Normal file
65
checks/webhook.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// 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 (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/ossf/scorecard/v4/checker"
|
||||||
|
"github.com/ossf/scorecard/v4/checks/evaluation"
|
||||||
|
"github.com/ossf/scorecard/v4/checks/raw"
|
||||||
|
sce "github.com/ossf/scorecard/v4/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// 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 Webhooks check.
|
||||||
|
func WebHooks(c *checker.CheckRequest) checker.CheckResult {
|
||||||
|
// TODO: remove this check when v6 is released
|
||||||
|
_, enabled := os.LookupEnv("SCORECARD_V6")
|
||||||
|
if !enabled {
|
||||||
|
c.Dlogger.Warn(&checker.LogMessage{
|
||||||
|
Text: "SCORECARD_V6 is not set, not running the Webhook check",
|
||||||
|
})
|
||||||
|
|
||||||
|
e := sce.WithMessage(sce.ErrorUnsupportedCheck, "SCORECARD_V6 is not set, not running the Webhook check")
|
||||||
|
return checker.CreateRuntimeErrorResult(CheckWebHooks, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawData, err := raw.WebHook(c)
|
||||||
|
if err != nil {
|
||||||
|
e := sce.WithMessage(sce.ErrScorecardInternal, err.Error())
|
||||||
|
return checker.CreateRuntimeErrorResult(CheckWebHooks, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the raw results.
|
||||||
|
if c.RawResults != nil {
|
||||||
|
c.RawResults.WebhookResults = rawData
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the score evaluation.
|
||||||
|
return evaluation.Webhooks(CheckWebHooks, c.Dlogger, &rawData)
|
||||||
|
}
|
142
checks/webhook_test.go
Normal file
142
checks/webhook_test.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
// 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"
|
||||||
|
"os"
|
||||||
|
"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"
|
||||||
|
scut "github.com/ossf/scorecard/v4/utests"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWebhooks(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
tests := []struct {
|
||||||
|
expected checker.CheckResult
|
||||||
|
uri string
|
||||||
|
err error
|
||||||
|
name string
|
||||||
|
webhooks []*clients.Webhook
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "No Webhooks",
|
||||||
|
uri: "github.com/owner/repo",
|
||||||
|
expected: checker.CheckResult{
|
||||||
|
Pass: true,
|
||||||
|
Score: 10,
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
webhooks: []*clients.Webhook{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "With Webhooks and secret set",
|
||||||
|
uri: "github.com/owner/repo",
|
||||||
|
expected: checker.CheckResult{
|
||||||
|
Pass: true,
|
||||||
|
Score: 10,
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
webhooks: []*clients.Webhook{
|
||||||
|
{
|
||||||
|
ID: 12345,
|
||||||
|
UsesAuthSecret: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "With Webhooks and no secret set",
|
||||||
|
uri: "github.com/owner/repo",
|
||||||
|
expected: checker.CheckResult{
|
||||||
|
Pass: false,
|
||||||
|
Score: 0,
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
webhooks: []*clients.Webhook{
|
||||||
|
{
|
||||||
|
ID: 12345,
|
||||||
|
UsesAuthSecret: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "With 2 Webhooks with and whitout secrets configured",
|
||||||
|
uri: "github.com/owner/repo",
|
||||||
|
expected: checker.CheckResult{
|
||||||
|
Pass: false,
|
||||||
|
Score: 5,
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
webhooks: []*clients.Webhook{
|
||||||
|
{
|
||||||
|
ID: 12345,
|
||||||
|
UsesAuthSecret: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 54321,
|
||||||
|
UsesAuthSecret: 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()
|
||||||
|
|
||||||
|
os.Setenv("SCORECARD_V6", "true")
|
||||||
|
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)
|
||||||
|
|
||||||
|
mockRepo.EXPECT().URI().Return(tt.uri).AnyTimes()
|
||||||
|
|
||||||
|
dl := scut.TestDetailLogger{}
|
||||||
|
req := checker.CheckRequest{
|
||||||
|
RepoClient: mockRepo,
|
||||||
|
Ctx: context.TODO(),
|
||||||
|
Dlogger: &dl,
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -45,6 +45,7 @@ type Client struct {
|
|||||||
checkruns *checkrunsHandler
|
checkruns *checkrunsHandler
|
||||||
statuses *statusesHandler
|
statuses *statusesHandler
|
||||||
search *searchHandler
|
search *searchHandler
|
||||||
|
webhook *webhookHandler
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
tarball tarballHandler
|
tarball tarballHandler
|
||||||
}
|
}
|
||||||
@ -97,6 +98,9 @@ func (client *Client) InitRepo(inputRepo clients.Repo, commitSHA string) error {
|
|||||||
// Setup searchHandler.
|
// Setup searchHandler.
|
||||||
client.search.init(client.ctx, client.repourl)
|
client.search.init(client.ctx, client.repourl)
|
||||||
|
|
||||||
|
// Setup webhookHandler.
|
||||||
|
client.webhook.init(client.ctx, client.repourl)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,6 +154,11 @@ func (client *Client) ListBranches() ([]*clients.BranchRef, error) {
|
|||||||
return client.branches.listBranches()
|
return client.branches.listBranches()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListWebhooks implements RepoClient.ListWebhooks.
|
||||||
|
func (client *Client) ListWebhooks() ([]*clients.Webhook, error) {
|
||||||
|
return client.webhook.listWebhooks()
|
||||||
|
}
|
||||||
|
|
||||||
// ListSuccessfulWorkflowRuns implements RepoClient.WorkflowRunsByFilename.
|
// ListSuccessfulWorkflowRuns implements RepoClient.WorkflowRunsByFilename.
|
||||||
func (client *Client) ListSuccessfulWorkflowRuns(filename string) ([]clients.WorkflowRun, error) {
|
func (client *Client) ListSuccessfulWorkflowRuns(filename string) ([]clients.WorkflowRun, error) {
|
||||||
return client.workflows.listSuccessfulWorkflowRuns(filename)
|
return client.workflows.listSuccessfulWorkflowRuns(filename)
|
||||||
@ -211,6 +220,9 @@ func CreateGithubRepoClientWithTransport(ctx context.Context, rt http.RoundTripp
|
|||||||
search: &searchHandler{
|
search: &searchHandler{
|
||||||
ghClient: client,
|
ghClient: client,
|
||||||
},
|
},
|
||||||
|
webhook: &webhookHandler{
|
||||||
|
ghClient: client,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
84
clients/githubrepo/webhook.go
Normal file
84
clients/githubrepo/webhook.go
Normal file
@ -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(),
|
||||||
|
UsesAuthSecret: getAuthSecret(hook.Config),
|
||||||
|
}
|
||||||
|
handler.webhook = append(handler.webhook, repoHook)
|
||||||
|
}
|
||||||
|
handler.errSetup = nil
|
||||||
|
})
|
||||||
|
return handler.errSetup
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAuthSecret(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
|
||||||
|
}
|
@ -201,6 +201,11 @@ func (client *localDirClient) ListStatuses(ref string) ([]clients.Status, error)
|
|||||||
return nil, fmt.Errorf("ListStatuses: %w", clients.ErrUnsupportedFeature)
|
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.
|
// Search implements RepoClient.Search.
|
||||||
func (client *localDirClient) Search(request clients.SearchRequest) (clients.SearchResponse, error) {
|
func (client *localDirClient) Search(request clients.SearchRequest) (clients.SearchResponse, error) {
|
||||||
return clients.SearchResponse{}, fmt.Errorf("Search: %w", clients.ErrUnsupportedFeature)
|
return clients.SearchResponse{}, fmt.Errorf("Search: %w", clients.ErrUnsupportedFeature)
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
package mockrepo
|
package mockrepo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
gomock "github.com/golang/mock/gomock"
|
||||||
@ -155,11 +154,8 @@ func (mr *MockRepoClientMockRecorder) ListCheckRunsForRef(ref interface{}) *gomo
|
|||||||
|
|
||||||
// ListCommits mocks base method.
|
// ListCommits mocks base method.
|
||||||
func (m *MockRepoClient) ListCommits() ([]clients.Commit, error) {
|
func (m *MockRepoClient) ListCommits() ([]clients.Commit, error) {
|
||||||
fmt.Println("mock.ListCommits")
|
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
fmt.Println("call mock.ListCommits")
|
|
||||||
ret := m.ctrl.Call(m, "ListCommits")
|
ret := m.ctrl.Call(m, "ListCommits")
|
||||||
fmt.Println("ret mock.ListCommits")
|
|
||||||
ret0, _ := ret[0].([]clients.Commit)
|
ret0, _ := ret[0].([]clients.Commit)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
@ -167,7 +163,6 @@ func (m *MockRepoClient) ListCommits() ([]clients.Commit, error) {
|
|||||||
|
|
||||||
// ListCommits indicates an expected call of ListCommits.
|
// ListCommits indicates an expected call of ListCommits.
|
||||||
func (mr *MockRepoClientMockRecorder) ListCommits() *gomock.Call {
|
func (mr *MockRepoClientMockRecorder) ListCommits() *gomock.Call {
|
||||||
fmt.Println("recorder.ListCommits")
|
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCommits", reflect.TypeOf((*MockRepoClient)(nil).ListCommits))
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCommits", reflect.TypeOf((*MockRepoClient)(nil).ListCommits))
|
||||||
}
|
}
|
||||||
@ -262,6 +257,21 @@ func (mr *MockRepoClientMockRecorder) ListSuccessfulWorkflowRuns(filename interf
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSuccessfulWorkflowRuns", reflect.TypeOf((*MockRepoClient)(nil).ListSuccessfulWorkflowRuns), filename)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSuccessfulWorkflowRuns", reflect.TypeOf((*MockRepoClient)(nil).ListSuccessfulWorkflowRuns), filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListWebhooks mocks base method.
|
||||||
|
func (m *MockRepoClient) ListWebhooks() ([]*clients.Webhook, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "ListWebhooks")
|
||||||
|
ret0, _ := ret[0].([]*clients.Webhook)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, "ListWebhooks", reflect.TypeOf((*MockRepoClient)(nil).ListWebhooks))
|
||||||
|
}
|
||||||
|
|
||||||
// Search mocks base method.
|
// Search mocks base method.
|
||||||
func (m *MockRepoClient) Search(request clients.SearchRequest) (clients.SearchResponse, error) {
|
func (m *MockRepoClient) Search(request clients.SearchRequest) (clients.SearchResponse, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
@ -41,6 +41,7 @@ type RepoClient interface {
|
|||||||
ListSuccessfulWorkflowRuns(filename string) ([]WorkflowRun, error)
|
ListSuccessfulWorkflowRuns(filename string) ([]WorkflowRun, error)
|
||||||
ListCheckRunsForRef(ref string) ([]CheckRun, error)
|
ListCheckRunsForRef(ref string) ([]CheckRun, error)
|
||||||
ListStatuses(ref string) ([]Status, error)
|
ListStatuses(ref string) ([]Status, error)
|
||||||
|
ListWebhooks() ([]*Webhook, error)
|
||||||
Search(request SearchRequest) (SearchResponse, error)
|
Search(request SearchRequest) (SearchResponse, error)
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
22
clients/webhook.go
Normal file
22
clients/webhook.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// 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 {
|
||||||
|
Path string
|
||||||
|
ID int64
|
||||||
|
UsesAuthSecret bool
|
||||||
|
}
|
@ -601,3 +601,13 @@ possible.
|
|||||||
**Remediation steps**
|
**Remediation steps**
|
||||||
- Fix the vulnerabilities. The details of each vulnerability can be found on <https://osv.dev>.
|
- Fix the vulnerabilities. The details of each vulnerability can be found on <https://osv.dev>.
|
||||||
|
|
||||||
|
## Webhooks
|
||||||
|
|
||||||
|
Risk: `Critical` (service possibly accessible to third parties)
|
||||||
|
|
||||||
|
This check determines whether the webhook defined in the repository has a token configured to authenticate the origins of requests.
|
||||||
|
|
||||||
|
**Remediation steps**
|
||||||
|
- Check whether your service supports token authentication.
|
||||||
|
- If there is support for token authentication, set the secret in the webhook configuration. See [Setting up a webhook](https://docs.github.com/en/developers/webhooks-and-events/webhooks/creating-webhooks#setting-up-a-webhook)
|
||||||
|
- If there is no support for token authentication, consider implementing it by following [these directions](https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks).
|
||||||
|
@ -731,3 +731,20 @@ checks:
|
|||||||
- >-
|
- >-
|
||||||
Alternately, create a `LICENSE` directory and add license files with a name
|
Alternately, create a `LICENSE` directory and add license files with a name
|
||||||
that matches your [SPDX license identifier](https://spdx.dev/ids/).
|
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: `Critical` (service possibly accessible to third parties)
|
||||||
|
|
||||||
|
This check determines whether the webhook defined in the repository has a token configured to authenticate the origins of requests.
|
||||||
|
remediation:
|
||||||
|
- >-
|
||||||
|
Check whether your service supports token authentication.
|
||||||
|
- >-
|
||||||
|
If there is support for token authentication, set the secret in the webhook configuration. See [Setting up a webhook](https://docs.github.com/en/developers/webhooks-and-events/webhooks/creating-webhooks#setting-up-a-webhook)
|
||||||
|
- >-
|
||||||
|
If there is no support for token authentication, consider implementing it by following [these directions](https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks).
|
||||||
|
Loading…
Reference in New Issue
Block a user