mirror of
https://github.com/ossf/scorecard.git
synced 2024-09-11 08:55:27 +03:00
🌱 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:
parent
be0b915f76
commit
1c3d9eb6e7
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
27
probes/hasRecentCommits/def.yml
Normal file
27
probes/hasRecentCommits/def.yml
Normal file
@ -0,0 +1,27 @@
|
||||
# Copyright 2023 OpenSSF Scorecard Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
id: 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.
|
78
probes/hasRecentCommits/impl.go
Normal file
78
probes/hasRecentCommits/impl.go
Normal 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
|
||||
}
|
130
probes/hasRecentCommits/impl_test.go
Normal file
130
probes/hasRecentCommits/impl_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
27
probes/issueActivityByProjectMember/def.yml
Normal file
27
probes/issueActivityByProjectMember/def.yml
Normal file
@ -0,0 +1,27 @@
|
||||
# Copyright 2023 OpenSSF Scorecard Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
id: 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.
|
100
probes/issueActivityByProjectMember/impl.go
Normal file
100
probes/issueActivityByProjectMember/impl.go
Normal 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
|
||||
}
|
235
probes/issueActivityByProjectMember/impl_test.go
Normal file
235
probes/issueActivityByProjectMember/impl_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
27
probes/notArchived/def.yml
Normal file
27
probes/notArchived/def.yml
Normal file
@ -0,0 +1,27 @@
|
||||
# Copyright 2023 OpenSSF Scorecard Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
id: 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.
|
63
probes/notArchived/impl.go
Normal file
63
probes/notArchived/impl.go
Normal 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
|
||||
}
|
91
probes/notArchived/impl_test.go
Normal file
91
probes/notArchived/impl_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
27
probes/notCreatedRecently/def.yml
Normal file
27
probes/notCreatedRecently/def.yml
Normal file
@ -0,0 +1,27 @@
|
||||
# Copyright 2023 OpenSSF Scorecard Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
id: 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.
|
73
probes/notCreatedRecently/impl.go
Normal file
73
probes/notCreatedRecently/impl.go
Normal 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
|
||||
}
|
88
probes/notCreatedRecently/impl_test.go
Normal file
88
probes/notCreatedRecently/impl_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user