mirror of
https://github.com/ossf/scorecard.git
synced 2024-09-11 08:55:27 +03:00
🌱 Add probes for Branch Protection (#3691)
* 🌱 Add probes for Branch Protection
Signed-off-by: AdamKorcz <adam@adalogics.com>
* specify that Scorecard only considers default and releases branches
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* reduce duplication in blocksDeleteOnBranches
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* use helper to test for boolean values
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* Fix typo, mention OutcomeNotAvailable
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* fix typo and elaborate on effort
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* fix typo. Specify which branches the probe considers
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* Fix copy paste typo
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* remove '/en' from url
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* change effort from 'High' to 'Low' in the blocksForcePushOnBranches probe def
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* fix remediation level
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* Change probe package name
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* improve probe definitions
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* refactor test names
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* Change motivation of two probes
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* downgrade effort of runsStatusChecksBeforeMerging
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* reduce complexity of blocksForcePushOnBranches
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* simplify requiresCodeOwnersReview logic
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* fix linter issues
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* fix copy paste error
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* differentiate trueMsg and falseMsg in requiresApproversForPullRequests
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* fix text in requiresCodeOwnersReview
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* change outcome in utils
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* fix lint issues
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* fix nit in text
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* use standardized messages
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* remove 'Uint32LargerThan0'
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* Add number of required reviewers to values. Refactor to avoid nil-dereference
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* fix nit log message
Signed-off-by: Adam Korczynski <adam@adalogics.com>
---------
Signed-off-by: AdamKorcz <adam@adalogics.com>
Signed-off-by: Adam Korczynski <adam@adalogics.com>
This commit is contained in:
parent
c1a0557dbf
commit
2e1059bb76
27
probes/blocksDeleteOnBranches/def.yml
Normal file
27
probes/blocksDeleteOnBranches/def.yml
Normal file
@ -0,0 +1,27 @@
|
||||
# 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: blocksDeleteOnBranches
|
||||
short: Check that the project blocks non-admins from deleting branches.
|
||||
motivation: >
|
||||
Allowing non-admins to delete project branches has a similar effect to performing force pushes.
|
||||
implementation: >
|
||||
Checks the protection rules of default and release branches.
|
||||
outcome:
|
||||
- The probe returns one OutcomePositive for each branch that is disallowed from users deleting it, and one OutcomeNegative for branches where users are able to delete it. Scorecard only considers default and releases branches.
|
||||
remediation:
|
||||
effort: Low
|
||||
text:
|
||||
- Disallow deletion of branches in your project to remove negative outcomes.
|
||||
- GitHub and Gitlab by default disable deleting a protected branch.
|
67
probes/blocksDeleteOnBranches/impl.go
Normal file
67
probes/blocksDeleteOnBranches/impl.go
Normal file
@ -0,0 +1,67 @@
|
||||
// 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.
|
||||
|
||||
//nolint:stylecheck
|
||||
package blocksDeleteOnBranches
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
"github.com/ossf/scorecard/v4/probes/internal/utils/uerror"
|
||||
)
|
||||
|
||||
//go:embed *.yml
|
||||
var fs embed.FS
|
||||
|
||||
const Probe = "blocksDeleteOnBranches"
|
||||
|
||||
func Run(raw *checker.RawResults) ([]finding.Finding, string, error) {
|
||||
if raw == nil {
|
||||
return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil)
|
||||
}
|
||||
|
||||
r := raw.BranchProtectionResults
|
||||
var findings []finding.Finding
|
||||
|
||||
for i := range r.Branches {
|
||||
branch := &r.Branches[i]
|
||||
|
||||
var text string
|
||||
var outcome finding.Outcome
|
||||
switch {
|
||||
case branch.BranchProtectionRule.AllowDeletions == nil:
|
||||
text = "could not determine whether branch is protected against deletion"
|
||||
outcome = finding.OutcomeNotAvailable
|
||||
case *branch.BranchProtectionRule.AllowDeletions:
|
||||
text = fmt.Sprintf("'allow deletion' enabled on branch '%s'", *branch.Name)
|
||||
outcome = finding.OutcomeNegative
|
||||
case !*branch.BranchProtectionRule.AllowDeletions:
|
||||
text = fmt.Sprintf("'allow deletion' disabled on branch '%s'", *branch.Name)
|
||||
outcome = finding.OutcomePositive
|
||||
default:
|
||||
}
|
||||
f, err := finding.NewWith(fs, Probe, text, nil, outcome)
|
||||
if err != nil {
|
||||
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||
}
|
||||
f = f.WithValues(map[string]int{
|
||||
*branch.Name: 1,
|
||||
})
|
||||
findings = append(findings, *f)
|
||||
}
|
||||
return findings, Probe, nil
|
||||
}
|
185
probes/blocksDeleteOnBranches/impl_test.go
Normal file
185
probes/blocksDeleteOnBranches/impl_test.go
Normal file
@ -0,0 +1,185 @@
|
||||
// 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.
|
||||
|
||||
//nolint:stylecheck
|
||||
package blocksDeleteOnBranches
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
)
|
||||
|
||||
func Test_Run(t *testing.T) {
|
||||
t.Parallel()
|
||||
trueVal := true
|
||||
falseVal := false
|
||||
branchVal1 := "branch-name1"
|
||||
branchVal2 := "branch-name1"
|
||||
|
||||
//nolint:govet
|
||||
tests := []struct {
|
||||
name string
|
||||
raw *checker.RawResults
|
||||
outcomes []finding.Outcome
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "One branch blocks branch deletion should result in one positive outcome",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
AllowDeletions: &falseVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Two branches that block branch deletions should result in two positive outcomes",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
AllowDeletions: &falseVal,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
AllowDeletions: &falseVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive, finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Two branches in total: One blocks branch deletion and one doesn't = 1 positive & 1 negative",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
AllowDeletions: &falseVal,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
AllowDeletions: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive, finding.OutcomeNegative,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Two branches in total: One blocks branch deletion and one doesn't = 1 negative & 1 positive",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
AllowDeletions: &trueVal,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
AllowDeletions: &falseVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative, finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Two branches in total: One blocks branch deletion and one lacks data = 1 negative & 1 unavailable",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
AllowDeletions: &trueVal,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
AllowDeletions: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative, finding.OutcomeNotAvailable,
|
||||
},
|
||||
},
|
||||
}
|
||||
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()
|
||||
|
||||
findings, s, err := Run(tt.raw)
|
||||
if !cmp.Equal(tt.err, err, cmpopts.EquateErrors()) {
|
||||
t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(tt.err, err, cmpopts.EquateErrors()))
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(Probe, s); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(len(tt.outcomes), len(findings)); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
for i := range tt.outcomes {
|
||||
outcome := &tt.outcomes[i]
|
||||
f := &findings[i]
|
||||
if diff := cmp.Diff(*outcome, f.Outcome); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
33
probes/blocksForcePushOnBranches/def.yml
Normal file
33
probes/blocksForcePushOnBranches/def.yml
Normal file
@ -0,0 +1,33 @@
|
||||
# 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: blocksForcePushOnBranches
|
||||
short: Check that the project blocks force push on its branches.
|
||||
motivation: >
|
||||
Allowing non-admins to force push to branches could allow untrusted users to make insecure changes to the behavior of the project.
|
||||
implementation: >
|
||||
Checks the protection rules of default and release branches.
|
||||
outcome:
|
||||
- The probe returns one OutcomePositive for each branch that is blocked from force pushes, and one OutcomeNegative for branches that allows force push.
|
||||
- Returns OutcomeNotAvailable if Scorecard cannot fetch the data from the repository.
|
||||
remediation:
|
||||
effort: Low
|
||||
text:
|
||||
- Disallow force pushes branches in your project to remove negative outcomes.
|
||||
- For GitHub-hosted projects, force pushes are disabled by default. To make sure it has not been enabled, see ["Allow force pushes"](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#allow-force-pushes).
|
||||
- For Gitlab-hosted projects, follow the ["Protected branches"](https://docs.gitlab.com/ee/user/project/protected_branches.html) documentation to see who can force push to the project.
|
||||
markdown:
|
||||
- Disallow force pushes branches in your project to remove negative outcomes.
|
||||
- For GitHub-hosted projects, force pushes are disabled by default. To make sure it has not been enabled, see ["Allow force pushes"](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#allow-force-pushes).
|
||||
- For Gitlab-hosted projects, follow the ["Protected branches"](https://docs.gitlab.com/ee/user/project/protected_branches.html) documentation to see who can force push to the project.
|
66
probes/blocksForcePushOnBranches/impl.go
Normal file
66
probes/blocksForcePushOnBranches/impl.go
Normal file
@ -0,0 +1,66 @@
|
||||
// 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.
|
||||
|
||||
//nolint:stylecheck
|
||||
package blocksForcePushOnBranches
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
"github.com/ossf/scorecard/v4/probes/internal/utils/uerror"
|
||||
)
|
||||
|
||||
//go:embed *.yml
|
||||
var fs embed.FS
|
||||
|
||||
const Probe = "blocksForcePushOnBranches"
|
||||
|
||||
func Run(raw *checker.RawResults) ([]finding.Finding, string, error) {
|
||||
if raw == nil {
|
||||
return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil)
|
||||
}
|
||||
|
||||
r := raw.BranchProtectionResults
|
||||
var findings []finding.Finding
|
||||
|
||||
for i := range r.Branches {
|
||||
branch := &r.Branches[i]
|
||||
var text string
|
||||
var outcome finding.Outcome
|
||||
switch {
|
||||
case branch.BranchProtectionRule.AllowForcePushes == nil:
|
||||
text = "could not determine whether for push is allowed"
|
||||
outcome = finding.OutcomeNotAvailable
|
||||
case *branch.BranchProtectionRule.AllowForcePushes:
|
||||
text = fmt.Sprintf("'force pushes' enabled on branch '%s'", *branch.Name)
|
||||
outcome = finding.OutcomeNegative
|
||||
case !*branch.BranchProtectionRule.AllowForcePushes:
|
||||
text = fmt.Sprintf("'force pushes' disabled on branch '%s'", *branch.Name)
|
||||
outcome = finding.OutcomePositive
|
||||
default:
|
||||
}
|
||||
f, err := finding.NewWith(fs, Probe, text, nil, outcome)
|
||||
if err != nil {
|
||||
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||
}
|
||||
f = f.WithValues(map[string]int{
|
||||
*branch.Name: 1,
|
||||
})
|
||||
findings = append(findings, *f)
|
||||
}
|
||||
return findings, Probe, nil
|
||||
}
|
185
probes/blocksForcePushOnBranches/impl_test.go
Normal file
185
probes/blocksForcePushOnBranches/impl_test.go
Normal file
@ -0,0 +1,185 @@
|
||||
// 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.
|
||||
|
||||
//nolint:stylecheck
|
||||
package blocksForcePushOnBranches
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
)
|
||||
|
||||
func Test_Run(t *testing.T) {
|
||||
t.Parallel()
|
||||
trueVal := true
|
||||
falseVal := false
|
||||
branchVal1 := "branch-name1"
|
||||
branchVal2 := "branch-name1"
|
||||
|
||||
//nolint:govet
|
||||
tests := []struct {
|
||||
name string
|
||||
raw *checker.RawResults
|
||||
outcomes []finding.Outcome
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "Blocks Force Push on 1/1 branches = 1 positive outcome",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
AllowForcePushes: &falseVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Blocks Force Push on 2/2 branches = 2 positive outcomes",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
AllowForcePushes: &falseVal,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
AllowForcePushes: &falseVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive, finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Blocks Force Push on 1/2 branches = 1 positive and 1 negative outcome",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
AllowForcePushes: &falseVal,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
AllowForcePushes: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive, finding.OutcomeNegative,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Blocks Force Push on 1/2 branches = 1 negative and 1 positive outcome",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
AllowForcePushes: &trueVal,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
AllowForcePushes: &falseVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative, finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Blocks Force Push on 0/2 branches, 1 branch lacks data = 1 negative and 1 unavailable",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
AllowForcePushes: &trueVal,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
AllowForcePushes: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative, finding.OutcomeNotAvailable,
|
||||
},
|
||||
},
|
||||
}
|
||||
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()
|
||||
|
||||
findings, s, err := Run(tt.raw)
|
||||
if !cmp.Equal(tt.err, err, cmpopts.EquateErrors()) {
|
||||
t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(tt.err, err, cmpopts.EquateErrors()))
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(Probe, s); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(len(tt.outcomes), len(findings)); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
for i := range tt.outcomes {
|
||||
outcome := &tt.outcomes[i]
|
||||
f := &findings[i]
|
||||
if diff := cmp.Diff(*outcome, f.Outcome); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
34
probes/branchProtectionAppliesToAdmins/def.yml
Normal file
34
probes/branchProtectionAppliesToAdmins/def.yml
Normal file
@ -0,0 +1,34 @@
|
||||
# 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: branchProtectionAppliesToAdmins
|
||||
short: Check that the projects branch protection rules apply to project admins.
|
||||
motivation: >
|
||||
Admins can make malicious code changes that users will be unaware of (see CVE-2022-23812); consuming software where this is disabled is encouraged.
|
||||
implementation: >
|
||||
Checks the protection rules of default and release branches.
|
||||
outcome:
|
||||
- The probe returns one OutcomePositive for each branch that enforces branch protection rules on admins, and one OutcomeNegative for branches that don't.
|
||||
remediation:
|
||||
effort: Medium
|
||||
text:
|
||||
- The remediation effort can be Low to High dependening on other branch protection settings.
|
||||
- Enforce protection rules for admins on all branches.
|
||||
- For GitHub-hosted projects, see the ["Do not allow bypassing the above settings"](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#do-not-allow-bypassing-the-above-settings) section in the GitHub docs.
|
||||
- For Gitlab-hosted projects, see the ["Protected branches"](https://docs.gitlab.com/ee/user/project/protected_branches.html) documentation.
|
||||
markdown:
|
||||
- The remediation effort can be Low to High dependening on other branch protection settings.
|
||||
- Enforce protection rules for admins on all branches.
|
||||
- For GitHub-hosted projects, see the ["Do not allow bypassing the above settings"](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#do-not-allow-bypassing-the-above-settings) section in the GitHub docs.
|
||||
- For Gitlab-hosted projects, see the ["Protected branches"](https://docs.gitlab.com/ee/user/project/protected_branches.html) documentation.
|
61
probes/branchProtectionAppliesToAdmins/impl.go
Normal file
61
probes/branchProtectionAppliesToAdmins/impl.go
Normal file
@ -0,0 +1,61 @@
|
||||
// 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.
|
||||
|
||||
//nolint:stylecheck
|
||||
package branchProtectionAppliesToAdmins
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
"github.com/ossf/scorecard/v4/probes/internal/utils/branchprotection"
|
||||
"github.com/ossf/scorecard/v4/probes/internal/utils/uerror"
|
||||
)
|
||||
|
||||
//go:embed *.yml
|
||||
var fs embed.FS
|
||||
|
||||
const Probe = "branchProtectionAppliesToAdmins"
|
||||
|
||||
func Run(raw *checker.RawResults) ([]finding.Finding, string, error) {
|
||||
if raw == nil {
|
||||
return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil)
|
||||
}
|
||||
|
||||
r := raw.BranchProtectionResults
|
||||
var findings []finding.Finding
|
||||
|
||||
for i := range r.Branches {
|
||||
branch := &r.Branches[i]
|
||||
|
||||
p := branch.BranchProtectionRule.EnforceAdmins
|
||||
text, outcome, err := branchprotection.GetTextOutcomeFromBool(p,
|
||||
"branch protection settings apply to administrators",
|
||||
*branch.Name)
|
||||
if err != nil {
|
||||
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||
}
|
||||
f, err := finding.NewWith(fs, Probe, text, nil, outcome)
|
||||
if err != nil {
|
||||
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||
}
|
||||
f = f.WithValues(map[string]int{
|
||||
*branch.Name: 1,
|
||||
})
|
||||
findings = append(findings, *f)
|
||||
}
|
||||
return findings, Probe, nil
|
||||
}
|
185
probes/branchProtectionAppliesToAdmins/impl_test.go
Normal file
185
probes/branchProtectionAppliesToAdmins/impl_test.go
Normal file
@ -0,0 +1,185 @@
|
||||
// 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.
|
||||
|
||||
//nolint:stylecheck
|
||||
package branchProtectionAppliesToAdmins
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
)
|
||||
|
||||
func Test_Run(t *testing.T) {
|
||||
t.Parallel()
|
||||
trueVal := true
|
||||
falseVal := false
|
||||
branchVal1 := "branch-name1"
|
||||
branchVal2 := "branch-name1"
|
||||
|
||||
//nolint:govet
|
||||
tests := []struct {
|
||||
name string
|
||||
raw *checker.RawResults
|
||||
outcomes []finding.Outcome
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "1 branch enforces protection rules on admins = 1 positive outcome",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
EnforceAdmins: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "1 branch enforces protection rules on admins = 2 positive outcomes",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
EnforceAdmins: &trueVal,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
EnforceAdmins: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive, finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "1 branch enforces protection rules on admins and 1 doesn't = 1 positive & 1 negative",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
EnforceAdmins: &trueVal,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
EnforceAdmins: &falseVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive, finding.OutcomeNegative,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "1 branch does not enforce protection rules on admins and 1 does = 1 negative & 1 positive",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
EnforceAdmins: &falseVal,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
EnforceAdmins: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative, finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "1 branch does not enforce protection rules on admins and 1 doesn't have data = 1 negative & 1 unavailable",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
EnforceAdmins: &falseVal,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
EnforceAdmins: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative, finding.OutcomeNotAvailable,
|
||||
},
|
||||
},
|
||||
}
|
||||
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()
|
||||
|
||||
findings, s, err := Run(tt.raw)
|
||||
if !cmp.Equal(tt.err, err, cmpopts.EquateErrors()) {
|
||||
t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(tt.err, err, cmpopts.EquateErrors()))
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(Probe, s); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(len(tt.outcomes), len(findings)); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
for i := range tt.outcomes {
|
||||
outcome := &tt.outcomes[i]
|
||||
f := &findings[i]
|
||||
if diff := cmp.Diff(*outcome, f.Outcome); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
28
probes/dismissesStaleReviews/def.yml
Normal file
28
probes/dismissesStaleReviews/def.yml
Normal file
@ -0,0 +1,28 @@
|
||||
# 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: dismissesStaleReviews
|
||||
short: Check that the project dismisses stale reviews when new commits are pushed.
|
||||
motivation: >
|
||||
When a project does not dismiss stale reviews, contributors can bring their pull requests to an approved state and then make malicious commits.
|
||||
implementation: >
|
||||
Checks the protection rules of default and release branches.
|
||||
outcome:
|
||||
- The probe returns one OutcomePositive for each branch that dismisses the stale status of PRs, and one OutcomeNegative for branches that don't.
|
||||
remediation:
|
||||
effort: High
|
||||
text:
|
||||
- Configure your repository so that the stale status of PRs is dismissed when users make new commits.
|
||||
- For GitHub-hosted projects, see ["Require pull request reviews before merging"](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#require-pull-request-reviews-before-merging).
|
||||
- For Gitlab-hosted projects, see ["Remove all approvals when commits are added to the source branch"](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/settings.html#remove-all-approvals-when-commits-are-added-to-the-source-branch).
|
61
probes/dismissesStaleReviews/impl.go
Normal file
61
probes/dismissesStaleReviews/impl.go
Normal file
@ -0,0 +1,61 @@
|
||||
// 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.
|
||||
|
||||
//nolint:stylecheck
|
||||
package dismissesStaleReviews
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
"github.com/ossf/scorecard/v4/probes/internal/utils/branchprotection"
|
||||
"github.com/ossf/scorecard/v4/probes/internal/utils/uerror"
|
||||
)
|
||||
|
||||
//go:embed *.yml
|
||||
var fs embed.FS
|
||||
|
||||
const Probe = "dismissesStaleReviews"
|
||||
|
||||
func Run(raw *checker.RawResults) ([]finding.Finding, string, error) {
|
||||
if raw == nil {
|
||||
return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil)
|
||||
}
|
||||
|
||||
r := raw.BranchProtectionResults
|
||||
var findings []finding.Finding
|
||||
|
||||
for i := range r.Branches {
|
||||
branch := &r.Branches[i]
|
||||
|
||||
p := branch.BranchProtectionRule.RequiredPullRequestReviews.DismissStaleReviews
|
||||
text, outcome, err := branchprotection.GetTextOutcomeFromBool(p,
|
||||
"stale review dismissal",
|
||||
*branch.Name)
|
||||
if err != nil {
|
||||
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||
}
|
||||
f, err := finding.NewWith(fs, Probe, text, nil, outcome)
|
||||
if err != nil {
|
||||
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||
}
|
||||
f = f.WithValues(map[string]int{
|
||||
*branch.Name: 1,
|
||||
})
|
||||
findings = append(findings, *f)
|
||||
}
|
||||
return findings, Probe, nil
|
||||
}
|
203
probes/dismissesStaleReviews/impl_test.go
Normal file
203
probes/dismissesStaleReviews/impl_test.go
Normal file
@ -0,0 +1,203 @@
|
||||
// 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.
|
||||
|
||||
//nolint:stylecheck
|
||||
package dismissesStaleReviews
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
)
|
||||
|
||||
func Test_Run(t *testing.T) {
|
||||
t.Parallel()
|
||||
trueVal := true
|
||||
falseVal := false
|
||||
branchVal1 := "branch-name1"
|
||||
branchVal2 := "branch-name1"
|
||||
|
||||
//nolint:govet
|
||||
tests := []struct {
|
||||
name string
|
||||
raw *checker.RawResults
|
||||
outcomes []finding.Outcome
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "Dismisses stale reviews on 1/1 branches",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
DismissStaleReviews: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Dismisses stale reviews on 2/2 branches",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
DismissStaleReviews: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
DismissStaleReviews: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive, finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Dismisses stale reviews on 1/2 branches - 1",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
DismissStaleReviews: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
DismissStaleReviews: &falseVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive, finding.OutcomeNegative,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Dismisses stale reviews on 1/2 branches - 2",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
DismissStaleReviews: &falseVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
DismissStaleReviews: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative, finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Dismisses stale reviews on 0/2 branches",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
DismissStaleReviews: &falseVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
DismissStaleReviews: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative, finding.OutcomeNotAvailable,
|
||||
},
|
||||
},
|
||||
}
|
||||
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()
|
||||
|
||||
findings, s, err := Run(tt.raw)
|
||||
if !cmp.Equal(tt.err, err, cmpopts.EquateErrors()) {
|
||||
t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(tt.err, err, cmpopts.EquateErrors()))
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(Probe, s); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(len(tt.outcomes), len(findings)); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
for i := range tt.outcomes {
|
||||
outcome := &tt.outcomes[i]
|
||||
f := &findings[i]
|
||||
if diff := cmp.Diff(*outcome, f.Outcome); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
39
probes/internal/utils/branchprotection/branchProtection.go
Normal file
39
probes/internal/utils/branchprotection/branchProtection.go
Normal file
@ -0,0 +1,39 @@
|
||||
// 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 branchprotection
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
)
|
||||
|
||||
var errWrongValue = errors.New("wrong value, should not happen")
|
||||
|
||||
func GetTextOutcomeFromBool(b *bool, rule, branchName string) (string, finding.Outcome, error) {
|
||||
switch {
|
||||
case b == nil:
|
||||
msg := fmt.Sprintf("unable to retrieve whether '%s' is required to merge on branch '%s'", rule, branchName)
|
||||
return msg, finding.OutcomeNotAvailable, nil
|
||||
case *b:
|
||||
msg := fmt.Sprintf("'%s' is required to merge on branch '%s'", rule, branchName)
|
||||
return msg, finding.OutcomePositive, nil
|
||||
case !*b:
|
||||
msg := fmt.Sprintf("'%s' is disable on branch '%s'", rule, branchName)
|
||||
return msg, finding.OutcomeNegative, nil
|
||||
}
|
||||
return "", finding.OutcomeError, errWrongValue
|
||||
}
|
32
probes/requiresApproversForPullRequests/def.yml
Normal file
32
probes/requiresApproversForPullRequests/def.yml
Normal file
@ -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: requiresApproversForPullRequests
|
||||
short: Check that the project requires approvers for pull requests.
|
||||
motivation: >
|
||||
Requiring approvers for pull requests makes it harder to introduce vulnerable code to the project.
|
||||
implementation: >
|
||||
The probe checks the number of required approvers in default and release branches of the project.
|
||||
outcome:
|
||||
- The probe returns one OutcomePositive for each branch that requires approval for PRs, and one OutcomeNegative for branches that don't.
|
||||
remediation:
|
||||
effort: High
|
||||
text:
|
||||
- Configure the project so that it requires approval to merge PRs.
|
||||
- For GitHub-hosted projects, see ["Approving a pull request with required reviews"](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/approving-a-pull-request-with-required-reviews).
|
||||
- For Gitlab-hosted projects, see ["Merge request approvals"](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/).
|
||||
markdown:
|
||||
- Configure the project so that it requires approval to merge PRs.
|
||||
- For GitHub-hosted projects, see ["Approving a pull request with required reviews"](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/approving-a-pull-request-with-required-reviews).
|
||||
- For Gitlab-hosted projects, see ["Merge request approvals"](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/).
|
80
probes/requiresApproversForPullRequests/impl.go
Normal file
80
probes/requiresApproversForPullRequests/impl.go
Normal file
@ -0,0 +1,80 @@
|
||||
// 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.
|
||||
|
||||
//nolint:stylecheck
|
||||
package requiresApproversForPullRequests
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
"github.com/ossf/scorecard/v4/probes/internal/utils/uerror"
|
||||
)
|
||||
|
||||
//go:embed *.yml
|
||||
var fs embed.FS
|
||||
|
||||
const Probe = "requiresApproversForPullRequests"
|
||||
|
||||
var errWrongValue = errors.New("wrong value, should not happen")
|
||||
|
||||
func Run(raw *checker.RawResults) ([]finding.Finding, string, error) {
|
||||
if raw == nil {
|
||||
return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil)
|
||||
}
|
||||
|
||||
r := raw.BranchProtectionResults
|
||||
var findings []finding.Finding
|
||||
|
||||
for i := range r.Branches {
|
||||
branch := &r.Branches[i]
|
||||
nilMsg := fmt.Sprintf("could not determine whether branch '%s' has required approving review count", *branch.Name)
|
||||
trueMsg := fmt.Sprintf("required approving review count on branch '%s'", *branch.Name)
|
||||
falseMsg := fmt.Sprintf("branch '%s' does not require approvers", *branch.Name)
|
||||
|
||||
p := branch.BranchProtectionRule.RequiredPullRequestReviews.RequiredApprovingReviewCount
|
||||
|
||||
f, err := finding.NewWith(fs, Probe, "", nil, finding.OutcomeNotAvailable)
|
||||
if err != nil {
|
||||
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case p == nil:
|
||||
f = f.WithMessage(nilMsg).WithOutcome(finding.OutcomeNotAvailable)
|
||||
f = f.WithValues(map[string]int{
|
||||
*branch.Name: 1,
|
||||
})
|
||||
case *p > 0:
|
||||
f = f.WithMessage(trueMsg).WithOutcome(finding.OutcomePositive)
|
||||
f = f.WithValues(map[string]int{
|
||||
*branch.Name: 1,
|
||||
"numberOfRequiredReviewers": int(*p),
|
||||
})
|
||||
case *p == 0:
|
||||
f = f.WithMessage(falseMsg).WithOutcome(finding.OutcomeNegative)
|
||||
f = f.WithValues(map[string]int{
|
||||
*branch.Name: 1,
|
||||
"numberOfRequiredReviewers": int(*p),
|
||||
})
|
||||
default:
|
||||
return nil, Probe, fmt.Errorf("create finding: %w", errWrongValue)
|
||||
}
|
||||
findings = append(findings, *f)
|
||||
}
|
||||
return findings, Probe, nil
|
||||
}
|
203
probes/requiresApproversForPullRequests/impl_test.go
Normal file
203
probes/requiresApproversForPullRequests/impl_test.go
Normal file
@ -0,0 +1,203 @@
|
||||
// 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.
|
||||
|
||||
//nolint:stylecheck
|
||||
package requiresApproversForPullRequests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
)
|
||||
|
||||
func Test_Run(t *testing.T) {
|
||||
t.Parallel()
|
||||
var zeroVal int32
|
||||
var oneVal int32 = 1
|
||||
branchVal1 := "branch-name1"
|
||||
branchVal2 := "branch-name1"
|
||||
|
||||
//nolint:govet
|
||||
tests := []struct {
|
||||
name string
|
||||
raw *checker.RawResults
|
||||
outcomes []finding.Outcome
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "1 branch requires 1 reviewer = 1 positive outcome = 1 positive outcome",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequiredApprovingReviewCount: &oneVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "2 branch require 1 reviewer each = 2 positive outcomes = 2 positive outcomes",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequiredApprovingReviewCount: &oneVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequiredApprovingReviewCount: &oneVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive, finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "1 branch requires 1 reviewer and 1 branch requires 0 reviewers = 1 positive and 1 negative",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequiredApprovingReviewCount: &oneVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequiredApprovingReviewCount: &zeroVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive, finding.OutcomeNegative,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "1 branch requires 0 reviewers and 1 branch requires 1 reviewer = 1 negative and 1 positive",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequiredApprovingReviewCount: &zeroVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequiredApprovingReviewCount: &oneVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative, finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "1 branch requires 0 reviewers and 1 branch lacks data = 1 negative and 1 unavailable",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequiredApprovingReviewCount: &zeroVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequiredApprovingReviewCount: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative, finding.OutcomeNotAvailable,
|
||||
},
|
||||
},
|
||||
}
|
||||
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()
|
||||
|
||||
findings, s, err := Run(tt.raw)
|
||||
if !cmp.Equal(tt.err, err, cmpopts.EquateErrors()) {
|
||||
t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(tt.err, err, cmpopts.EquateErrors()))
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(Probe, s); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(len(tt.outcomes), len(findings)); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
for i := range tt.outcomes {
|
||||
outcome := &tt.outcomes[i]
|
||||
f := &findings[i]
|
||||
if diff := cmp.Diff(*outcome, f.Outcome); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
32
probes/requiresCodeOwnersReview/def.yml
Normal file
32
probes/requiresCodeOwnersReview/def.yml
Normal file
@ -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: requiresCodeOwnersReview
|
||||
short: Check that the project requires dedicated code owners to review PRs.
|
||||
motivation: >
|
||||
Code owners are expected to have deep knowledge about a code; Having experienced reviewers for PRs is expected to prevent security issues.
|
||||
implementation: >
|
||||
The probe checks which branches require code owner reviews. The probe only considers default and release branches.
|
||||
outcome:
|
||||
- The probe returns one OutcomePositive for each branch that requires code owner review for PRs, and one OutcomeNegative for branches that don't.
|
||||
remediation:
|
||||
effort: High
|
||||
text:
|
||||
- Configure the project such that code owners must review PRs.
|
||||
- For GitHub-hosted projects, see [the About code owners documentation](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners).
|
||||
- For Gitlab-hosted projects, see [the Code Owners documentation](https://docs.gitlab.com/ee/user/project/codeowners/).
|
||||
markdown:
|
||||
- Configure the project such that code owners must review PRs.
|
||||
- For GitHub-hosted projects, see [the About code owners documentation](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners).
|
||||
- For Gitlab-hosted projects, see [the Code Owners documentation](https://docs.gitlab.com/ee/user/project/codeowners/).
|
70
probes/requiresCodeOwnersReview/impl.go
Normal file
70
probes/requiresCodeOwnersReview/impl.go
Normal file
@ -0,0 +1,70 @@
|
||||
// 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.
|
||||
|
||||
//nolint:stylecheck
|
||||
package requiresCodeOwnersReview
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
"github.com/ossf/scorecard/v4/probes/internal/utils/uerror"
|
||||
)
|
||||
|
||||
//go:embed *.yml
|
||||
var fs embed.FS
|
||||
|
||||
const Probe = "requiresCodeOwnersReview"
|
||||
|
||||
func Run(raw *checker.RawResults) ([]finding.Finding, string, error) {
|
||||
if raw == nil {
|
||||
return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil)
|
||||
}
|
||||
|
||||
r := raw.BranchProtectionResults
|
||||
var findings []finding.Finding
|
||||
|
||||
for i := range r.Branches {
|
||||
branch := &r.Branches[i]
|
||||
reqOwnerReviews := branch.BranchProtectionRule.RequiredPullRequestReviews.RequireCodeOwnerReviews
|
||||
var text string
|
||||
var outcome finding.Outcome
|
||||
|
||||
switch {
|
||||
case reqOwnerReviews == nil:
|
||||
text = "could not determine whether codeowners review is allowed"
|
||||
outcome = finding.OutcomeNotAvailable
|
||||
case !*reqOwnerReviews:
|
||||
text = fmt.Sprintf("codeowners review is not required on branch '%s'", *branch.Name)
|
||||
outcome = finding.OutcomeNegative
|
||||
case len(r.CodeownersFiles) == 0:
|
||||
text = "codeowners review is required - but no codeowners file found in repo"
|
||||
outcome = finding.OutcomeNegative
|
||||
default:
|
||||
text = fmt.Sprintf("codeowner review is required on branch '%s'", *branch.Name)
|
||||
outcome = finding.OutcomePositive
|
||||
}
|
||||
f, err := finding.NewWith(fs, Probe, text, nil, outcome)
|
||||
if err != nil {
|
||||
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||
}
|
||||
f = f.WithValues(map[string]int{
|
||||
*branch.Name: 1,
|
||||
})
|
||||
findings = append(findings, *f)
|
||||
}
|
||||
return findings, Probe, nil
|
||||
}
|
315
probes/requiresCodeOwnersReview/impl_test.go
Normal file
315
probes/requiresCodeOwnersReview/impl_test.go
Normal file
@ -0,0 +1,315 @@
|
||||
// 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.
|
||||
|
||||
//nolint:stylecheck
|
||||
package requiresCodeOwnersReview
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
)
|
||||
|
||||
func Test_Run(t *testing.T) {
|
||||
t.Parallel()
|
||||
trueVal := true
|
||||
falseVal := false
|
||||
branchVal1 := "branch-name1"
|
||||
branchVal2 := "branch-name1"
|
||||
//nolint:govet
|
||||
tests := []struct {
|
||||
name string
|
||||
raw *checker.RawResults
|
||||
outcomes []finding.Outcome
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "1 branch requires code owner reviews with viles = 1 positive outcome",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequireCodeOwnerReviews: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
CodeownersFiles: []string{"file"},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "1 branch requires code owner reviews without files = 1 negative outcome",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequireCodeOwnerReviews: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
CodeownersFiles: []string{},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "2 branches require code owner reviews with files = 2 positive outcomes",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequireCodeOwnerReviews: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequireCodeOwnerReviews: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
CodeownersFiles: []string{"file"},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive, finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "2 branches require code owner reviews with files = 2 negative outcomes",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequireCodeOwnerReviews: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequireCodeOwnerReviews: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
CodeownersFiles: []string{},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative, finding.OutcomeNegative,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "1 branches require code owner reviews and 1 branch doesn't with files = 1 positive 1 negative",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequireCodeOwnerReviews: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequireCodeOwnerReviews: &falseVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
CodeownersFiles: []string{"file"},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive, finding.OutcomeNegative,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Requires code owner reviews on 1/2 branches - without files = 2 negative outcomes",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequireCodeOwnerReviews: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequireCodeOwnerReviews: &falseVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
CodeownersFiles: []string{},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative, finding.OutcomeNegative,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Requires code owner reviews on 1/2 branches - with files = 1 negative and 1 positive",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequireCodeOwnerReviews: &falseVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequireCodeOwnerReviews: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
CodeownersFiles: []string{"file"},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative, finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Requires code owner reviews on 1/2 branches - without files = 2 negative outcomes",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequireCodeOwnerReviews: &falseVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequireCodeOwnerReviews: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
CodeownersFiles: []string{},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative, finding.OutcomeNegative,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "1 branch does not require code owner review and 1 lacks data = 1 negative and 1 unavailable",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequireCodeOwnerReviews: &falseVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequiredPullRequestReviews: clients.PullRequestReviewRule{
|
||||
RequireCodeOwnerReviews: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
CodeownersFiles: []string{"file"},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative, finding.OutcomeNotAvailable,
|
||||
},
|
||||
},
|
||||
}
|
||||
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()
|
||||
|
||||
findings, s, err := Run(tt.raw)
|
||||
if !cmp.Equal(tt.err, err, cmpopts.EquateErrors()) {
|
||||
t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(tt.err, err, cmpopts.EquateErrors()))
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(Probe, s); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(len(tt.outcomes), len(findings)); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
for i := range tt.outcomes {
|
||||
outcome := &tt.outcomes[i]
|
||||
f := &findings[i]
|
||||
if diff := cmp.Diff(*outcome, f.Outcome); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
32
probes/requiresLastPushApproval/def.yml
Normal file
32
probes/requiresLastPushApproval/def.yml
Normal file
@ -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: requiresLastPushApproval
|
||||
short: Check that the project requires approval of the most recent push.
|
||||
motivation: >
|
||||
Requiring approval of the most recent push prevents contributors from sneaking malicious commits into a PR after it has been approved.
|
||||
implementation: >
|
||||
The probe checks the protection rules of default and release branches branches.
|
||||
outcome:
|
||||
- The probe returns one OutcomePositive for each branch that requires approval of the most recent push, and one OutcomeNegative for branches that don't.
|
||||
remediation:
|
||||
effort: High
|
||||
text:
|
||||
- Configure the project such that it requires approval of the most recent push.
|
||||
- For GitHub-hosted projects, see [the documentation on protected branches](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches) and how to require approval of the last push from someone who did not make the last push.
|
||||
- For Gitlab-hosted projects, see how to [remove all approvals when commits a added to the source branch](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/settings.html#remove-all-approvals-when-commits-are-added-to-the-source-branch).
|
||||
markdown:
|
||||
- Configure the project such that it requires approval of the most recent push.
|
||||
- For GitHub-hosted projects, see [the documentation on protected branches](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches) and how to require approval of the last push from someone who did not make the last push.
|
||||
- For Gitlab-hosted projects, see how to [remove all approvals when commits a added to the source branch](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/settings.html#remove-all-approvals-when-commits-are-added-to-the-source-branch).
|
59
probes/requiresLastPushApproval/impl.go
Normal file
59
probes/requiresLastPushApproval/impl.go
Normal file
@ -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.
|
||||
|
||||
//nolint:stylecheck
|
||||
package requiresLastPushApproval
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
"github.com/ossf/scorecard/v4/probes/internal/utils/branchprotection"
|
||||
"github.com/ossf/scorecard/v4/probes/internal/utils/uerror"
|
||||
)
|
||||
|
||||
//go:embed *.yml
|
||||
var fs embed.FS
|
||||
|
||||
const Probe = "requiresLastPushApproval"
|
||||
|
||||
func Run(raw *checker.RawResults) ([]finding.Finding, string, error) {
|
||||
if raw == nil {
|
||||
return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil)
|
||||
}
|
||||
|
||||
r := raw.BranchProtectionResults
|
||||
var findings []finding.Finding
|
||||
|
||||
for i := range r.Branches {
|
||||
branch := &r.Branches[i]
|
||||
|
||||
p := branch.BranchProtectionRule.RequireLastPushApproval
|
||||
text, outcome, err := branchprotection.GetTextOutcomeFromBool(p, "last push approval", *branch.Name)
|
||||
if err != nil {
|
||||
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||
}
|
||||
f, err := finding.NewWith(fs, Probe, text, nil, outcome)
|
||||
if err != nil {
|
||||
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||
}
|
||||
f = f.WithValues(map[string]int{
|
||||
*branch.Name: 1,
|
||||
})
|
||||
findings = append(findings, *f)
|
||||
}
|
||||
return findings, Probe, nil
|
||||
}
|
184
probes/requiresLastPushApproval/impl_test.go
Normal file
184
probes/requiresLastPushApproval/impl_test.go
Normal file
@ -0,0 +1,184 @@
|
||||
// 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.
|
||||
|
||||
//nolint:stylecheck
|
||||
package requiresLastPushApproval
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
)
|
||||
|
||||
func Test_Run(t *testing.T) {
|
||||
t.Parallel()
|
||||
trueVal := true
|
||||
falseVal := false
|
||||
branchVal1 := "branch-name1"
|
||||
branchVal2 := "branch-name1"
|
||||
//nolint:govet
|
||||
tests := []struct {
|
||||
name string
|
||||
raw *checker.RawResults
|
||||
outcomes []finding.Outcome
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "1 branch requires last push approval = 1 positive outcome",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequireLastPushApproval: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "2 branches requirs last push approval = 2 positive outcomes",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequireLastPushApproval: &trueVal,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequireLastPushApproval: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive, finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Last push approval enabled on 1/2 branches = 1 positive and 1 negative outcomes",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequireLastPushApproval: &trueVal,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequireLastPushApproval: &falseVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive, finding.OutcomeNegative,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Last push approval enabled on 1/2 branches = 1 negative and 1 positive outcomes",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequireLastPushApproval: &falseVal,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequireLastPushApproval: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative, finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "1 branch does not require last push approval and 1 lacks data = 1 negative and 1 unavailable",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequireLastPushApproval: &falseVal,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
RequireLastPushApproval: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative, finding.OutcomeNotAvailable,
|
||||
},
|
||||
},
|
||||
}
|
||||
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()
|
||||
|
||||
findings, s, err := Run(tt.raw)
|
||||
if !cmp.Equal(tt.err, err, cmpopts.EquateErrors()) {
|
||||
t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(tt.err, err, cmpopts.EquateErrors()))
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(Probe, s); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(len(tt.outcomes), len(findings)); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
for i := range tt.outcomes {
|
||||
outcome := &tt.outcomes[i]
|
||||
f := &findings[i]
|
||||
if diff := cmp.Diff(*outcome, f.Outcome); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
32
probes/requiresUpToDateBranches/def.yml
Normal file
32
probes/requiresUpToDateBranches/def.yml
Normal file
@ -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: requiresUpToDateBranches
|
||||
short: Check that the project requires PRs to be in sync with the base branch.
|
||||
motivation: >
|
||||
Requiring PRs to be in sync with the base branch is good practice.
|
||||
implementation: >
|
||||
The probe checks the branch protection rules of default and release branches in the repository.
|
||||
outcome:
|
||||
- The probe returns one OutcomePositive for each branch that requires PRs to be in sync with the base branch, and one OutcomeNegative for branches that don't.
|
||||
remediation:
|
||||
effort: High
|
||||
text:
|
||||
- Configure the repository such that it requires PRs to be in sync with the base branch.
|
||||
- For GitHub-hosted projects, followed [these instructions](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#require-status-checks-before-merging)
|
||||
- For Gitlab-hosted projects, use [semi-linear merge methods](https://docs.gitlab.com/ee/user/project/merge_requests/methods/#rebasing-in-semi-linear-merge-methods).
|
||||
markdown:
|
||||
- Configure the repository such that it requires PRs to be in sync with the base branch.
|
||||
- For GitHub-hosted projects, followed [these instructions](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#require-status-checks-before-merging)
|
||||
- For Gitlab-hosted projects, use [semi-linear merge methods](https://docs.gitlab.com/ee/user/project/merge_requests/methods/#rebasing-in-semi-linear-merge-methods).
|
61
probes/requiresUpToDateBranches/impl.go
Normal file
61
probes/requiresUpToDateBranches/impl.go
Normal file
@ -0,0 +1,61 @@
|
||||
// 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.
|
||||
|
||||
//nolint:stylecheck
|
||||
package requiresUpToDateBranches
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
"github.com/ossf/scorecard/v4/probes/internal/utils/branchprotection"
|
||||
"github.com/ossf/scorecard/v4/probes/internal/utils/uerror"
|
||||
)
|
||||
|
||||
//go:embed *.yml
|
||||
var fs embed.FS
|
||||
|
||||
const Probe = "requiresUpToDateBranches"
|
||||
|
||||
func Run(raw *checker.RawResults) ([]finding.Finding, string, error) {
|
||||
if raw == nil {
|
||||
return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil)
|
||||
}
|
||||
|
||||
r := raw.BranchProtectionResults
|
||||
var findings []finding.Finding
|
||||
|
||||
for i := range r.Branches {
|
||||
branch := &r.Branches[i]
|
||||
|
||||
p := branch.BranchProtectionRule.CheckRules.UpToDateBeforeMerge
|
||||
text, outcome, err := branchprotection.GetTextOutcomeFromBool(p,
|
||||
"up-to-date branches",
|
||||
*branch.Name)
|
||||
if err != nil {
|
||||
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||
}
|
||||
f, err := finding.NewWith(fs, Probe, text, nil, outcome)
|
||||
if err != nil {
|
||||
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||
}
|
||||
f = f.WithValues(map[string]int{
|
||||
*branch.Name: 1,
|
||||
})
|
||||
findings = append(findings, *f)
|
||||
}
|
||||
return findings, Probe, nil
|
||||
}
|
202
probes/requiresUpToDateBranches/impl_test.go
Normal file
202
probes/requiresUpToDateBranches/impl_test.go
Normal file
@ -0,0 +1,202 @@
|
||||
// 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.
|
||||
|
||||
//nolint:stylecheck
|
||||
package requiresUpToDateBranches
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
)
|
||||
|
||||
func Test_Run(t *testing.T) {
|
||||
t.Parallel()
|
||||
trueVal := true
|
||||
falseVal := false
|
||||
branchVal1 := "branch-name1"
|
||||
branchVal2 := "branch-name1"
|
||||
//nolint:govet
|
||||
tests := []struct {
|
||||
name string
|
||||
raw *checker.RawResults
|
||||
outcomes []finding.Outcome
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "1 branch requires up-to-date before merge = 1 positive outcome",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
CheckRules: clients.StatusChecksRule{
|
||||
UpToDateBeforeMerge: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "2 branches require up-to-date before merge = 2 positive outcomes",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
CheckRules: clients.StatusChecksRule{
|
||||
UpToDateBeforeMerge: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
CheckRules: clients.StatusChecksRule{
|
||||
UpToDateBeforeMerge: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive, finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Requires up to date branches on 1/2 branches = 1 positive and 1 negative outcomes",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
CheckRules: clients.StatusChecksRule{
|
||||
UpToDateBeforeMerge: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
CheckRules: clients.StatusChecksRule{
|
||||
UpToDateBeforeMerge: &falseVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive, finding.OutcomeNegative,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Requires up to date branches on 1/2 branches = 1 negative and 1 positive outcomes",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
CheckRules: clients.StatusChecksRule{
|
||||
UpToDateBeforeMerge: &falseVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
CheckRules: clients.StatusChecksRule{
|
||||
UpToDateBeforeMerge: &trueVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative, finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "1 branch does no require up-to-date before merge and 1 branch lacks data= 1 positive & 1 unavailable",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
CheckRules: clients.StatusChecksRule{
|
||||
UpToDateBeforeMerge: &falseVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
CheckRules: clients.StatusChecksRule{
|
||||
UpToDateBeforeMerge: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative, finding.OutcomeNotAvailable,
|
||||
},
|
||||
},
|
||||
}
|
||||
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()
|
||||
|
||||
findings, s, err := Run(tt.raw)
|
||||
if !cmp.Equal(tt.err, err, cmpopts.EquateErrors()) {
|
||||
t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(tt.err, err, cmpopts.EquateErrors()))
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(Probe, s); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(len(tt.outcomes), len(findings)); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
for i := range tt.outcomes {
|
||||
outcome := &tt.outcomes[i]
|
||||
f := &findings[i]
|
||||
if diff := cmp.Diff(*outcome, f.Outcome); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
26
probes/runsStatusChecksBeforeMerging/def.yml
Normal file
26
probes/runsStatusChecksBeforeMerging/def.yml
Normal file
@ -0,0 +1,26 @@
|
||||
# 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: runsStatusChecksBeforeMerging
|
||||
short: Check that the project runs required status checks
|
||||
motivation: >
|
||||
Required status checks can check for common errors and resolve issues in PRs.
|
||||
implementation: >
|
||||
The probe checks the rules for default and release branches in the projects repository.
|
||||
outcome:
|
||||
- The probe returns one OutcomePositive for each branch that runs required status checks, and one OutcomeNegative for branches that don't.
|
||||
remediation:
|
||||
effort: Medium
|
||||
text:
|
||||
- Enable required status checks by following [these guidelines](https://docs.github.com/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks).
|
68
probes/runsStatusChecksBeforeMerging/impl.go
Normal file
68
probes/runsStatusChecksBeforeMerging/impl.go
Normal file
@ -0,0 +1,68 @@
|
||||
// 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.
|
||||
|
||||
//nolint:stylecheck
|
||||
package runsStatusChecksBeforeMerging
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
"github.com/ossf/scorecard/v4/probes/internal/utils/uerror"
|
||||
)
|
||||
|
||||
//go:embed *.yml
|
||||
var fs embed.FS
|
||||
|
||||
const Probe = "runsStatusChecksBeforeMerging"
|
||||
|
||||
func Run(raw *checker.RawResults) ([]finding.Finding, string, error) {
|
||||
if raw == nil {
|
||||
return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil)
|
||||
}
|
||||
|
||||
r := raw.BranchProtectionResults
|
||||
var findings []finding.Finding
|
||||
|
||||
for i := range r.Branches {
|
||||
branch := &r.Branches[i]
|
||||
switch {
|
||||
case len(branch.BranchProtectionRule.CheckRules.Contexts) > 0:
|
||||
f, err := finding.NewWith(fs, Probe,
|
||||
fmt.Sprintf("status check found to merge onto on branch '%s'", *branch.Name), nil,
|
||||
finding.OutcomePositive)
|
||||
if err != nil {
|
||||
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||
}
|
||||
f = f.WithValues(map[string]int{
|
||||
*branch.Name: 1,
|
||||
})
|
||||
findings = append(findings, *f)
|
||||
default:
|
||||
f, err := finding.NewWith(fs, Probe,
|
||||
fmt.Sprintf("no status checks found to merge onto branch '%s'", *branch.Name), nil,
|
||||
finding.OutcomeNegative)
|
||||
if err != nil {
|
||||
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||
}
|
||||
f = f.WithValues(map[string]int{
|
||||
*branch.Name: 1,
|
||||
})
|
||||
findings = append(findings, *f)
|
||||
}
|
||||
}
|
||||
return findings, Probe, nil
|
||||
}
|
201
probes/runsStatusChecksBeforeMerging/impl_test.go
Normal file
201
probes/runsStatusChecksBeforeMerging/impl_test.go
Normal file
@ -0,0 +1,201 @@
|
||||
// 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.
|
||||
|
||||
//nolint:stylecheck
|
||||
package runsStatusChecksBeforeMerging
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
"github.com/ossf/scorecard/v4/finding"
|
||||
)
|
||||
|
||||
func Test_Run(t *testing.T) {
|
||||
t.Parallel()
|
||||
branchVal1 := "branch-name1"
|
||||
branchVal2 := "branch-name1"
|
||||
|
||||
//nolint:govet
|
||||
tests := []struct {
|
||||
name string
|
||||
raw *checker.RawResults
|
||||
outcomes []finding.Outcome
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "Runs status checks on 1/1 branches with contexts = 1 positive outcome",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
CheckRules: clients.StatusChecksRule{
|
||||
Contexts: []string{"foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Runs status checks on 2/2 branches with contexts = 2 positive outcomes",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
CheckRules: clients.StatusChecksRule{
|
||||
Contexts: []string{"foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
CheckRules: clients.StatusChecksRule{
|
||||
Contexts: []string{"foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive, finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Runs status checks on 1/2 branches = 1 positive and 1 negative outcome",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
CheckRules: clients.StatusChecksRule{
|
||||
Contexts: []string{"foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
CheckRules: clients.StatusChecksRule{
|
||||
Contexts: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomePositive, finding.OutcomeNegative,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Runs status checks on 1/2 branches = 1 negative and 1 positive outcome",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
CheckRules: clients.StatusChecksRule{
|
||||
Contexts: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
CheckRules: clients.StatusChecksRule{
|
||||
Contexts: []string{"foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative, finding.OutcomePositive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Runs status checks on 0/2 branches = 2 negative outcomes",
|
||||
raw: &checker.RawResults{
|
||||
BranchProtectionResults: checker.BranchProtectionsData{
|
||||
Branches: []clients.BranchRef{
|
||||
{
|
||||
Name: &branchVal1,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
CheckRules: clients.StatusChecksRule{
|
||||
Contexts: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &branchVal2,
|
||||
BranchProtectionRule: clients.BranchProtectionRule{
|
||||
CheckRules: clients.StatusChecksRule{
|
||||
Contexts: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outcomes: []finding.Outcome{
|
||||
finding.OutcomeNegative, finding.OutcomeNegative,
|
||||
},
|
||||
},
|
||||
}
|
||||
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()
|
||||
|
||||
findings, s, err := Run(tt.raw)
|
||||
if !cmp.Equal(tt.err, err, cmpopts.EquateErrors()) {
|
||||
t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(tt.err, err, cmpopts.EquateErrors()))
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(Probe, s); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(len(tt.outcomes), len(findings)); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
for i := range tt.outcomes {
|
||||
outcome := &tt.outcomes[i]
|
||||
f := &findings[i]
|
||||
if diff := cmp.Diff(*outcome, f.Outcome); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user