Skip to content

Commit

Permalink
checks: add GitHub Webhook check
Browse files Browse the repository at this point in the history
Signed-off-by: Carlos Panato <[email protected]>
  • Loading branch information
cpanato committed Feb 25, 2022
1 parent 504f134 commit c1d955c
Show file tree
Hide file tree
Showing 14 changed files with 657 additions and 100 deletions.
13 changes: 13 additions & 0 deletions checker/raw_result.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
45 changes: 45 additions & 0 deletions checks/raw/webhook.go
Original file line number Diff line number Diff line change
@@ -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
}
135 changes: 135 additions & 0 deletions checks/raw/webhooks_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
66 changes: 66 additions & 0 deletions checks/webhook.go
Original file line number Diff line number Diff line change
@@ -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))
}
126 changes: 126 additions & 0 deletions checks/webhook_test.go
Original file line number Diff line number Diff line change
@@ -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()
})
}
}
Loading

0 comments on commit c1d955c

Please sign in to comment.