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:
laurentsimon 2022-03-29 09:35:42 -07:00 committed by GitHub
parent 682e6ea176
commit 037a3f3516
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 471 additions and 110 deletions

View File

@ -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

View File

@ -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

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

View File

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

View File

@ -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 {

View File

@ -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),
}

View File

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

View File

@ -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)

View File

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

View File

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

View File

@ -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())

View File

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

View File

@ -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