Skip to content

Commit

Permalink
✨ Raw result for Maintained check (#1780)
Browse files Browse the repository at this point in the history
* draft

* draft

* raw results for Maintained check

* updates

* updates

* missing files

* updates

* unit tests

* e2e tests

* tests

* linter

* updates
  • Loading branch information
laurentsimon authored Mar 29, 2022
1 parent 682e6ea commit 037a3f3
Show file tree
Hide file tree
Showing 14 changed files with 471 additions and 110 deletions.
55 changes: 52 additions & 3 deletions checker/raw_result.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
}

Expand All @@ -121,6 +146,7 @@ type DefaultBranchCommit struct {
SHA string
CommitMessage string
MergeRequest *MergeRequest
CommitDate *time.Time
Committer User
}

Expand All @@ -143,8 +169,31 @@ 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 {
Expand Down
6 changes: 3 additions & 3 deletions checks/cii_best_practices.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
103 changes: 103 additions & 0 deletions checks/evaluation/maintained.go
Original file line number Diff line number Diff line change
@@ -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
}
97 changes: 12 additions & 85 deletions checks/maintained.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
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")
}

// 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()
// 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)
}
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
// Set the raw results.
if c.RawResults != nil {
c.RawResults.MaintainedResults = rawData
}
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)
}
Loading

0 comments on commit 037a3f3

Please sign in to comment.