mirror of
https://github.com/ossf/scorecard.git
synced 2024-09-11 00:45:36 +03:00
✨ Raw result for Maintained check (#1780)
* draft * draft * raw results for Maintained check * updates * updates * missing files * updates * unit tests * e2e tests * tests * linter * updates
This commit is contained in:
parent
682e6ea176
commit
037a3f3516
@ -25,6 +25,15 @@ type RawResults struct {
|
||||
DependencyUpdateToolResults DependencyUpdateToolData
|
||||
BranchProtectionResults BranchProtectionsData
|
||||
CodeReviewResults CodeReviewData
|
||||
MaintainedResults MaintainedData
|
||||
}
|
||||
|
||||
// MaintainedData contains the raw results
|
||||
// for the Maintained check.
|
||||
type MaintainedData struct {
|
||||
Issues []Issue
|
||||
DefaultBranchCommits []DefaultBranchCommit
|
||||
ArchivedStatus ArchivedStatus
|
||||
}
|
||||
|
||||
// CodeReviewData contains the raw results
|
||||
@ -107,9 +116,25 @@ type Run struct {
|
||||
// TODO: add fields, e.g., Result=["success", "failure"]
|
||||
}
|
||||
|
||||
// Comment represents a comment for a pull request or an issue.
|
||||
type Comment struct {
|
||||
CreatedAt *time.Time
|
||||
Author *User
|
||||
// TODO: add ields if needed, e.g., content.
|
||||
}
|
||||
|
||||
// ArchivedStatus definess the archived status.
|
||||
type ArchivedStatus struct {
|
||||
Status bool
|
||||
// TODO: add fields, e.g., date of archival.
|
||||
}
|
||||
|
||||
// Issue represents an issue.
|
||||
type Issue struct {
|
||||
URL string
|
||||
CreatedAt *time.Time
|
||||
Author *User
|
||||
URL string
|
||||
Comments []Comment
|
||||
// TODO: add fields, e.g., state=[opened|closed]
|
||||
}
|
||||
|
||||
@ -121,6 +146,7 @@ type DefaultBranchCommit struct {
|
||||
SHA string
|
||||
CommitMessage string
|
||||
MergeRequest *MergeRequest
|
||||
CommitDate *time.Time
|
||||
Committer User
|
||||
}
|
||||
|
||||
@ -143,9 +169,32 @@ type Review struct {
|
||||
|
||||
// User represent a user.
|
||||
type User struct {
|
||||
Login string
|
||||
RepoAssociation *RepoAssociation
|
||||
Login string
|
||||
}
|
||||
|
||||
// RepoAssociation represents a user relationship with a repo.
|
||||
type RepoAssociation string
|
||||
|
||||
const (
|
||||
// RepoAssociationCollaborator has been invited to collaborate on the repository.
|
||||
RepoAssociationCollaborator RepoAssociation = RepoAssociation("collaborator")
|
||||
// RepoAssociationContributor is an contributor to the repository.
|
||||
RepoAssociationContributor RepoAssociation = RepoAssociation("contributor")
|
||||
// RepoAssociationOwner is an owner of the repository.
|
||||
RepoAssociationOwner RepoAssociation = RepoAssociation("owner")
|
||||
// RepoAssociationMember is a member of the organization that owns the repository.
|
||||
RepoAssociationMember RepoAssociation = RepoAssociation("member")
|
||||
// RepoAssociationFirstTimer has previously committed to the repository.
|
||||
RepoAssociationFirstTimer RepoAssociation = RepoAssociation("first-timer")
|
||||
// RepoAssociationFirstTimeContributor has not previously committed to the repository.
|
||||
RepoAssociationFirstTimeContributor RepoAssociation = RepoAssociation("first-timer-contributor")
|
||||
// RepoAssociationMannequin is a placeholder for an unclaimed user.
|
||||
RepoAssociationMannequin RepoAssociation = RepoAssociation("unknown")
|
||||
// RepoAssociationNone has no association with the repository.
|
||||
RepoAssociationNone RepoAssociation = RepoAssociation("none")
|
||||
)
|
||||
|
||||
// File represents a file.
|
||||
type File struct {
|
||||
Path string
|
||||
|
@ -26,10 +26,10 @@ const (
|
||||
// CheckCIIBestPractices is the registered name for CIIBestPractices.
|
||||
CheckCIIBestPractices = "CII-Best-Practices"
|
||||
silverScore = 7
|
||||
// Note: if this value is changed, please update the action's threshold score
|
||||
// Note: if this value is changed, please update the action's threshold score
|
||||
// https://github.com/ossf/scorecard-action/blob/main/policies/template.yml#L61.
|
||||
passingScore = 5
|
||||
inProgressScore = 2
|
||||
passingScore = 5
|
||||
inProgressScore = 2
|
||||
)
|
||||
|
||||
//nolint:gochecknoinits
|
||||
|
103
checks/evaluation/maintained.go
Normal file
103
checks/evaluation/maintained.go
Normal file
@ -0,0 +1,103 @@
|
||||
// Copyright 2021 Security Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package evaluation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
sce "github.com/ossf/scorecard/v4/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
lookBackDays = 90
|
||||
activityPerWeek = 1
|
||||
daysInOneWeek = 7
|
||||
)
|
||||
|
||||
// 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")
|
||||
return checker.CreateRuntimeErrorResult(name, e)
|
||||
}
|
||||
|
||||
if r.ArchivedStatus.Status {
|
||||
return checker.CreateMinScoreResult(name, "repo is marked as archived")
|
||||
}
|
||||
|
||||
// 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].CommitDate.After(threshold) {
|
||||
commitsWithinThreshold++
|
||||
}
|
||||
}
|
||||
|
||||
issuesUpdatedWithinThreshold := 0
|
||||
for i := range r.Issues {
|
||||
if hasActivityByCollaboratorOrHigher(&r.Issues[i], threshold) {
|
||||
issuesUpdatedWithinThreshold++
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// hasActivityByCollaboratorOrHigher returns true if the issue was created or commented on by an
|
||||
// owner/collaborator/member since the threshold.
|
||||
func hasActivityByCollaboratorOrHigher(issue *checker.Issue, threshold time.Time) bool {
|
||||
if issue == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if isCollaboratorOrHigher(issue.Author) && 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 isCollaboratorOrHigher(comment.Author) && comment.CreatedAt != nil &&
|
||||
comment.CreatedAt.After(threshold) {
|
||||
// The author of the comment is a collaborator or higher.
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isCollaboratorOrHigher returns true if the user is a collaborator or higher.
|
||||
func isCollaboratorOrHigher(user *checker.User) bool {
|
||||
if user == nil || user.RepoAssociation == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
priviledgedRoles := []checker.RepoAssociation{
|
||||
checker.RepoAssociationOwner,
|
||||
checker.RepoAssociationCollaborator,
|
||||
checker.RepoAssociationContributor,
|
||||
checker.RepoAssociationMember,
|
||||
}
|
||||
for _, role := range priviledgedRoles {
|
||||
if role == *user.RepoAssociation {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
@ -15,108 +15,35 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
"github.com/ossf/scorecard/v4/checks/evaluation"
|
||||
"github.com/ossf/scorecard/v4/checks/raw"
|
||||
sce "github.com/ossf/scorecard/v4/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// CheckMaintained is the exported check name for Maintained.
|
||||
CheckMaintained = "Maintained"
|
||||
lookBackDays = 90
|
||||
activityPerWeek = 1
|
||||
daysInOneWeek = 7
|
||||
)
|
||||
// CheckMaintained is the exported check name for Maintained.
|
||||
const CheckMaintained = "Maintained"
|
||||
|
||||
//nolint:gochecknoinits
|
||||
func init() {
|
||||
if err := registerCheck(CheckMaintained, IsMaintained, nil); err != nil {
|
||||
if err := registerCheck(CheckMaintained, Maintained, nil); err != nil {
|
||||
// this should never happen
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// IsMaintained runs Maintained check.
|
||||
func IsMaintained(c *checker.CheckRequest) checker.CheckResult {
|
||||
archived, err := c.RepoClient.IsArchived()
|
||||
// Maintained runs Maintained check.
|
||||
func Maintained(c *checker.CheckRequest) checker.CheckResult {
|
||||
rawData, err := raw.Maintained(c)
|
||||
if err != nil {
|
||||
e := sce.WithMessage(sce.ErrScorecardInternal, err.Error())
|
||||
return checker.CreateRuntimeErrorResult(CheckMaintained, e)
|
||||
}
|
||||
if archived {
|
||||
return checker.CreateMinScoreResult(CheckMaintained, "repo is marked as archived")
|
||||
|
||||
// Set the raw results.
|
||||
if c.RawResults != nil {
|
||||
c.RawResults.MaintainedResults = rawData
|
||||
}
|
||||
|
||||
// If not explicitly marked archived, look for activity in past `lookBackDays`.
|
||||
threshold := time.Now().AddDate(0 /*years*/, 0 /*months*/, -1*lookBackDays /*days*/)
|
||||
|
||||
commits, err := c.RepoClient.ListCommits()
|
||||
if err != nil {
|
||||
e := sce.WithMessage(sce.ErrScorecardInternal, err.Error())
|
||||
return checker.CreateRuntimeErrorResult(CheckMaintained, e)
|
||||
}
|
||||
commitsWithinThreshold := 0
|
||||
for i := range commits {
|
||||
if commits[i].CommittedDate.After(threshold) {
|
||||
commitsWithinThreshold++
|
||||
}
|
||||
}
|
||||
|
||||
issues, err := c.RepoClient.ListIssues()
|
||||
if err != nil {
|
||||
e := sce.WithMessage(sce.ErrScorecardInternal, err.Error())
|
||||
return checker.CreateRuntimeErrorResult(CheckMaintained, e)
|
||||
}
|
||||
issuesUpdatedWithinThreshold := 0
|
||||
for i := range issues {
|
||||
if hasActivityByCollaboratorOrHigher(&issues[i], threshold) {
|
||||
issuesUpdatedWithinThreshold++
|
||||
}
|
||||
}
|
||||
|
||||
return checker.CreateProportionalScoreResult(CheckMaintained, fmt.Sprintf(
|
||||
"%d commit(s) out of %d and %d issue activity out of %d found in the last %d days",
|
||||
commitsWithinThreshold, len(commits), issuesUpdatedWithinThreshold, len(issues), lookBackDays),
|
||||
commitsWithinThreshold+issuesUpdatedWithinThreshold, 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 isCollaboratorOrHigher(issue.AuthorAssociation) && 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 isCollaboratorOrHigher(comment.AuthorAssociation) && comment.CreatedAt != nil &&
|
||||
comment.CreatedAt.After(threshold) {
|
||||
// The author of the comment is a collaborator or higher.
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isCollaboratorOrHigher returns true if the user is a collaborator or higher.
|
||||
func isCollaboratorOrHigher(repoAssociation *clients.RepoAssociation) bool {
|
||||
if repoAssociation == nil {
|
||||
return false
|
||||
}
|
||||
priviledgedRoles := []clients.RepoAssociation{
|
||||
clients.RepoAssociationCollaborator,
|
||||
clients.RepoAssociationMember,
|
||||
clients.RepoAssociationOwner,
|
||||
}
|
||||
for _, role := range priviledgedRoles {
|
||||
if role == *repoAssociation {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return evaluation.Maintained(CheckMaintained, c.Dlogger, &rawData)
|
||||
}
|
||||
|
@ -27,10 +27,10 @@ import (
|
||||
scut "github.com/ossf/scorecard/v4/utests"
|
||||
)
|
||||
|
||||
// nolint: gocognit
|
||||
// ignoring the linter for cyclomatic complexity because it is a test func
|
||||
// TestMaintained tests the maintained check.
|
||||
func TestMaintained(t *testing.T) {
|
||||
//nolint
|
||||
func Test_Maintained(t *testing.T) {
|
||||
t.Parallel()
|
||||
threeHundredDaysAgo := time.Now().AddDate(0, 0, -300)
|
||||
twoHundredDaysAgo := time.Now().AddDate(0, 0, -200)
|
||||
@ -38,7 +38,13 @@ func TestMaintained(t *testing.T) {
|
||||
oneDayAgo := time.Now().AddDate(0, 0, -1)
|
||||
ownerAssociation := clients.RepoAssociationOwner
|
||||
noneAssociation := clients.RepoAssociationNone
|
||||
//fieldalignment lint issue. Ignoring it as it is not important for this test.
|
||||
// fieldalignment lint issue. Ignoring it as it is not important for this test.
|
||||
someone := clients.User{
|
||||
Login: "someone",
|
||||
}
|
||||
otheruser := clients.User{
|
||||
Login: "someone-else",
|
||||
}
|
||||
//nolint
|
||||
tests := []struct {
|
||||
err error
|
||||
@ -85,7 +91,6 @@ func TestMaintained(t *testing.T) {
|
||||
Score: -1,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "repo with no commits or issues",
|
||||
isarchived: false,
|
||||
@ -125,10 +130,12 @@ func TestMaintained(t *testing.T) {
|
||||
{
|
||||
CreatedAt: &threeHundredDaysAgo,
|
||||
AuthorAssociation: &ownerAssociation,
|
||||
Author: &someone,
|
||||
},
|
||||
{
|
||||
CreatedAt: &twoHundredDaysAgo,
|
||||
AuthorAssociation: &noneAssociation,
|
||||
Author: &someone,
|
||||
},
|
||||
},
|
||||
expected: checker.CheckResult{
|
||||
@ -143,10 +150,12 @@ func TestMaintained(t *testing.T) {
|
||||
{
|
||||
CreatedAt: &fiveDaysAgo,
|
||||
AuthorAssociation: &noneAssociation,
|
||||
Author: &someone,
|
||||
},
|
||||
{
|
||||
CreatedAt: &oneDayAgo,
|
||||
AuthorAssociation: &noneAssociation,
|
||||
Author: &someone,
|
||||
},
|
||||
},
|
||||
expected: checker.CheckResult{
|
||||
@ -165,6 +174,7 @@ func TestMaintained(t *testing.T) {
|
||||
{
|
||||
CreatedAt: &oneDayAgo,
|
||||
AuthorAssociation: &noneAssociation,
|
||||
Author: &someone,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -175,6 +185,7 @@ func TestMaintained(t *testing.T) {
|
||||
{
|
||||
CreatedAt: &oneDayAgo,
|
||||
AuthorAssociation: &noneAssociation,
|
||||
Author: &someone,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -195,6 +206,7 @@ func TestMaintained(t *testing.T) {
|
||||
{
|
||||
CreatedAt: &twoHundredDaysAgo,
|
||||
AuthorAssociation: &ownerAssociation,
|
||||
Author: &someone,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -205,6 +217,7 @@ func TestMaintained(t *testing.T) {
|
||||
{
|
||||
CreatedAt: &twoHundredDaysAgo,
|
||||
AuthorAssociation: &ownerAssociation,
|
||||
Author: &someone,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -225,6 +238,7 @@ func TestMaintained(t *testing.T) {
|
||||
{
|
||||
CreatedAt: &fiveDaysAgo,
|
||||
AuthorAssociation: &ownerAssociation,
|
||||
Author: &someone,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -235,6 +249,7 @@ func TestMaintained(t *testing.T) {
|
||||
{
|
||||
CreatedAt: &oneDayAgo,
|
||||
AuthorAssociation: &ownerAssociation,
|
||||
Author: &someone,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -251,16 +266,38 @@ func TestMaintained(t *testing.T) {
|
||||
{
|
||||
CreatedAt: &fiveDaysAgo,
|
||||
AuthorAssociation: &ownerAssociation,
|
||||
Author: &someone,
|
||||
},
|
||||
{
|
||||
CreatedAt: &oneDayAgo,
|
||||
AuthorAssociation: &ownerAssociation,
|
||||
Author: &someone,
|
||||
},
|
||||
},
|
||||
expected: checker.CheckResult{
|
||||
Score: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "new issues by non-owner",
|
||||
isarchived: false,
|
||||
commits: []clients.Commit{},
|
||||
issues: []clients.Issue{
|
||||
{
|
||||
CreatedAt: &fiveDaysAgo,
|
||||
AuthorAssociation: &noneAssociation,
|
||||
Author: &otheruser,
|
||||
},
|
||||
{
|
||||
CreatedAt: &oneDayAgo,
|
||||
AuthorAssociation: &noneAssociation,
|
||||
Author: &otheruser,
|
||||
},
|
||||
},
|
||||
expected: checker.CheckResult{
|
||||
Score: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@ -279,7 +316,7 @@ func TestMaintained(t *testing.T) {
|
||||
return tt.isarchived, nil
|
||||
})
|
||||
|
||||
if !tt.isarchived {
|
||||
if tt.archiveerr == nil {
|
||||
mockRepo.EXPECT().ListCommits().DoAndReturn(
|
||||
func() ([]clients.Commit, error) {
|
||||
if tt.commiterr != nil {
|
||||
@ -288,6 +325,7 @@ func TestMaintained(t *testing.T) {
|
||||
return tt.commits, tt.err
|
||||
},
|
||||
).MinTimes(1)
|
||||
|
||||
if tt.commiterr == nil {
|
||||
mockRepo.EXPECT().ListIssues().DoAndReturn(
|
||||
func() ([]clients.Issue, error) {
|
||||
@ -304,7 +342,7 @@ func TestMaintained(t *testing.T) {
|
||||
RepoClient: mockRepo,
|
||||
}
|
||||
req.Dlogger = &scut.TestDetailLogger{}
|
||||
res := IsMaintained(&req)
|
||||
res := Maintained(&req)
|
||||
|
||||
if tt.err != nil {
|
||||
if res.Error2 == nil {
|
||||
|
@ -32,19 +32,20 @@ func CodeReview(c clients.RepoClient) (checker.CodeReviewData, error) {
|
||||
}
|
||||
|
||||
for i := range commits {
|
||||
results = append(results, getRawDataFrom(&commits[i]))
|
||||
results = append(results, getRawDataFromCommit(&commits[i]))
|
||||
}
|
||||
|
||||
return checker.CodeReviewData{DefaultBranchCommits: results}, nil
|
||||
}
|
||||
|
||||
func getRawDataFrom(c *clients.Commit) checker.DefaultBranchCommit {
|
||||
func getRawDataFromCommit(c *clients.Commit) checker.DefaultBranchCommit {
|
||||
r := checker.DefaultBranchCommit{
|
||||
Committer: checker.User{
|
||||
Login: c.Committer.Login,
|
||||
},
|
||||
SHA: c.SHA,
|
||||
CommitMessage: c.Message,
|
||||
CommitDate: &c.CommittedDate,
|
||||
MergeRequest: mergeRequest(&c.AssociatedMergeRequest),
|
||||
}
|
||||
|
||||
|
@ -191,8 +191,8 @@ func Test_mergeRequest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test_getRawDataFrom tests the getRawDataFrom function.
|
||||
func Test_getRawDataFrom(t *testing.T) {
|
||||
// Test_getRawDataFromCommit tests the getRawDataFromCommit function.
|
||||
func Test_getRawDataFromCommit(t *testing.T) {
|
||||
t.Parallel()
|
||||
type args struct {
|
||||
c *clients.Commit
|
||||
@ -203,7 +203,7 @@ func Test_getRawDataFrom(t *testing.T) {
|
||||
want checker.DefaultBranchCommit
|
||||
}{
|
||||
{
|
||||
name: "Test_getRawDataFrom",
|
||||
name: "Test_getRawDataFromCommit",
|
||||
args: args{
|
||||
c: &clients.Commit{
|
||||
CommittedDate: time.Time{},
|
||||
@ -214,6 +214,7 @@ func Test_getRawDataFrom(t *testing.T) {
|
||||
want: checker.DefaultBranchCommit{
|
||||
SHA: "sha",
|
||||
CommitMessage: "message",
|
||||
CommitDate: &time.Time{},
|
||||
MergeRequest: &checker.MergeRequest{
|
||||
Labels: []string{},
|
||||
Reviews: []checker.Review{},
|
||||
@ -226,7 +227,7 @@ func Test_getRawDataFrom(t *testing.T) {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := getRawDataFrom(tt.args.c); !cmp.Equal(got, tt.want) {
|
||||
if got := getRawDataFromCommit(tt.args.c); !cmp.Equal(got, tt.want) {
|
||||
t.Errorf(cmp.Diff(got, tt.want))
|
||||
}
|
||||
})
|
||||
|
124
checks/raw/maintained.go
Normal file
124
checks/raw/maintained.go
Normal file
@ -0,0 +1,124 @@
|
||||
// Copyright Security Scorecard Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package raw
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
"github.com/ossf/scorecard/v4/clients"
|
||||
)
|
||||
|
||||
// Maintained checks for maintenance.
|
||||
func Maintained(c *checker.CheckRequest) (checker.MaintainedData, error) {
|
||||
var result checker.MaintainedData
|
||||
|
||||
// Archived status.
|
||||
archived, err := c.RepoClient.IsArchived()
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("%w", err)
|
||||
}
|
||||
result.ArchivedStatus.Status = archived
|
||||
|
||||
// Recent commits.
|
||||
commits, err := c.RepoClient.ListCommits()
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("%w", err)
|
||||
}
|
||||
|
||||
for i := range commits {
|
||||
// Note: getRawDataFromCommit() is defined in Code-Review check.
|
||||
result.DefaultBranchCommits = append(result.DefaultBranchCommits,
|
||||
getRawDataFromCommit(&commits[i]))
|
||||
}
|
||||
|
||||
// Recent issues.
|
||||
issues, err := c.RepoClient.ListIssues()
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("%w", err)
|
||||
}
|
||||
|
||||
for i := range issues {
|
||||
// Create issue.
|
||||
issue := checker.Issue{
|
||||
CreatedAt: issues[i].CreatedAt,
|
||||
}
|
||||
// Add author if not nil.
|
||||
if issues[i].Author != nil {
|
||||
issue.Author = &checker.User{
|
||||
Login: issues[i].Author.Login,
|
||||
RepoAssociation: getAssociation(issues[i].AuthorAssociation),
|
||||
}
|
||||
}
|
||||
// Add URL if not nil.
|
||||
if issues[i].URI != nil {
|
||||
issue.URL = *issues[i].URI
|
||||
}
|
||||
|
||||
// Add comments.
|
||||
for j := range issues[i].Comments {
|
||||
comment := checker.Comment{
|
||||
CreatedAt: issues[i].Comments[j].CreatedAt,
|
||||
}
|
||||
if issues[i].Comments[j].Author != nil {
|
||||
comment.Author = &checker.User{
|
||||
Login: issues[i].Comments[j].Author.Login,
|
||||
RepoAssociation: getAssociation(issues[i].Comments[j].AuthorAssociation),
|
||||
}
|
||||
}
|
||||
|
||||
issue.Comments = append(issue.Comments, comment)
|
||||
}
|
||||
|
||||
result.Issues = append(result.Issues, issue)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getAssociation(a *clients.RepoAssociation) *checker.RepoAssociation {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch *a {
|
||||
case clients.RepoAssociationContributor:
|
||||
v := checker.RepoAssociationContributor
|
||||
return &v
|
||||
case clients.RepoAssociationCollaborator:
|
||||
v := checker.RepoAssociationCollaborator
|
||||
return &v
|
||||
case clients.RepoAssociationOwner:
|
||||
v := checker.RepoAssociationOwner
|
||||
return &v
|
||||
case clients.RepoAssociationMember:
|
||||
v := checker.RepoAssociationMember
|
||||
return &v
|
||||
case clients.RepoAssociationFirstTimer:
|
||||
v := checker.RepoAssociationFirstTimer
|
||||
return &v
|
||||
case clients.RepoAssociationMannequin:
|
||||
v := checker.RepoAssociationMannequin
|
||||
return &v
|
||||
case clients.RepoAssociationNone:
|
||||
v := checker.RepoAssociationNone
|
||||
return &v
|
||||
case clients.RepoAssociationFirstTimeContributor:
|
||||
v := checker.RepoAssociationFirstTimeContributor
|
||||
return &v
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
@ -100,11 +100,17 @@ type graphqlData struct {
|
||||
// nolint: revive,stylecheck // naming according to githubv4 convention.
|
||||
Url *string
|
||||
AuthorAssociation *string
|
||||
CreatedAt *time.Time
|
||||
Comments struct {
|
||||
Author struct {
|
||||
Login githubv4.String
|
||||
}
|
||||
CreatedAt *time.Time
|
||||
Comments struct {
|
||||
Nodes []struct {
|
||||
AuthorAssociation *string
|
||||
CreatedAt *time.Time
|
||||
Author struct {
|
||||
Login githubv4.String
|
||||
}
|
||||
}
|
||||
} `graphql:"comments(last: $issueCommentsToAnalyze)"`
|
||||
}
|
||||
@ -265,10 +271,20 @@ func issuesFrom(data *graphqlData) []clients.Issue {
|
||||
copyStringPtr(issue.Url, &tmpIssue.URI)
|
||||
copyRepoAssociationPtr(getRepoAssociation(issue.AuthorAssociation), &tmpIssue.AuthorAssociation)
|
||||
copyTimePtr(issue.CreatedAt, &tmpIssue.CreatedAt)
|
||||
if issue.Author.Login != "" {
|
||||
tmpIssue.Author = &clients.User{
|
||||
Login: string(issue.Author.Login),
|
||||
}
|
||||
}
|
||||
for _, comment := range issue.Comments.Nodes {
|
||||
var tmpComment clients.IssueComment
|
||||
copyRepoAssociationPtr(getRepoAssociation(comment.AuthorAssociation), &tmpComment.AuthorAssociation)
|
||||
copyTimePtr(comment.CreatedAt, &tmpComment.CreatedAt)
|
||||
if comment.Author.Login != "" {
|
||||
tmpComment.Author = &clients.User{
|
||||
Login: string(comment.Author.Login),
|
||||
}
|
||||
}
|
||||
tmpIssue.Comments = append(tmpIssue.Comments, tmpComment)
|
||||
}
|
||||
ret = append(ret, tmpIssue)
|
||||
|
@ -20,6 +20,7 @@ import "time"
|
||||
type Issue struct {
|
||||
URI *string
|
||||
CreatedAt *time.Time
|
||||
Author *User
|
||||
AuthorAssociation *RepoAssociation
|
||||
Comments []IssueComment
|
||||
}
|
||||
@ -27,5 +28,6 @@ type Issue struct {
|
||||
// IssueComment represents a comment on an issue.
|
||||
type IssueComment struct {
|
||||
CreatedAt *time.Time
|
||||
Author *User
|
||||
AuthorAssociation *RepoAssociation
|
||||
}
|
||||
|
@ -20,6 +20,7 @@
|
||||
package mockrepo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
@ -154,8 +155,11 @@ func (mr *MockRepoClientMockRecorder) ListCheckRunsForRef(ref interface{}) *gomo
|
||||
|
||||
// ListCommits mocks base method.
|
||||
func (m *MockRepoClient) ListCommits() ([]clients.Commit, error) {
|
||||
fmt.Println("mock.ListCommits")
|
||||
m.ctrl.T.Helper()
|
||||
fmt.Println("call mock.ListCommits")
|
||||
ret := m.ctrl.Call(m, "ListCommits")
|
||||
fmt.Println("ret mock.ListCommits")
|
||||
ret0, _ := ret[0].([]clients.Commit)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
@ -163,6 +167,7 @@ func (m *MockRepoClient) ListCommits() ([]clients.Commit, error) {
|
||||
|
||||
// ListCommits indicates an expected call of ListCommits.
|
||||
func (mr *MockRepoClientMockRecorder) ListCommits() *gomock.Call {
|
||||
fmt.Println("recorder.ListCommits")
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCommits", reflect.TypeOf((*MockRepoClient)(nil).ListCommits))
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ var _ = Describe("E2E TEST:"+checks.CheckMaintained, func() {
|
||||
NumberOfInfo: 0,
|
||||
NumberOfDebug: 0,
|
||||
}
|
||||
result := checks.IsMaintained(&req)
|
||||
result := checks.Maintained(&req)
|
||||
// UPGRADEv2: to remove.
|
||||
// Old version.
|
||||
Expect(result.Error).Should(BeNil())
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/ossf/scorecard/v4/checker"
|
||||
sce "github.com/ossf/scorecard/v4/errors"
|
||||
@ -70,7 +71,8 @@ type jsonReview struct {
|
||||
}
|
||||
|
||||
type jsonUser struct {
|
||||
Login string `json:"login"`
|
||||
RepoAssociation *string `json:"repo-association"`
|
||||
Login string `json:"login"`
|
||||
}
|
||||
|
||||
//nolint:govet
|
||||
@ -98,7 +100,29 @@ type jsonDatabaseVulnerability struct {
|
||||
// TODO: additional information
|
||||
}
|
||||
|
||||
type jsonArchivedStatus struct {
|
||||
Status bool `json:"status"`
|
||||
// TODO: add fields, e.g. date of archival, etc.
|
||||
}
|
||||
|
||||
type jsonComment struct {
|
||||
CreatedAt *time.Time `json:"created-at"`
|
||||
Author *jsonUser `json:"author"`
|
||||
// TODO: add ields if needed, e.g., content.
|
||||
}
|
||||
|
||||
type jsonIssue struct {
|
||||
CreatedAt *time.Time `json:"created-at"`
|
||||
Author *jsonUser `json:"author"`
|
||||
URL string `json:"URL"`
|
||||
Comments []jsonComment `json:"comments"`
|
||||
// TODO: add fields, e.g., state=[opened|closed]
|
||||
}
|
||||
|
||||
type jsonRawResults struct {
|
||||
// List of recent issues.
|
||||
RecentIssues []jsonIssue `json:"issues"`
|
||||
// List of vulnerabilities.
|
||||
DatabaseVulnerabilities []jsonDatabaseVulnerability `json:"database-vulnerabilities"`
|
||||
// List of binaries found in the repo.
|
||||
Binaries []jsonFile `json:"binaries"`
|
||||
@ -112,15 +136,76 @@ type jsonRawResults struct {
|
||||
BranchProtections []jsonBranchProtection `json:"branch-protections"`
|
||||
// Commits.
|
||||
DefaultBranchCommits []jsonDefaultBranchCommit `json:"default-branch-commits"`
|
||||
// Archived status of the repo.
|
||||
ArchivedStatus jsonArchivedStatus `json:"archived"`
|
||||
}
|
||||
|
||||
//nolint:unparam
|
||||
func (r *jsonScorecardRawResult) addCodeReviewRawResults(cr *checker.CodeReviewData) error {
|
||||
func getRepoAssociation(author *checker.User) *string {
|
||||
if author == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if author.RepoAssociation == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
s := string(*author.RepoAssociation)
|
||||
return &s
|
||||
}
|
||||
|
||||
func (r *jsonScorecardRawResult) addMaintainedRawResults(mr *checker.MaintainedData) error {
|
||||
// Set archived status.
|
||||
r.Results.ArchivedStatus = jsonArchivedStatus{Status: mr.ArchivedStatus.Status}
|
||||
|
||||
// Issues.
|
||||
for i := range mr.Issues {
|
||||
issue := jsonIssue{
|
||||
CreatedAt: mr.Issues[i].CreatedAt,
|
||||
URL: mr.Issues[i].URL,
|
||||
}
|
||||
|
||||
if mr.Issues[i].Author != nil {
|
||||
issue.Author = &jsonUser{
|
||||
Login: mr.Issues[i].Author.Login,
|
||||
RepoAssociation: getRepoAssociation(mr.Issues[i].Author),
|
||||
}
|
||||
}
|
||||
|
||||
for j := range mr.Issues[i].Comments {
|
||||
comment := jsonComment{
|
||||
CreatedAt: mr.Issues[i].Comments[j].CreatedAt,
|
||||
}
|
||||
|
||||
if mr.Issues[i].Comments[j].Author != nil {
|
||||
comment.Author = &jsonUser{
|
||||
Login: mr.Issues[i].Comments[j].Author.Login,
|
||||
RepoAssociation: getRepoAssociation(mr.Issues[i].Comments[j].Author),
|
||||
}
|
||||
}
|
||||
|
||||
issue.Comments = append(issue.Comments, comment)
|
||||
}
|
||||
|
||||
r.Results.RecentIssues = append(r.Results.RecentIssues, issue)
|
||||
}
|
||||
|
||||
return r.setDefaultCommitData(mr.DefaultBranchCommits)
|
||||
}
|
||||
|
||||
// Function shared between addMaintainedRawResults() and addCodeReviewRawResults().
|
||||
func (r *jsonScorecardRawResult) setDefaultCommitData(commits []checker.DefaultBranchCommit) error {
|
||||
if len(r.Results.DefaultBranchCommits) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
r.Results.DefaultBranchCommits = []jsonDefaultBranchCommit{}
|
||||
for _, commit := range cr.DefaultBranchCommits {
|
||||
for _, commit := range commits {
|
||||
com := jsonDefaultBranchCommit{
|
||||
Committer: jsonUser{
|
||||
Login: commit.Committer.Login,
|
||||
// Note: repo association is not available. We could
|
||||
// try to use issue information to set it, but we're likely to miss
|
||||
// many anyway.
|
||||
},
|
||||
CommitMessage: commit.CommitMessage,
|
||||
SHA: commit.SHA,
|
||||
@ -158,6 +243,10 @@ func (r *jsonScorecardRawResult) addCodeReviewRawResults(cr *checker.CodeReviewD
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *jsonScorecardRawResult) addCodeReviewRawResults(cr *checker.CodeReviewData) error {
|
||||
return r.setDefaultCommitData(cr.DefaultBranchCommits)
|
||||
}
|
||||
|
||||
//nolint:unparam
|
||||
func (r *jsonScorecardRawResult) addVulnerbilitiesRawResults(vd *checker.VulnerabilitiesData) error {
|
||||
r.Results.DatabaseVulnerabilities = []jsonDatabaseVulnerability{}
|
||||
@ -273,6 +362,11 @@ func (r *jsonScorecardRawResult) fillJSONRawResults(raw *checker.RawResults) err
|
||||
return sce.WithMessage(sce.ErrScorecardInternal, err.Error())
|
||||
}
|
||||
|
||||
// Maintained.
|
||||
if err := r.addMaintainedRawResults(&raw.MaintainedResults); err != nil {
|
||||
return sce.WithMessage(sce.ErrScorecardInternal, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -43,6 +43,7 @@ type RepoInfo struct {
|
||||
}
|
||||
|
||||
// ScorecardResult struct is returned on a successful Scorecard run.
|
||||
//nolint
|
||||
type ScorecardResult struct {
|
||||
Repo RepoInfo
|
||||
Date time.Time
|
||||
|
Loading…
Reference in New Issue
Block a user