mirror of
https://github.com/ossf/scorecard.git
synced 2024-09-11 00:45:36 +03:00
✨ Raw results for Contributors check (#1919)
* update * update * linter * linter
This commit is contained in:
parent
8fdb0e767e
commit
b4700ab5df
@ -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.
|
||||
|
@ -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)
|
||||
}
|
||||
|
75
checks/evaluation/contributors.go
Normal file
75
checks/evaluation/contributors.go
Normal 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)
|
||||
}
|
89
checks/raw/contributors.go
Normal file
89
checks/raw/contributors.go
Normal 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
|
||||
}
|
@ -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,
|
||||
|
@ -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())
|
||||
|
Loading…
Reference in New Issue
Block a user