diff --git a/checker/raw_result.go b/checker/raw_result.go index 8605af99..41d4aea2 100644 --- a/checker/raw_result.go +++ b/checker/raw_result.go @@ -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 diff --git a/checks/cii_best_practices.go b/checks/cii_best_practices.go index 44abaf3b..6e6e5ee6 100644 --- a/checks/cii_best_practices.go +++ b/checks/cii_best_practices.go @@ -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 diff --git a/checks/evaluation/maintained.go b/checks/evaluation/maintained.go new file mode 100644 index 00000000..b0da2441 --- /dev/null +++ b/checks/evaluation/maintained.go @@ -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 +} diff --git a/checks/maintained.go b/checks/maintained.go index 5a4142e0..c38cd917 100644 --- a/checks/maintained.go +++ b/checks/maintained.go @@ -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) } diff --git a/checks/maintained_test.go b/checks/maintained_test.go index c55c1e27..0edfd3f8 100644 --- a/checks/maintained_test.go +++ b/checks/maintained_test.go @@ -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 { diff --git a/checks/raw/code_review.go b/checks/raw/code_review.go index 8eb6453f..4227b2e1 100644 --- a/checks/raw/code_review.go +++ b/checks/raw/code_review.go @@ -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), } diff --git a/checks/raw/code_review_test.go b/checks/raw/code_review_test.go index 26705f14..87c2d2c6 100644 --- a/checks/raw/code_review_test.go +++ b/checks/raw/code_review_test.go @@ -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)) } }) diff --git a/checks/raw/maintained.go b/checks/raw/maintained.go new file mode 100644 index 00000000..c8dac8c1 --- /dev/null +++ b/checks/raw/maintained.go @@ -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 + } +} diff --git a/clients/githubrepo/graphql.go b/clients/githubrepo/graphql.go index bb3123ac..25663d78 100644 --- a/clients/githubrepo/graphql.go +++ b/clients/githubrepo/graphql.go @@ -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) diff --git a/clients/issue.go b/clients/issue.go index 412d06ca..ef687a31 100644 --- a/clients/issue.go +++ b/clients/issue.go @@ -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 } diff --git a/clients/mockclients/repo_client.go b/clients/mockclients/repo_client.go index 8f458bcb..ee2979e9 100644 --- a/clients/mockclients/repo_client.go +++ b/clients/mockclients/repo_client.go @@ -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)) } diff --git a/e2e/maintained_test.go b/e2e/maintained_test.go index d93ec85b..680e2ae3 100644 --- a/e2e/maintained_test.go +++ b/e2e/maintained_test.go @@ -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()) diff --git a/pkg/json_raw_results.go b/pkg/json_raw_results.go index 25420a4d..100fcd75 100644 --- a/pkg/json_raw_results.go +++ b/pkg/json_raw_results.go @@ -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 } diff --git a/pkg/scorecard_result.go b/pkg/scorecard_result.go index b3fd008b..d1ab57b6 100644 --- a/pkg/scorecard_result.go +++ b/pkg/scorecard_result.go @@ -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