Raw results for Contributors check (#1919)

* update

* update

* linter

* linter
This commit is contained in:
laurentsimon 2022-05-18 11:13:10 -07:00 committed by GitHub
parent 8fdb0e767e
commit b4700ab5df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 267 additions and 50 deletions

View File

@ -29,6 +29,7 @@ type RawResults struct {
BranchProtectionResults BranchProtectionsData
CodeReviewResults CodeReviewData
WebhookResults WebhooksData
ContributorsResults ContributorsData
MaintainedResults MaintainedData
SignedReleasesResults SignedReleasesData
LicenseResults LicenseData
@ -54,6 +55,11 @@ type CodeReviewData struct {
DefaultBranchCommits []DefaultBranchCommit
}
// ContributorsData represents contributor information.
type ContributorsData struct {
Users []User
}
// VulnerabilitiesData contains the raw results
// for the Vulnerabilities check.
type VulnerabilitiesData struct {
@ -194,8 +200,8 @@ type MergeRequest struct {
// Review represent a review using the built-in review system.
type Review struct {
Reviewer User
State string
Reviewer User
// TODO(Review): add fields here if needed.
}
@ -203,6 +209,23 @@ type Review struct {
type User struct {
RepoAssociation *RepoAssociation
Login string
// Orgnization refers to a GitHub org.
Organizations []Organization
// Companies refer to a claim by a user in their profile.
Companies []Company
NumContributions uint
}
// Organization represents a GitHub organization.
type Organization struct {
Login string
// TODO: other info.
}
// Company represents a company in a user's profile.
type Company struct {
Name string
// TODO: other info.
}
// RepoAssociation represents a user relationship with a repo.

View File

@ -15,19 +15,14 @@
package checks
import (
"fmt"
"strings"
"github.com/ossf/scorecard/v4/checker"
"github.com/ossf/scorecard/v4/checks/evaluation"
"github.com/ossf/scorecard/v4/checks/raw"
sce "github.com/ossf/scorecard/v4/errors"
)
const (
minContributionsPerUser = 5
numberCompaniesForTopScore = 3
// CheckContributors is the registered name for Contributors.
CheckContributors = "Contributors"
)
// CheckContributors is the registered name for Contributors.
const CheckContributors = "Contributors"
//nolint:gochecknoinits
func init() {
@ -39,44 +34,17 @@ func init() {
// Contributors run Contributors check.
func Contributors(c *checker.CheckRequest) checker.CheckResult {
contribs, err := c.RepoClient.ListContributors()
rawData, err := raw.Contributors(c.RepoClient)
if err != nil {
e := sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("Client.Repositories.ListContributors: %v", err))
e := sce.WithMessage(sce.ErrScorecardInternal, err.Error())
return checker.CreateRuntimeErrorResult(CheckContributors, e)
}
companies := map[string]struct{}{}
for _, contrib := range contribs {
if contrib.NumContributions < minContributionsPerUser {
continue
}
for _, org := range contrib.Organizations {
if org.Login != "" {
companies[org.Login] = struct{}{}
}
}
company := contrib.Company
if company != "" {
company = strings.ToLower(company)
company = strings.ReplaceAll(company, "inc.", "")
company = strings.ReplaceAll(company, "llc", "")
company = strings.ReplaceAll(company, ",", "")
company = strings.TrimLeft(company, "@")
company = strings.Trim(company, " ")
companies[company] = struct{}{}
}
}
names := []string{}
for c := range companies {
names = append(names, c)
// Return raw results.
if c.RawResults != nil {
c.RawResults.ContributorsResults = rawData
}
c.Dlogger.Info(&checker.LogMessage{
Text: fmt.Sprintf("contributors work for: %v", strings.Join(names, ",")),
})
reason := fmt.Sprintf("%d different companies found", len(companies))
return checker.CreateProportionalScoreResult(CheckContributors, reason, len(companies), numberCompaniesForTopScore)
// Return the score evaluation.
return evaluation.Contributors(CheckContributors, c.Dlogger, &rawData)
}

View File

@ -0,0 +1,75 @@
// 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"
"sort"
"strings"
"github.com/ossf/scorecard/v4/checker"
sce "github.com/ossf/scorecard/v4/errors"
)
const (
minContributionsPerUser = 5
numberCompaniesForTopScore = 3
)
// Contributors applies the score policy for the Contributors check.
func Contributors(name string, dl checker.DetailLogger,
r *checker.ContributorsData,
) checker.CheckResult {
if r == nil {
e := sce.WithMessage(sce.ErrScorecardInternal, "empty raw data")
return checker.CreateRuntimeErrorResult(name, e)
}
entities := make(map[string]bool)
for _, user := range r.Users {
if user.NumContributions < minContributionsPerUser {
continue
}
for _, org := range user.Organizations {
entities[org.Login] = true
}
for _, comp := range user.Companies {
entities[comp.Name] = true
}
}
names := []string{}
for c := range entities {
names = append(names, c)
}
sort.Strings(names)
if len(name) > 0 {
dl.Info(&checker.LogMessage{
Text: fmt.Sprintf("contributors work for %v", strings.Join(names, ",")),
})
} else {
dl.Warn(&checker.LogMessage{
Text: "no contributors have an org or company",
})
}
reason := fmt.Sprintf("%d different organizations found", len(entities))
return checker.CreateProportionalScoreResult(name, reason, len(entities), numberCompaniesForTopScore)
}

View File

@ -0,0 +1,89 @@
// Copyright 2020 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"
"strings"
"github.com/ossf/scorecard/v4/checker"
"github.com/ossf/scorecard/v4/clients"
)
// Contributors retrieves the raw data for the Contributors check.
func Contributors(c clients.RepoClient) (checker.ContributorsData, error) {
var users []checker.User
contribs, err := c.ListContributors()
if err != nil {
return checker.ContributorsData{}, fmt.Errorf("Client.Repositories.ListContributors: %w", err)
}
for _, contrib := range contribs {
user := checker.User{
Login: contrib.User.Login,
NumContributions: uint(contrib.NumContributions),
}
for _, org := range contrib.Organizations {
if org.Login != "" && !orgContains(user.Organizations, org.Login) {
user.Organizations = append(user.Organizations,
checker.Organization{
Login: org.Login,
},
)
}
}
company := contrib.Company
if company != "" {
company = strings.ToLower(company)
company = strings.ReplaceAll(company, "inc.", "")
company = strings.ReplaceAll(company, "llc", "")
company = strings.ReplaceAll(company, ",", "")
company = strings.TrimLeft(company, "@")
company = strings.Trim(company, " ")
if company != "" && !companyContains(user.Companies, company) {
user.Companies = append(user.Companies,
checker.Company{
Name: company,
},
)
}
}
users = append(users, user)
}
return checker.ContributorsData{Users: users}, nil
}
func companyContains(cs []checker.Company, name string) bool {
for _, a := range cs {
if a.Name == name {
return true
}
}
return false
}
func orgContains(os []checker.Organization, login string) bool {
for _, a := range os {
if a.Login == login {
return true
}
}
return false
}

View File

@ -118,7 +118,8 @@ type jsonRawResults struct {
//nolint:unparam
func addCodeReviewRawResults(r *jsonScorecardRawResult, cr *checker.CodeReviewData) error {
r.Results.DefaultBranchCommits = []jsonDefaultBranchCommit{}
for _, commit := range cr.DefaultBranchCommits {
for i := range cr.DefaultBranchCommits {
commit := cr.DefaultBranchCommits[i]
com := jsonDefaultBranchCommit{
Committer: jsonUser{
Login: commit.Committer.Login,

View File

@ -73,13 +73,33 @@ type jsonBranchProtection struct {
}
type jsonReview struct {
Reviewer jsonUser `json:"reviewer"`
State string `json:"state"`
Reviewer jsonUser `json:"reviewer"`
}
type jsonUser struct {
RepoAssociation *string `json:"repo-association"`
RepoAssociation *string `json:"repo-association,omitempty"`
Login string `json:"login"`
// Orgnization refers to a GitHub org.
Organizations []jsonOrganization `json:"organization,omitempty"`
// Companies refer to a claim by a user in their profile.
Companies []jsonCompany `json:"company,omitempty"`
NumContributions uint `json:"NumContributions"`
}
type jsonContributors struct {
Users []jsonUser `json:"users"`
// TODO: high-level statistics, etc
}
type jsonOrganization struct {
Login string `json:"login"`
// TODO: other info.
}
type jsonCompany struct {
Name string `json:"name"`
// TODO: other info.
}
//nolint:govet
@ -92,11 +112,10 @@ type jsonMergeRequest struct {
type jsonDefaultBranchCommit struct {
// ApprovedReviews *jsonApprovedReviews `json:"approved-reviews"`
Committer jsonUser `json:"committer"`
MergeRequest *jsonMergeRequest `json:"merge-request"`
CommitMessage string `json:"commit-message"`
SHA string `json:"sha"`
Committer jsonUser `json:"committer"`
// TODO: check runs, etc.
}
@ -189,6 +208,10 @@ type jsonRawResults struct {
DependencyUpdateTools []jsonTool `json:"dependency-update-tools"`
// Branch protection settings for development and release branches.
BranchProtections []jsonBranchProtection `json:"branch-protections"`
// Contributors. Note: we could use the list of commits instead to store this data.
// However, it's harder to get statistics using commit list, so we have a dedicated
// structure for it.
Contributors jsonContributors `json:"Contributors"`
// Commits.
DefaultBranchCommits []jsonDefaultBranchCommit `json:"default-branch-commits"`
// Archived status of the repo.
@ -231,6 +254,38 @@ func (r *jsonScorecardRawResult) addDangerousWorkflowRawResults(df *checker.Dang
return nil
}
//nolint:unparam
func (r *jsonScorecardRawResult) addContributorsRawResults(cr *checker.ContributorsData) error {
r.Results.Contributors = jsonContributors{}
for _, user := range cr.Users {
u := jsonUser{
Login: user.Login,
NumContributions: user.NumContributions,
}
for _, org := range user.Organizations {
u.Organizations = append(u.Organizations,
jsonOrganization{
Login: org.Login,
},
)
}
for _, comp := range user.Companies {
u.Companies = append(u.Companies,
jsonCompany{
Name: comp.Name,
},
)
}
r.Results.Contributors.Users = append(r.Results.Contributors.Users, u)
}
return nil
}
//nolint:unparam
func (r *jsonScorecardRawResult) addSignedReleasesRawResults(sr *checker.SignedReleasesData) error {
r.Results.Releases = []jsonRelease{}
@ -307,7 +362,8 @@ func (r *jsonScorecardRawResult) setDefaultCommitData(commits []checker.DefaultB
}
r.Results.DefaultBranchCommits = []jsonDefaultBranchCommit{}
for _, commit := range commits {
for i := range commits {
commit := commits[i]
com := jsonDefaultBranchCommit{
Committer: jsonUser{
Login: commit.Committer.Login,
@ -506,6 +562,11 @@ func (r *jsonScorecardRawResult) fillJSONRawResults(raw *checker.RawResults) err
return sce.WithMessage(sce.ErrScorecardInternal, err.Error())
}
// Contributors.
if err := r.addContributorsRawResults(&raw.ContributorsResults); err != nil {
return sce.WithMessage(sce.ErrScorecardInternal, err.Error())
}
// CII-Best-Practices.
if err := r.addOssfBestPracticesRawResults(&raw.CIIBestPracticesResults); err != nil {
return sce.WithMessage(sce.ErrScorecardInternal, err.Error())