🌱 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:
AdamKorcz 2023-12-27 22:33:06 +00:00 committed by GitHub
parent c1a0557dbf
commit 2e1059bb76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 2771 additions and 0 deletions

View 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.

View 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
}

View 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)
}
}
})
}
}

View 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.

View 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
}

View 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)
}
}
})
}
}

View 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.

View 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
}

View 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)
}
}
})
}
}

View 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).

View 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
}

View 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)
}
}
})
}
}

View 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
}

View 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/).

View 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
}

View 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)
}
}
})
}
}

View 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/).

View 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
}

View 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)
}
}
})
}
}

View 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).

View 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
}

View 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)
}
}
})
}
}

View 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).

View 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
}

View 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)
}
}
})
}
}

View 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).

View 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
}

View 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)
}
}
})
}
}