🌱 Migrate Maintained check to probes (#3507)

* 🌱 Migrate Maintained check to probes

Signed-off-by: AdamKorcz <adam@adalogics.com>

* fix typos

Signed-off-by: AdamKorcz <adam@adalogics.com>

* rename 'archived' probe to 'notArchvied

Signed-off-by: AdamKorcz <adam@adalogics.com>

* remove part of comment

Signed-off-by: AdamKorcz <adam@adalogics.com>

* fix typo

Signed-off-by: AdamKorcz <adam@adalogics.com>

* log negative findings

Signed-off-by: AdamKorcz <adam@adalogics.com>

* log non positive findings if repo was created less than 90 days ago

Signed-off-by: AdamKorcz <adam@adalogics.com>

* rename probe from 'activityOnIssuesByCollaboratorsMembersOrOwnersInLast90Days' to 'issueActivityByProjectMember'

Signed-off-by: AdamKorcz <adam@adalogics.com>

* change probe descriptions

Signed-off-by: AdamKorcz <adam@adalogics.com>

* rename 'wasCreatedInLast90Days' probe to 'notCreatedInLast90Days'

Signed-off-by: AdamKorcz <adam@adalogics.com>

* Add tests with zero issues

Signed-off-by: AdamKorcz <adam@adalogics.com>

* use values instead of returning multiple findings

Signed-off-by: AdamKorcz <adam@adalogics.com>

* return negative findings instead of non-positive

Signed-off-by: AdamKorcz <adam@adalogics.com>

* correct 'notCreatedInLast90Days' probe definition

Signed-off-by: AdamKorcz <adam@adalogics.com>

* make nested conditionals a single line

Signed-off-by: AdamKorcz <adam@adalogics.com>

* make nested conditionals a single line

Signed-off-by: AdamKorcz <adam@adalogics.com>

* change var name 'issuesUpdatedWithinThreshold' to 'numberOfIssuesUpdatedWithinThreshold'

Signed-off-by: AdamKorcz <adam@adalogics.com>

* rename 'notCreatedInLast90Days' to 'notCreatedRecently'

Signed-off-by: AdamKorcz <adam@adalogics.com>

* explain 'commitsWithinThreshold' in probe definition

Signed-off-by: AdamKorcz <adam@adalogics.com>

* rename 'commitsInLast90Days' to 'hasRecentCommits'" -s

Signed-off-by: AdamKorcz <adam@adalogics.com>

* fix linter issues

Signed-off-by: AdamKorcz <adam@adalogics.com>

* define 'numberOfIssuesUpdatedWithinThreshold'

Signed-off-by: AdamKorcz <adam@adalogics.com>

---------

Signed-off-by: AdamKorcz <adam@adalogics.com>
This commit is contained in:
AdamKorcz 2023-11-17 17:57:10 +00:00 committed by GitHub
parent be0b915f76
commit 1c3d9eb6e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1124 additions and 238 deletions

View File

@ -16,11 +16,14 @@ package evaluation
import (
"fmt"
"time"
"github.com/ossf/scorecard/v4/checker"
"github.com/ossf/scorecard/v4/clients"
sce "github.com/ossf/scorecard/v4/errors"
"github.com/ossf/scorecard/v4/finding"
"github.com/ossf/scorecard/v4/probes/hasRecentCommits"
"github.com/ossf/scorecard/v4/probes/issueActivityByProjectMember"
"github.com/ossf/scorecard/v4/probes/notArchived"
"github.com/ossf/scorecard/v4/probes/notCreatedRecently"
)
const (
@ -30,68 +33,67 @@ const (
)
// Maintained applies the score policy for the Maintained check.
func Maintained(name string, dl checker.DetailLogger, r *checker.MaintainedData) checker.CheckResult {
if r == nil {
e := sce.WithMessage(sce.ErrScorecardInternal, "empty raw data")
func Maintained(name string,
findings []finding.Finding, dl checker.DetailLogger,
) checker.CheckResult {
// We have 4 unique probes, each should have a finding.
expectedProbes := []string{
notArchived.Probe,
issueActivityByProjectMember.Probe,
hasRecentCommits.Probe,
notCreatedRecently.Probe,
}
if !finding.UniqueProbesEqual(findings, expectedProbes) {
e := sce.WithMessage(sce.ErrScorecardInternal, "invalid probe results")
return checker.CreateRuntimeErrorResult(name, e)
}
if r.ArchivedStatus.Status {
return checker.CreateMinScoreResult(name, "repo is marked as archived")
if projectIsArchived(findings) {
checker.LogFindings(negativeFindings(findings), dl)
return checker.CreateMinScoreResult(name, "project is archived")
}
if projectWasCreatedInLast90Days(findings) {
checker.LogFindings(negativeFindings(findings), dl)
return checker.CreateMinScoreResult(name, "project was created in last 90 days. please review its contents carefully")
}
// If not explicitly marked archived, look for activity in past `lookBackDays`.
threshold := time.Now().AddDate(0 /*years*/, 0 /*months*/, -1*lookBackDays /*days*/)
commitsWithinThreshold := 0
for i := range r.DefaultBranchCommits {
if r.DefaultBranchCommits[i].CommittedDate.After(threshold) {
commitsWithinThreshold++
}
}
numberOfIssuesUpdatedWithinThreshold := 0
// Emit a warning if this repo was created recently
recencyThreshold := time.Now().AddDate(0 /*years*/, 0 /*months*/, -1*lookBackDays /*days*/)
if r.CreatedAt.After(recencyThreshold) {
dl.Warn(&checker.LogMessage{
Text: fmt.Sprintf("repo was created in the last %d days (Created at: %s), please review its contents carefully",
lookBackDays, r.CreatedAt.Format(time.RFC3339)),
})
daysSinceRepoCreated := int(time.Since(r.CreatedAt).Hours() / 24)
return checker.CreateMinScoreResult(name,
fmt.Sprintf("repo was created %d days ago, not enough maintenance history", daysSinceRepoCreated),
)
}
issuesUpdatedWithinThreshold := 0
for i := range r.Issues {
if hasActivityByCollaboratorOrHigher(&r.Issues[i], threshold) {
issuesUpdatedWithinThreshold++
for i := range findings {
f := &findings[i]
if f.Outcome == finding.OutcomePositive {
switch f.Probe {
case issueActivityByProjectMember.Probe:
numberOfIssuesUpdatedWithinThreshold = f.Values["numberOfIssuesUpdatedWithinThreshold"]
case hasRecentCommits.Probe:
commitsWithinThreshold = f.Values["commitsWithinThreshold"]
}
}
}
return checker.CreateProportionalScoreResult(name, fmt.Sprintf(
"%d commit(s) out of %d and %d issue activity out of %d found in the last %d days",
commitsWithinThreshold, len(r.DefaultBranchCommits), issuesUpdatedWithinThreshold, len(r.Issues), lookBackDays),
commitsWithinThreshold+issuesUpdatedWithinThreshold, activityPerWeek*lookBackDays/daysInOneWeek)
"%d commit(s) and %d issue activity found in the last %d days",
commitsWithinThreshold, numberOfIssuesUpdatedWithinThreshold, lookBackDays),
commitsWithinThreshold+numberOfIssuesUpdatedWithinThreshold, activityPerWeek*lookBackDays/daysInOneWeek)
}
// hasActivityByCollaboratorOrHigher returns true if the issue was created or commented on by an
// owner/collaborator/member since the threshold.
func hasActivityByCollaboratorOrHigher(issue *clients.Issue, threshold time.Time) bool {
if issue == nil {
return false
}
if issue.AuthorAssociation.Gte(clients.RepoAssociationCollaborator) &&
issue.CreatedAt != nil && issue.CreatedAt.After(threshold) {
// The creator of the issue is a collaborator or higher.
return true
}
for _, comment := range issue.Comments {
if comment.AuthorAssociation.Gte(clients.RepoAssociationCollaborator) &&
comment.CreatedAt != nil &&
comment.CreatedAt.After(threshold) {
// The author of the comment is a collaborator or higher.
func projectIsArchived(findings []finding.Finding) bool {
for i := range findings {
f := &findings[i]
if f.Outcome == finding.OutcomeNegative && f.Probe == notArchived.Probe {
return true
}
}
return false
}
func projectWasCreatedInLast90Days(findings []finding.Finding) bool {
for i := range findings {
f := &findings[i]
if f.Outcome == finding.OutcomeNegative && f.Probe == notCreatedRecently.Probe {
return true
}
}

View File

@ -15,221 +15,120 @@ package evaluation
import (
"testing"
"time"
"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"
sce "github.com/ossf/scorecard/v4/errors"
"github.com/ossf/scorecard/v4/finding"
scut "github.com/ossf/scorecard/v4/utests"
)
func Test_hasActivityByCollaboratorOrHigher(t *testing.T) {
t.Parallel()
r := clients.RepoAssociationCollaborator
twentDaysAgo := time.Now().AddDate(0 /*years*/, 0 /*months*/, -20 /*days*/)
type args struct {
issue *clients.Issue
threshold time.Time
}
tests := []struct { //nolint:govet
name string
args args
want bool
}{
{
name: "nil issue",
args: args{
issue: nil,
threshold: time.Now(),
},
want: false,
},
{
name: "repo-association collaborator",
args: args{
issue: &clients.Issue{
CreatedAt: nil,
AuthorAssociation: &r,
},
},
want: false,
},
{
name: "twentyDaysAgo",
args: args{
issue: &clients.Issue{
CreatedAt: &twentDaysAgo,
AuthorAssociation: &r,
},
},
want: true,
},
{
name: "repo-association collaborator with comment",
args: args{
issue: &clients.Issue{
CreatedAt: nil,
AuthorAssociation: &r,
Comments: []clients.IssueComment{
{
CreatedAt: &twentDaysAgo,
AuthorAssociation: &r,
},
},
},
},
want: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := hasActivityByCollaboratorOrHigher(tt.args.issue, tt.args.threshold); got != tt.want {
t.Errorf("hasActivityByCollaboratorOrHigher() = %v, want %v", got, tt.want)
}
})
}
}
func TestMaintained(t *testing.T) {
twentyDaysAgo := time.Now().AddDate(0 /*years*/, 0 /*months*/, -20 /*days*/)
collab := clients.RepoAssociationCollaborator
t.Parallel()
type args struct { //nolint:govet
name string
dl checker.DetailLogger
r *checker.MaintainedData
}
tests := []struct {
name string
args args
want checker.CheckResult
name string
findings []finding.Finding
result scut.TestReturn
}{
{
name: "nil",
args: args{
name: "test",
dl: nil,
r: nil,
name: "Two commits in last 90 days",
findings: []finding.Finding{
{
Probe: "hasRecentCommits",
Outcome: finding.OutcomePositive,
Values: map[string]int{
"commitsWithinThreshold": 2,
},
}, {
Probe: "issueActivityByProjectMember",
Outcome: finding.OutcomePositive,
Values: map[string]int{
"numberOfIssuesUpdatedWithinThreshold": 1,
},
}, {
Probe: "notArchived",
Outcome: finding.OutcomePositive,
}, {
Probe: "notCreatedRecently",
Outcome: finding.OutcomePositive,
},
},
want: checker.CheckResult{
Name: "test",
Version: 2,
Reason: "internal error: empty raw data",
Score: -1,
result: scut.TestReturn{
Score: 2,
},
},
{
name: "archived",
args: args{
name: "test",
dl: nil,
r: &checker.MaintainedData{
ArchivedStatus: checker.ArchivedStatus{Status: true},
name: "No issues, no commits and not archived",
findings: []finding.Finding{
{
Probe: "hasRecentCommits",
Outcome: finding.OutcomeNegative,
}, {
Probe: "issueActivityByProjectMember",
Outcome: finding.OutcomeNegative,
}, {
Probe: "notArchived",
Outcome: finding.OutcomePositive,
}, {
Probe: "notCreatedRecently",
Outcome: finding.OutcomePositive,
},
},
want: checker.CheckResult{
Name: "test",
Version: 2,
Reason: "repo is marked as archived",
Score: 0,
result: scut.TestReturn{
Score: 0,
},
},
{
name: "no activity",
args: args{
name: "test",
dl: nil,
r: &checker.MaintainedData{
ArchivedStatus: checker.ArchivedStatus{Status: false},
DefaultBranchCommits: []clients.Commit{
{
CommittedDate: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
},
},
name: "Wrong probe name",
findings: []finding.Finding{
{
Probe: "hasRecentCommits",
Outcome: finding.OutcomeNegative,
}, {
Probe: "issueActivityByProjectMember",
Outcome: finding.OutcomeNegative,
}, {
Probe: "archvied", /*misspelling*/
Outcome: finding.OutcomePositive,
}, {
Probe: "notCreatedRecently",
Outcome: finding.OutcomePositive,
},
},
want: checker.CheckResult{
Name: "test",
Version: 2,
Reason: "0 commit(s) out of 1 and 0 issue activity out of 0 found in the last 90 days -- score normalized to 0",
Score: 0,
result: scut.TestReturn{
Score: -1,
Error: sce.ErrScorecardInternal,
},
},
{
name: "commit activity in the last 30 days",
args: args{
name: "test",
dl: &scut.TestDetailLogger{},
r: &checker.MaintainedData{
ArchivedStatus: checker.ArchivedStatus{Status: false},
DefaultBranchCommits: []clients.Commit{
{
CommittedDate: time.Now().AddDate(0 /*years*/, 0 /*months*/, -20 /*days*/),
},
{
CommittedDate: time.Now().AddDate(0 /*years*/, 0 /*months*/, -10 /*days*/),
},
},
Issues: []clients.Issue{
{
CreatedAt: &twentyDaysAgo,
AuthorAssociation: &collab,
},
},
CreatedAt: time.Now().AddDate(0 /*years*/, 0 /*months*/, -100 /*days*/),
name: "Project is archived",
findings: []finding.Finding{
{
Probe: "hasRecentCommits",
Outcome: finding.OutcomeNegative,
}, {
Probe: "issueActivityByProjectMember",
Outcome: finding.OutcomeNegative,
}, {
Probe: "notArchived",
Outcome: finding.OutcomeNegative,
}, {
Probe: "notCreatedRecently",
Outcome: finding.OutcomePositive,
},
},
want: checker.CheckResult{
Name: "test",
Version: 2,
Reason: "2 commit(s) out of 2 and 1 issue activity out of 1 found in the last 90 days -- score normalized to 2",
Score: 2,
},
},
{
name: "Repo created recently",
args: args{
name: "test",
dl: &scut.TestDetailLogger{},
r: &checker.MaintainedData{
ArchivedStatus: checker.ArchivedStatus{Status: false},
DefaultBranchCommits: []clients.Commit{
{
CommittedDate: time.Now().AddDate(0 /*years*/, 0 /*months*/, -20 /*days*/),
},
{
CommittedDate: time.Now().AddDate(0 /*years*/, 0 /*months*/, -10 /*days*/),
},
},
Issues: []clients.Issue{
{
CreatedAt: &twentyDaysAgo,
AuthorAssociation: &collab,
},
},
CreatedAt: time.Now().AddDate(0 /*years*/, 0 /*months*/, -10 /*days*/),
},
},
want: checker.CheckResult{
Name: "test",
Version: 2,
Reason: "repo was created 10 days ago, not enough maintenance history",
Score: 0,
result: scut.TestReturn{
Score: 0,
NumberOfWarn: 3,
},
},
}
for _, tt := range tests {
tt := tt
tt := tt // Parallel testing
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := Maintained(tt.args.name, tt.args.dl, tt.args.r); !cmp.Equal(got, tt.want, cmpopts.IgnoreFields(checker.CheckResult{}, "Error")) { //nolint:lll
t.Errorf("Maintained() = %v, want %v", got, cmp.Diff(got, tt.want, cmpopts.IgnoreFields(checker.CheckResult{}, "Error"))) //nolint:lll
dl := scut.TestDetailLogger{}
got := Maintained(tt.name, tt.findings, &dl)
if !scut.ValidateTestReturn(t, tt.name, &tt.result, &got, &dl) {
t.Errorf("got %v, expected %v", got, tt.result)
}
})
}

View File

@ -19,6 +19,8 @@ import (
"github.com/ossf/scorecard/v4/checks/evaluation"
"github.com/ossf/scorecard/v4/checks/raw"
sce "github.com/ossf/scorecard/v4/errors"
"github.com/ossf/scorecard/v4/probes"
"github.com/ossf/scorecard/v4/probes/zrunner"
)
// CheckMaintained is the exported check name for Maintained.
@ -41,9 +43,16 @@ func Maintained(c *checker.CheckRequest) checker.CheckResult {
}
// Set the raw results.
if c.RawResults != nil {
c.RawResults.MaintainedResults = rawData
pRawResults := getRawResults(c)
pRawResults.MaintainedResults = rawData
// Evaluate the probes.
findings, err := zrunner.Run(pRawResults, probes.Maintained)
if err != nil {
e := sce.WithMessage(sce.ErrScorecardInternal, err.Error())
return checker.CreateRuntimeErrorResult(CheckMaintained, e)
}
return evaluation.Maintained(CheckMaintained, c.Dlogger, &rawData)
// Return the score evaluation.
return evaluation.Maintained(CheckMaintained, findings, c.Dlogger)
}

View File

@ -36,6 +36,10 @@ import (
"github.com/ossf/scorecard/v4/probes/hasLicenseFile"
"github.com/ossf/scorecard/v4/probes/hasLicenseFileAtTopDir"
"github.com/ossf/scorecard/v4/probes/hasOSVVulnerabilities"
"github.com/ossf/scorecard/v4/probes/hasRecentCommits"
"github.com/ossf/scorecard/v4/probes/issueActivityByProjectMember"
"github.com/ossf/scorecard/v4/probes/notArchived"
"github.com/ossf/scorecard/v4/probes/notCreatedRecently"
"github.com/ossf/scorecard/v4/probes/packagedWithAutomatedWorkflow"
"github.com/ossf/scorecard/v4/probes/sastToolCodeQLInstalled"
"github.com/ossf/scorecard/v4/probes/sastToolRunsOnAllCommits"
@ -107,6 +111,12 @@ var (
hasDangerousWorkflowScriptInjection.Run,
hasDangerousWorkflowUntrustedCheckout.Run,
}
Maintained = []ProbeImpl{
notArchived.Run,
hasRecentCommits.Run,
issueActivityByProjectMember.Run,
notCreatedRecently.Run,
}
)
//nolint:gochecknoinits

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: hasRecentCommits
short: Check whether the project has at least one commit per week over the last 90 days.
motivation: >
A project which is not active might not be patched, have its dependencies patched, or be actively tested and used. However, a lack of active maintenance is not necessarily always a problem. Some software, especially smaller utility functions, does not normally need to be maintained. For example, a library that determines if an integer is even would not normally need maintenance unless an underlying implementation language definition changed. A lack of active maintenance should signal that potential users should investigate further to judge the situation. A project may not need further features or maintenance; In this case, the probe results can be disregarded.
implementation: >
The implementation checks the number of commits made in the last 90 days by any user type.
outcome:
- If the project has commits from the last 90 days, the probe returns one OutcomePositive with a "commitsWithinThreshold" value which contains the number of commits that the probe found within the threshold. The probe will also return a "lookBackDays" value which is the number of days that the probe includes in its threshold - which is 90.
- If the project does not have commits in the last 90 days, the probe returns a single OutcomeNegative.
remediation:
effort: Low
text:
- The only way to remediate this probe is to make contributions to the project, however, some projects have reached a level of maturity that does require further contributions.

View File

@ -0,0 +1,78 @@
// 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 hasRecentCommits
import (
"embed"
"fmt"
"time"
"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 (
lookBackDays = 90
)
const Probe = "hasRecentCommits"
func Run(raw *checker.RawResults) ([]finding.Finding, string, error) {
if raw == nil {
return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil)
}
var findings []finding.Finding
r := raw.MaintainedResults
threshold := time.Now().AddDate(0 /*years*/, 0 /*months*/, -1*lookBackDays /*days*/)
commitsWithinThreshold := 0
for i := range r.DefaultBranchCommits {
commit := r.DefaultBranchCommits[i]
if commit.CommittedDate.After(threshold) {
commitsWithinThreshold++
}
}
if commitsWithinThreshold > 0 {
f, err := finding.NewWith(fs, Probe,
"Found a contribution within the threshold.", nil,
finding.OutcomePositive)
if err != nil {
return nil, Probe, fmt.Errorf("create finding: %w", err)
}
f = f.WithValues(map[string]int{
"commitsWithinThreshold": commitsWithinThreshold,
"lookBackDays": 90,
})
findings = append(findings, *f)
} else {
f, err := finding.NewWith(fs, Probe,
"Did not find contribution within the threshold.", nil,
finding.OutcomeNegative)
if err != nil {
return nil, Probe, fmt.Errorf("create finding: %w", err)
}
findings = append(findings, *f)
}
return findings, Probe, nil
}

View File

@ -0,0 +1,130 @@
// 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 hasRecentCommits
import (
"testing"
"time"
"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 fiveCommitsInThreshold() []clients.Commit {
fiveCommitsInThreshold := make([]clients.Commit, 0)
for i := 0; i < 5; i++ {
commit := clients.Commit{
CommittedDate: time.Now().AddDate(0 /*years*/, 0 /*months*/, -1*i /*days*/),
}
fiveCommitsInThreshold = append(fiveCommitsInThreshold, commit)
}
return fiveCommitsInThreshold
}
func twentyCommitsInThresholdAndtwentyNot() []clients.Commit {
twentyCommitsInThresholdAndtwentyNot := make([]clients.Commit, 0)
for i := 70; i < 111; i++ {
commit := clients.Commit{
CommittedDate: time.Now().AddDate(0 /*years*/, 0 /*months*/, -1*i /*days*/),
}
twentyCommitsInThresholdAndtwentyNot = append(twentyCommitsInThresholdAndtwentyNot, commit)
}
return twentyCommitsInThresholdAndtwentyNot
}
func Test_Run(t *testing.T) {
t.Parallel()
//nolint:govet
tests := []struct {
name string
raw *checker.RawResults
outcomes []finding.Outcome
values map[string]int
err error
}{
{
name: "Has no issues in threshold",
raw: &checker.RawResults{
MaintainedResults: checker.MaintainedData{
Issues: []clients.Issue{},
},
},
outcomes: []finding.Outcome{finding.OutcomeNegative},
},
{
name: "Has five commits in threshold",
raw: &checker.RawResults{
MaintainedResults: checker.MaintainedData{
DefaultBranchCommits: fiveCommitsInThreshold(),
},
},
values: map[string]int{
"commitsWithinThreshold": 5,
"lookBackDays": 90,
},
outcomes: []finding.Outcome{finding.OutcomePositive},
},
{
name: "Has twenty in threshold",
raw: &checker.RawResults{
MaintainedResults: checker.MaintainedData{
DefaultBranchCommits: twentyCommitsInThresholdAndtwentyNot(),
},
},
values: map[string]int{
"commitsWithinThreshold": 20,
"lookBackDays": 90,
},
outcomes: []finding.Outcome{finding.OutcomePositive},
},
}
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 findings {
outcome := &tt.outcomes[i]
f := &findings[i]
if tt.values != nil {
if diff := cmp.Diff(tt.values, f.Values); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
}
if diff := cmp.Diff(*outcome, f.Outcome); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
}
})
}
}

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: issueActivityByProjectMember
short: Checks that a collaborator, member or owner has participated in issues in the last 90 days.
motivation: >
A project which is not active might not be patched, have its dependencies patched, or be actively tested and used. However, a lack of active maintenance is not necessarily always a problem. Some software, especially smaller utility functions, does not normally need to be maintained. For example, a library that determines if an integer is even would not normally need maintenance unless an underlying implementation language definition changed. A lack of active maintenance should signal that potential users should investigate further to judge the situation.
implementation: >
The probe checks whether collaborators, members or owners of a project have participated in issues in the last 90 days.
outcome:
- If collaborators, members or owners have participated in issues in the last 90 days, the probe returns one OutcomePositive. The probe also returns a "numberOfIssuesUpdatedWithinThreshold" value with represents the number of issues on the repository which project collaborators, members or owners have shown activity in.
- If collaborators, members or owners have NOT participated in issues in the last 90 days, the probe returns a single OutcomeNegative.
remediation:
effort: High
text:
- It is not possible for users of a project to affect the issue activity of collaborators, members or owners of a project.

View File

@ -0,0 +1,100 @@
// 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 issueActivityByProjectMember
import (
"embed"
"fmt"
"time"
"github.com/ossf/scorecard/v4/checker"
"github.com/ossf/scorecard/v4/clients"
"github.com/ossf/scorecard/v4/finding"
"github.com/ossf/scorecard/v4/probes/internal/utils/uerror"
)
//go:embed *.yml
var fs embed.FS
const (
lookBackDays = 90
)
const Probe = "issueActivityByProjectMember"
func Run(raw *checker.RawResults) ([]finding.Finding, string, error) {
if raw == nil {
return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil)
}
r := raw.MaintainedResults
numberOfIssuesUpdatedWithinThreshold := 0
// Look for activity in past `lookBackDays`.
threshold := time.Now().AddDate(0 /*years*/, 0 /*months*/, -1*lookBackDays /*days*/)
var findings []finding.Finding
for i := range r.Issues {
if hasActivityByCollaboratorOrHigher(&r.Issues[i], threshold) {
numberOfIssuesUpdatedWithinThreshold++
}
}
if numberOfIssuesUpdatedWithinThreshold > 0 {
f, err := finding.NewWith(fs, Probe,
"Found a issue within the threshold.", nil,
finding.OutcomePositive)
if err != nil {
return nil, Probe, fmt.Errorf("create finding: %w", err)
}
f = f.WithValues(map[string]int{
"numberOfIssuesUpdatedWithinThreshold": numberOfIssuesUpdatedWithinThreshold,
})
findings = append(findings, *f)
} else {
f, err := finding.NewWith(fs, Probe,
"Did not find issues within the threshold.", nil,
finding.OutcomeNegative)
if err != nil {
return nil, Probe, fmt.Errorf("create finding: %w", err)
}
findings = append(findings, *f)
}
return findings, Probe, nil
}
// hasActivityByCollaboratorOrHigher returns true if the issue was created or commented on by an
// owner/collaborator/member since the threshold.
func hasActivityByCollaboratorOrHigher(issue *clients.Issue, threshold time.Time) bool {
if issue == nil {
return false
}
if issue.AuthorAssociation.Gte(clients.RepoAssociationCollaborator) &&
issue.CreatedAt != nil && issue.CreatedAt.After(threshold) {
// The creator of the issue is a collaborator or higher.
return true
}
for _, comment := range issue.Comments {
if comment.AuthorAssociation.Gte(clients.RepoAssociationCollaborator) &&
comment.CreatedAt != nil &&
comment.CreatedAt.After(threshold) {
// The author of the comment is a collaborator or higher.
return true
}
}
return false
}

View File

@ -0,0 +1,235 @@
// 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 issueActivityByProjectMember
import (
"testing"
"time"
"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"
)
var (
collab = clients.RepoAssociationCollaborator
firstTimeUser = clients.RepoAssociationFirstTimeContributor
)
func fiveIssuesInThreshold() []clients.Issue {
fiveIssuesInThreshold := make([]clients.Issue, 0)
for i := 0; i < 5; i++ {
createdAt := time.Now().AddDate(0 /*years*/, 0 /*months*/, -1*i /*days*/)
commit := clients.Issue{
CreatedAt: &createdAt,
AuthorAssociation: &collab,
}
fiveIssuesInThreshold = append(fiveIssuesInThreshold, commit)
}
return fiveIssuesInThreshold
}
func fiveInThresholdByCollabAndFiveByFirstTimeUser() []clients.Issue {
fiveInThresholdByCollabAndFiveByFirstTimeUser := make([]clients.Issue, 0)
for i := 0; i < 10; i++ {
createdAt := time.Now().AddDate(0 /*years*/, 0 /*months*/, -1*i /*days*/)
commit := clients.Issue{
CreatedAt: &createdAt,
}
if i > 4 {
commit.AuthorAssociation = &collab
} else {
commit.AuthorAssociation = &firstTimeUser
}
fiveInThresholdByCollabAndFiveByFirstTimeUser = append(fiveInThresholdByCollabAndFiveByFirstTimeUser, commit)
}
return fiveInThresholdByCollabAndFiveByFirstTimeUser
}
func twentyIssuesInThresholdAndtwentyNot() []clients.Issue {
twentyIssuesInThresholdAndtwentyNot := make([]clients.Issue, 0)
for i := 70; i < 111; i++ {
createdAt := time.Now().AddDate(0 /*years*/, 0 /*months*/, -1*i /*days*/)
commit := clients.Issue{
CreatedAt: &createdAt,
AuthorAssociation: &collab,
}
twentyIssuesInThresholdAndtwentyNot = append(twentyIssuesInThresholdAndtwentyNot, commit)
}
return twentyIssuesInThresholdAndtwentyNot
}
func Test_Run(t *testing.T) {
t.Parallel()
//nolint:govet
tests := []struct {
name string
values map[string]int
raw *checker.RawResults
outcomes []finding.Outcome
err error
}{
{
name: "Has no issues in threshold",
raw: &checker.RawResults{
MaintainedResults: checker.MaintainedData{
Issues: []clients.Issue{},
},
},
outcomes: []finding.Outcome{finding.OutcomeNegative},
},
{
name: "Has 5 issues in threshold",
raw: &checker.RawResults{
MaintainedResults: checker.MaintainedData{
Issues: fiveIssuesInThreshold(),
},
},
values: map[string]int{
"numberOfIssuesUpdatedWithinThreshold": 5,
},
outcomes: []finding.Outcome{finding.OutcomePositive},
},
{
name: "Has 20 issues in threshold",
raw: &checker.RawResults{
MaintainedResults: checker.MaintainedData{
Issues: twentyIssuesInThresholdAndtwentyNot(),
},
},
values: map[string]int{
"numberOfIssuesUpdatedWithinThreshold": 20,
},
outcomes: []finding.Outcome{finding.OutcomePositive},
},
{
name: "Has 5 issues by collaborator and 5 by first time user",
raw: &checker.RawResults{
MaintainedResults: checker.MaintainedData{
Issues: fiveInThresholdByCollabAndFiveByFirstTimeUser(),
},
},
values: map[string]int{
"numberOfIssuesUpdatedWithinThreshold": 5,
},
outcomes: []finding.Outcome{finding.OutcomePositive},
},
}
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 findings {
outcome := &tt.outcomes[i]
f := &findings[i]
if tt.values != nil {
if diff := cmp.Diff(tt.values, f.Values); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
}
if diff := cmp.Diff(*outcome, f.Outcome); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
}
})
}
}
func Test_hasActivityByCollaboratorOrHigher(t *testing.T) {
t.Parallel()
r := clients.RepoAssociationCollaborator
twentDaysAgo := time.Now().AddDate(0 /*years*/, 0 /*months*/, -20 /*days*/)
type args struct {
issue *clients.Issue
threshold time.Time
}
tests := []struct { //nolint:govet
name string
args args
want bool
}{
{
name: "nil issue",
args: args{
issue: nil,
threshold: time.Now(),
},
want: false,
},
{
name: "repo-association collaborator",
args: args{
issue: &clients.Issue{
CreatedAt: nil,
AuthorAssociation: &r,
},
},
want: false,
},
{
name: "twentyDaysAgo",
args: args{
issue: &clients.Issue{
CreatedAt: &twentDaysAgo,
AuthorAssociation: &r,
},
},
want: true,
},
{
name: "repo-association collaborator with comment",
args: args{
issue: &clients.Issue{
CreatedAt: nil,
AuthorAssociation: &r,
Comments: []clients.IssueComment{
{
CreatedAt: &twentDaysAgo,
AuthorAssociation: &r,
},
},
},
},
want: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := hasActivityByCollaboratorOrHigher(tt.args.issue, tt.args.threshold); got != tt.want {
t.Errorf("hasActivityByCollaboratorOrHigher() = %v, want %v", got, tt.want)
}
})
}
}

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: notArchived
short: Check that the project is archvied
motivation: >
A project which is not active might not be patched, have its dependencies patched, or be actively tested and used. However, a lack of active maintenance is not necessarily always a problem. Some software, especially smaller utility functions, does not normally need to be maintained. For example, a library that determines if an integer is even would not normally need maintenance unless an underlying implementation language definition changed. A lack of active maintenance should signal that potential users should investigate further to judge the situation.
implementation: >
The probe checks the Archived Status of a project.
outcome:
- If the project is archived, the outcome is OutcomeNegative.
- If the project is not archived, the outcome is OutcomePositive.
remediation:
effort: High
text:
- Non-collaborators, members or owners cannot affect the outcome of this probe.

View File

@ -0,0 +1,63 @@
// 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 notArchived
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 = "notArchived"
func Run(raw *checker.RawResults) ([]finding.Finding, string, error) {
if raw == nil {
return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil)
}
r := raw.MaintainedResults
if r.ArchivedStatus.Status {
return negativeOutcome()
}
return positiveOutcome()
}
func negativeOutcome() ([]finding.Finding, string, error) {
f, err := finding.NewWith(fs, Probe,
"Repository is archived.", nil,
finding.OutcomeNegative)
if err != nil {
return nil, Probe, fmt.Errorf("create finding: %w", err)
}
return []finding.Finding{*f}, Probe, nil
}
func positiveOutcome() ([]finding.Finding, string, error) {
f, err := finding.NewWith(fs, Probe,
"Repository is not archived.", nil,
finding.OutcomePositive)
if err != nil {
return nil, Probe, fmt.Errorf("create finding: %w", err)
}
return []finding.Finding{*f}, Probe, nil
}

View File

@ -0,0 +1,91 @@
// 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 notArchived
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/finding"
)
func Test_Run(t *testing.T) {
t.Parallel()
//nolint:govet
tests := []struct {
name string
raw *checker.RawResults
outcomes []finding.Outcome
err error
}{
{
name: "Is archived",
raw: &checker.RawResults{
MaintainedResults: checker.MaintainedData{
ArchivedStatus: checker.ArchivedStatus{
Status: true,
},
},
},
outcomes: []finding.Outcome{
finding.OutcomeNegative,
},
},
{
name: "Is not archived",
raw: &checker.RawResults{
MaintainedResults: checker.MaintainedData{
ArchivedStatus: checker.ArchivedStatus{
Status: false,
},
},
},
outcomes: []finding.Outcome{
finding.OutcomePositive,
},
},
}
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,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: notCreatedRecently
short: Checks that the project was not created in the last 90 days.
motivation: >
When Scorecard checks the activity of a project in the last 90 days, the project may not have been created before the last 90 days. As such, Scorecard cannot give an accurate score. This probe helps Scorecard assess whether it can give an accurrate score when checking the project activity in the last 90 days.
implementation: >
The implementation checks the creation date is within the last 90 days.
outcome:
- If the project was created within the last 90 days, the outcome is OutcomeNegative (0).
- If the project was created before the last 90 days, the outcome is OutcomePositive (1). The finding will include a "lookBackDays" value which is the time period that the probe looks back in.
remediation:
effort: Low
text:
- The only remediation for this probe is to wait until 90 days have passed after a project has been created.

View File

@ -0,0 +1,73 @@
// 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 notCreatedRecently
import (
"embed"
"fmt"
"time"
"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 (
lookBackDays = 90
)
const Probe = "notCreatedRecently"
func Run(raw *checker.RawResults) ([]finding.Finding, string, error) {
if raw == nil {
return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil)
}
r := raw.MaintainedResults
recencyThreshold := time.Now().AddDate(0 /*years*/, 0 /*months*/, -1*lookBackDays /*days*/)
if r.CreatedAt.After(recencyThreshold) {
return negativeOutcome()
}
return positiveOutcome()
}
func negativeOutcome() ([]finding.Finding, string, error) {
f, err := finding.NewWith(fs, Probe,
"Repository was created in last 90 days.", nil,
finding.OutcomeNegative)
if err != nil {
return nil, Probe, fmt.Errorf("create finding: %w", err)
}
return []finding.Finding{*f}, Probe, nil
}
func positiveOutcome() ([]finding.Finding, string, error) {
f, err := finding.NewWith(fs, Probe,
"Repository was not created in last 90 days.", nil,
finding.OutcomePositive)
if err != nil {
return nil, Probe, fmt.Errorf("create finding: %w", err)
}
f = f.WithValues(map[string]int{
"lookBackDays": 90,
})
return []finding.Finding{*f}, Probe, nil
}

View File

@ -0,0 +1,88 @@
// 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 notCreatedRecently
import (
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/ossf/scorecard/v4/checker"
"github.com/ossf/scorecard/v4/finding"
)
func Test_Run(t *testing.T) {
t.Parallel()
//nolint:govet
tests := []struct {
name string
raw *checker.RawResults
outcomes []finding.Outcome
err error
}{
{
name: "Was created 10 days ago",
raw: &checker.RawResults{
MaintainedResults: checker.MaintainedData{
CreatedAt: time.Now().AddDate(0 /*years*/, 0 /*months*/, -10 /*days*/),
},
},
outcomes: []finding.Outcome{
finding.OutcomeNegative,
},
},
{
name: "Was creted 100 days ago",
raw: &checker.RawResults{
MaintainedResults: checker.MaintainedData{
CreatedAt: time.Now().AddDate(0 /*years*/, 0 /*months*/, -100 /*days*/),
},
},
outcomes: []finding.Outcome{
finding.OutcomePositive,
},
},
}
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)
}
}
})
}
}