mirror of
https://github.com/ossf/scorecard.git
synced 2024-09-11 08:55:27 +03:00
Initial commit.
This commit is contained in:
commit
3ee3c748e9
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# IDE directories
|
||||
.vscode/
|
201
LICENSE
Normal file
201
LICENSE
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
56
README.md
Normal file
56
README.md
Normal file
@ -0,0 +1,56 @@
|
||||
# Open Source Scorecards
|
||||
|
||||
Motivation: <TODO>
|
||||
|
||||
## Usage
|
||||
|
||||
The program only requires one argument to run, the name of the repo:
|
||||
|
||||
```shell
|
||||
$ scorecards --repo=github.com/kubernetes/kubernetes
|
||||
Security-MD 10 true
|
||||
Contributors 10 true
|
||||
Signed-Tags 7 false
|
||||
Signed-Releases 0 false
|
||||
Code-Review 10 true
|
||||
CI-Tests 10 true
|
||||
Frozen-Deps 10 true
|
||||
```
|
||||
|
||||
You'll probably also need to set an Oauth token to avoid rate limits.
|
||||
You can create a personal access token by following these steps: https://docs.github.com/en/free-pro-team@latest/developers/apps/about-apps#personal-access-tokens
|
||||
|
||||
Set that as an environment variable:
|
||||
|
||||
```shell
|
||||
export GITHUB_OAUTH_TOKEN=<your token>
|
||||
```
|
||||
|
||||
## Checks
|
||||
|
||||
The following checks are all run against the target project:
|
||||
|
||||
| Name | Description |
|
||||
|---|---|
|
||||
| Security-MD | Does the project contain security policies? |
|
||||
| Contributors | Does the project have contributors from at least two different organizations? |
|
||||
| Frozen-Deps | Does the project declare and freeze dependencies? |
|
||||
| Signed-Tags | Does the project cryptographically sign release tags? |
|
||||
| Signed-Releases | Does the project cryptographically sign releases? |
|
||||
| CI-Tests | Does the project run tests in CI? |
|
||||
| Code-Review | Does the project require code review before code is merged? |
|
||||
|
||||
To see detailed information on how each check works, see the check-specific documentation pages.
|
||||
|
||||
## Results
|
||||
|
||||
Each check returns a pass/fail decision, as well as a confidence score between 0 and 10.
|
||||
A confidence of 0 should indicate the check was unable to achieve any real signal, and the result
|
||||
should be ignored.
|
||||
A confidence of 10 indicates the check is completely sure of the result.
|
||||
|
||||
Many of the checks are based on heuristics, contributions are welcome to improve the detection!
|
||||
|
||||
## Contributing
|
||||
|
||||
See the [Contributing](contributing.md) documentation for guidance on how to contribute.
|
15
checker/checker.go
Normal file
15
checker/checker.go
Normal file
@ -0,0 +1,15 @@
|
||||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/go-github/v32/github"
|
||||
)
|
||||
|
||||
type Checker struct {
|
||||
Ctx context.Context
|
||||
Client *github.Client
|
||||
HttpClient *http.Client
|
||||
Owner, Repo string
|
||||
}
|
55
checks/check.go
Normal file
55
checks/check.go
Normal file
@ -0,0 +1,55 @@
|
||||
package checks
|
||||
|
||||
import "github.com/dlorenc/scorecard/checker"
|
||||
|
||||
type CheckResult struct {
|
||||
Pass bool
|
||||
Message string
|
||||
Confidence int
|
||||
ShouldRetry bool
|
||||
Error error
|
||||
}
|
||||
|
||||
var InconclusiveResult = CheckResult{
|
||||
Pass: false,
|
||||
Confidence: 0,
|
||||
}
|
||||
|
||||
var retryResult = CheckResult{
|
||||
Pass: false,
|
||||
ShouldRetry: true,
|
||||
}
|
||||
|
||||
func RetryResult(err error) CheckResult {
|
||||
r := retryResult
|
||||
r.Error = err
|
||||
return r
|
||||
}
|
||||
|
||||
type CheckFn func(*checker.Checker) CheckResult
|
||||
|
||||
func MultiCheck(fns ...CheckFn) CheckFn {
|
||||
threshold := 7
|
||||
|
||||
return func(c *checker.Checker) CheckResult {
|
||||
var maxResult CheckResult
|
||||
|
||||
for _, fn := range fns {
|
||||
result := fn(c)
|
||||
if result.Confidence > threshold {
|
||||
return result
|
||||
}
|
||||
if result.Confidence >= maxResult.Confidence {
|
||||
maxResult = result
|
||||
}
|
||||
}
|
||||
return maxResult
|
||||
}
|
||||
}
|
||||
|
||||
type NamedCheck struct {
|
||||
Name string
|
||||
Fn CheckFn
|
||||
}
|
||||
|
||||
var AllChecks = []NamedCheck{}
|
127
checks/codereview.go
Normal file
127
checks/codereview.go
Normal file
@ -0,0 +1,127 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"github.com/dlorenc/scorecard/checker"
|
||||
"github.com/google/go-github/v32/github"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AllChecks = append(AllChecks, NamedCheck{
|
||||
Name: "Code-Review",
|
||||
Fn: DoesCodeReview,
|
||||
})
|
||||
}
|
||||
|
||||
// DoesCodeReview attempts to determine whether a project requires review before code gets merged.
|
||||
// It uses a set of heuristics:
|
||||
// - Looking at the repo configuration to see if reviews are required
|
||||
// - Checking if most of the recent merged PRs were "Approved"
|
||||
// - Looking for other well-known review labels
|
||||
func DoesCodeReview(c *checker.Checker) CheckResult {
|
||||
return MultiCheck(
|
||||
IsPrReviewRequired,
|
||||
GithubCodeReview,
|
||||
ProwCodeReview,
|
||||
)(c)
|
||||
}
|
||||
|
||||
func GithubCodeReview(c *checker.Checker) CheckResult {
|
||||
// Look at some merged PRs to see if they were reviewed
|
||||
prs, _, err := c.Client.PullRequests.List(c.Ctx, c.Owner, c.Repo, &github.PullRequestListOptions{
|
||||
State: "closed",
|
||||
})
|
||||
if err != nil {
|
||||
return InconclusiveResult
|
||||
}
|
||||
|
||||
totalMerged := 0
|
||||
totalReviewed := 0
|
||||
for _, pr := range prs {
|
||||
if pr.MergedAt == nil {
|
||||
continue
|
||||
}
|
||||
totalMerged++
|
||||
// Merged PR!
|
||||
reviews, _, err := c.Client.PullRequests.ListReviews(c.Ctx, c.Owner, c.Repo, pr.GetNumber(), &github.ListOptions{})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, r := range reviews {
|
||||
if r.GetState() == "APPROVED" {
|
||||
totalReviewed++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Threshold is 3/4 of merged PRs
|
||||
actual := float32(totalReviewed) / float32(totalMerged)
|
||||
if actual >= .75 {
|
||||
return CheckResult{
|
||||
Pass: true,
|
||||
Confidence: int(actual * 10),
|
||||
}
|
||||
}
|
||||
return CheckResult{
|
||||
Pass: false,
|
||||
Confidence: int(10 - int(actual*10)),
|
||||
}
|
||||
}
|
||||
|
||||
func IsPrReviewRequired(c *checker.Checker) CheckResult {
|
||||
// Look to see if review is enforced.
|
||||
r, _, err := c.Client.Repositories.Get(c.Ctx, c.Owner, c.Repo)
|
||||
if err != nil {
|
||||
return RetryResult(err)
|
||||
}
|
||||
|
||||
// Check the branch protection rules, we may not be able to get these though.
|
||||
bp, _, err := c.Client.Repositories.GetBranchProtection(c.Ctx, c.Owner, c.Repo, r.GetDefaultBranch())
|
||||
if err != nil {
|
||||
return InconclusiveResult
|
||||
}
|
||||
if bp.GetRequiredPullRequestReviews().RequiredApprovingReviewCount >= 1 {
|
||||
return CheckResult{
|
||||
Pass: true,
|
||||
Confidence: 10,
|
||||
}
|
||||
}
|
||||
return InconclusiveResult
|
||||
}
|
||||
|
||||
func ProwCodeReview(c *checker.Checker) CheckResult {
|
||||
// Look at some merged PRs to see if they were reviewed
|
||||
prs, _, err := c.Client.PullRequests.List(c.Ctx, c.Owner, c.Repo, &github.PullRequestListOptions{
|
||||
State: "closed",
|
||||
})
|
||||
if err != nil {
|
||||
return InconclusiveResult
|
||||
}
|
||||
|
||||
totalMerged := 0
|
||||
totalReviewed := 0
|
||||
for _, pr := range prs {
|
||||
if pr.MergedAt == nil {
|
||||
continue
|
||||
}
|
||||
totalMerged++
|
||||
for _, l := range pr.Labels {
|
||||
if l.GetName() == "lgtm" || l.GetName() == "approved" {
|
||||
totalReviewed++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// Threshold is 3/4 of merged PRs
|
||||
actual := float32(totalReviewed) / float32(totalMerged)
|
||||
if actual >= .75 {
|
||||
return CheckResult{
|
||||
Pass: true,
|
||||
Confidence: int(actual * 10),
|
||||
}
|
||||
}
|
||||
return CheckResult{
|
||||
Pass: false,
|
||||
Confidence: int(10 - int(actual*10)),
|
||||
}
|
||||
}
|
43
checks/contributors.go
Normal file
43
checks/contributors.go
Normal file
@ -0,0 +1,43 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"github.com/dlorenc/scorecard/checker"
|
||||
"github.com/google/go-github/v32/github"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AllChecks = append(AllChecks, NamedCheck{
|
||||
Name: "Contributors",
|
||||
Fn: Contributors,
|
||||
})
|
||||
}
|
||||
|
||||
func Contributors(c *checker.Checker) CheckResult {
|
||||
contribs, _, err := c.Client.Repositories.ListContributors(c.Ctx, c.Owner, c.Repo, &github.ListContributorsOptions{})
|
||||
if err != nil {
|
||||
return RetryResult(err)
|
||||
}
|
||||
|
||||
companies := map[string]struct{}{}
|
||||
for _, contrib := range contribs {
|
||||
if contrib.GetContributions() >= 5 {
|
||||
u, _, err := c.Client.Users.Get(c.Ctx, contrib.GetLogin())
|
||||
if err != nil {
|
||||
return RetryResult(err)
|
||||
}
|
||||
if u.GetCompany() != "" {
|
||||
companies[u.GetCompany()] = struct{}{}
|
||||
}
|
||||
}
|
||||
if len(companies) > 2 {
|
||||
return CheckResult{
|
||||
Pass: true,
|
||||
Confidence: 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
return CheckResult{
|
||||
Pass: false,
|
||||
Confidence: 10,
|
||||
}
|
||||
}
|
88
checks/deps.go
Normal file
88
checks/deps.go
Normal file
@ -0,0 +1,88 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/dlorenc/scorecard/checker"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AllChecks = append(AllChecks, NamedCheck{
|
||||
Name: "Frozen-Deps",
|
||||
Fn: FrozenDeps,
|
||||
})
|
||||
}
|
||||
|
||||
func FrozenDeps(c *checker.Checker) CheckResult {
|
||||
r, _, err := c.Client.Repositories.Get(c.Ctx, c.Owner, c.Repo)
|
||||
if err != nil {
|
||||
return RetryResult(err)
|
||||
}
|
||||
url := r.GetArchiveURL()
|
||||
url = strings.Replace(url, "{archive_format}", "tarball", 1)
|
||||
url = strings.Replace(url, "{/ref}", r.GetDefaultBranch(), 1)
|
||||
|
||||
// Download
|
||||
resp, err := c.HttpClient.Get(url)
|
||||
if err != nil {
|
||||
return RetryResult(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
gz, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return RetryResult(err)
|
||||
}
|
||||
tr := tar.NewReader(gz)
|
||||
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
// Strip the repo name
|
||||
names := strings.SplitN(hdr.Name, "/", 2)
|
||||
if len(names) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
name := names[1]
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return RetryResult(err)
|
||||
}
|
||||
|
||||
switch strings.ToLower(name) {
|
||||
case "go.mod", "go.sum":
|
||||
return CheckResult{
|
||||
Pass: true,
|
||||
Confidence: 10,
|
||||
}
|
||||
case "vendor", "third_party":
|
||||
return CheckResult{
|
||||
Pass: true,
|
||||
Confidence: 10,
|
||||
}
|
||||
case "package-lock.json":
|
||||
return CheckResult{
|
||||
Pass: true,
|
||||
Confidence: 10,
|
||||
}
|
||||
case "requirements.txt":
|
||||
return CheckResult{
|
||||
Pass: true,
|
||||
Confidence: 10,
|
||||
}
|
||||
case "gemfile.lock":
|
||||
return CheckResult{
|
||||
Pass: true,
|
||||
Confidence: 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
return CheckResult{
|
||||
Pass: false,
|
||||
Confidence: 5,
|
||||
}
|
||||
}
|
63
checks/releases.go
Normal file
63
checks/releases.go
Normal file
@ -0,0 +1,63 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/dlorenc/scorecard/checker"
|
||||
"github.com/google/go-github/v32/github"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AllChecks = append(AllChecks, NamedCheck{
|
||||
Name: "Signed-Releases",
|
||||
Fn: SignedReleases,
|
||||
})
|
||||
}
|
||||
|
||||
func SignedReleases(c *checker.Checker) CheckResult {
|
||||
releases, _, err := c.Client.Repositories.ListReleases(c.Ctx, c.Owner, c.Repo, &github.ListOptions{})
|
||||
if err != nil {
|
||||
return RetryResult(err)
|
||||
}
|
||||
|
||||
totalReleases := 0
|
||||
totalSigned := 0
|
||||
for _, r := range releases {
|
||||
assets, _, err := c.Client.Repositories.ListReleaseAssets(c.Ctx, c.Owner, c.Repo, r.GetID(), &github.ListOptions{})
|
||||
if err != nil {
|
||||
return RetryResult(err)
|
||||
}
|
||||
if len(assets) <= 1 {
|
||||
continue
|
||||
}
|
||||
totalReleases++
|
||||
signed := false
|
||||
for _, asset := range assets {
|
||||
for _, suffix := range []string{".sig", ".minisig"} {
|
||||
if strings.HasSuffix(asset.GetName(), suffix) {
|
||||
signed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if signed {
|
||||
totalSigned++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if totalReleases == 0 {
|
||||
return InconclusiveResult
|
||||
}
|
||||
actual := float32(totalSigned) / float32(totalReleases)
|
||||
if actual >= .75 {
|
||||
return CheckResult{
|
||||
Pass: true,
|
||||
Confidence: int(actual * 10),
|
||||
}
|
||||
}
|
||||
return CheckResult{
|
||||
Pass: false,
|
||||
Confidence: int(10 - int(actual*10)),
|
||||
}
|
||||
}
|
31
checks/security.go
Normal file
31
checks/security.go
Normal file
@ -0,0 +1,31 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"github.com/dlorenc/scorecard/checker"
|
||||
"github.com/google/go-github/v32/github"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AllChecks = append(AllChecks, NamedCheck{
|
||||
Name: "Security-MD",
|
||||
Fn: Securitymd,
|
||||
})
|
||||
}
|
||||
|
||||
func Securitymd(c *checker.Checker) CheckResult {
|
||||
for _, fp := range []string{".github/SECURITY.md", ".github/security.md", "security.md", "SECURITY.md"} {
|
||||
dc, err := c.Client.Repositories.DownloadContents(c.Ctx, c.Owner, c.Repo, fp, &github.RepositoryContentGetOptions{})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
dc.Close()
|
||||
return CheckResult{
|
||||
Pass: true,
|
||||
Confidence: 10,
|
||||
}
|
||||
}
|
||||
return CheckResult{
|
||||
Pass: false,
|
||||
Confidence: 10,
|
||||
}
|
||||
}
|
46
checks/tags.go
Normal file
46
checks/tags.go
Normal file
@ -0,0 +1,46 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"github.com/dlorenc/scorecard/checker"
|
||||
"github.com/google/go-github/v32/github"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AllChecks = append(AllChecks, NamedCheck{
|
||||
Name: "Signed-Tags",
|
||||
Fn: SignedTags,
|
||||
})
|
||||
}
|
||||
|
||||
func SignedTags(c *checker.Checker) CheckResult {
|
||||
tags, _, err := c.Client.Repositories.ListTags(c.Ctx, c.Owner, c.Repo, &github.ListOptions{})
|
||||
if err != nil {
|
||||
return RetryResult(err)
|
||||
}
|
||||
|
||||
totalReleases := 0
|
||||
totalSigned := 0
|
||||
for _, t := range tags {
|
||||
totalReleases++
|
||||
gt, _, err := c.Client.Git.GetCommit(c.Ctx, c.Owner, c.Repo, t.GetCommit().GetSHA())
|
||||
if err != nil {
|
||||
return RetryResult(err)
|
||||
}
|
||||
if gt.GetVerification().GetVerified() {
|
||||
totalSigned++
|
||||
}
|
||||
}
|
||||
|
||||
// Threshold is 3/4 of releases
|
||||
actual := float32(totalSigned) / float32(totalReleases)
|
||||
if actual >= .75 {
|
||||
return CheckResult{
|
||||
Pass: true,
|
||||
Confidence: int(actual * 10),
|
||||
}
|
||||
}
|
||||
return CheckResult{
|
||||
Pass: false,
|
||||
Confidence: int(10 - int(actual*10)),
|
||||
}
|
||||
}
|
66
checks/tests.go
Normal file
66
checks/tests.go
Normal file
@ -0,0 +1,66 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/dlorenc/scorecard/checker"
|
||||
"github.com/google/go-github/v32/github"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AllChecks = append(AllChecks, NamedCheck{
|
||||
Name: "CI-Tests",
|
||||
Fn: GithubChecks,
|
||||
})
|
||||
}
|
||||
|
||||
func GithubChecks(c *checker.Checker) CheckResult {
|
||||
prs, _, err := c.Client.PullRequests.List(c.Ctx, c.Owner, c.Repo, &github.PullRequestListOptions{
|
||||
State: "closed",
|
||||
})
|
||||
if err != nil {
|
||||
return RetryResult(err)
|
||||
}
|
||||
|
||||
totalMerged := 0
|
||||
totalTested := 0
|
||||
for _, pr := range prs {
|
||||
if pr.MergedAt == nil {
|
||||
continue
|
||||
}
|
||||
totalMerged++
|
||||
statuses, _, err := c.Client.Repositories.ListStatuses(c.Ctx, c.Owner, c.Repo, pr.GetHead().GetSHA(), &github.ListOptions{})
|
||||
if err != nil {
|
||||
return RetryResult(err)
|
||||
}
|
||||
for _, status := range statuses {
|
||||
if status.GetState() != "success" {
|
||||
continue
|
||||
}
|
||||
c := status.GetContext()
|
||||
hadTest := false
|
||||
for _, pattern := range []string{"travis-ci", "buildkite", "e2e"} {
|
||||
if strings.Contains(c, pattern) {
|
||||
hadTest = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hadTest {
|
||||
totalTested++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// Threshold is 3/4 of merged PRs
|
||||
actual := float32(totalTested) / float32(totalMerged)
|
||||
if actual >= .75 {
|
||||
return CheckResult{
|
||||
Pass: true,
|
||||
Confidence: int(actual * 10),
|
||||
}
|
||||
}
|
||||
return CheckResult{
|
||||
Pass: false,
|
||||
Confidence: int(10 - int(actual*10)),
|
||||
}
|
||||
}
|
66
contributing.md
Normal file
66
contributing.md
Normal file
@ -0,0 +1,66 @@
|
||||
# Contributing to OSS Scorecards!
|
||||
|
||||
Thank you for contributing your time and expertise to the OSS Scorecards project.
|
||||
This document describes the contribution guidelines for the project.
|
||||
|
||||
**Note:** Before you start contributing, you must read and abide by our **[Code of Conduct](./code-of-conduct.md)**.
|
||||
|
||||
## Contributing code
|
||||
|
||||
### Getting started
|
||||
|
||||
1. Create [a GitHub account](https://github.com/join)
|
||||
1. Create a [personal access token](https://docs.github.com/en/free-pro-team@latest/developers/apps/about-apps#personal-access-tokens)
|
||||
1. Set up your [development environment](#environment-setup)
|
||||
|
||||
Then you can [iterate](#iterating).
|
||||
|
||||
## Environment Setup
|
||||
|
||||
You must install these tools:
|
||||
|
||||
1. [`git`](https://help.github.com/articles/set-up-git/): For source control
|
||||
|
||||
1. [`go`](https://golang.org/doc/install): The language Tekton Pipelines is
|
||||
built in. You need go version [v1.15](https://golang.org/dl/) or higher.
|
||||
|
||||
## Iterating
|
||||
|
||||
You can build the project with:
|
||||
|
||||
```shell
|
||||
go build .
|
||||
```
|
||||
|
||||
You can also use `go run` to iterate without a separate rebuild step:
|
||||
|
||||
```shell
|
||||
go run . --repo=<repo>
|
||||
```
|
||||
|
||||
You can run tests with:
|
||||
|
||||
```shell
|
||||
go test .
|
||||
```
|
||||
|
||||
## Adding New Checks
|
||||
|
||||
Each check is currently just a function of type `CheckFn`.
|
||||
The signature is:
|
||||
|
||||
```golang
|
||||
type CheckFn func(*checker.Checker) CheckResult
|
||||
```
|
||||
|
||||
Checks are registered in an init function:
|
||||
|
||||
```golang
|
||||
AllChecks = append(AllChecks, NamedCheck{
|
||||
Name: "Code-Review",
|
||||
Fn: DoesCodeReview,
|
||||
})
|
||||
```
|
||||
|
||||
Currently only one set of checks can be run.
|
||||
In the future, we'll allow declaring multiple suites and configuring which checks get run.
|
8
go.mod
Normal file
8
go.mod
Normal file
@ -0,0 +1,8 @@
|
||||
module github.com/dlorenc/scorecard
|
||||
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/google/go-github/v32 v32.1.0
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be
|
||||
)
|
16
go.sum
Normal file
16
go.sum
Normal file
@ -0,0 +1,16 @@
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-github/v32 v32.1.0 h1:GWkQOdXqviCPx7Q7Fj+KyPoGm4SwHRh8rheoPhd27II=
|
||||
github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
70
main.go
Normal file
70
main.go
Normal file
@ -0,0 +1,70 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/dlorenc/scorecard/checker"
|
||||
"github.com/dlorenc/scorecard/checks"
|
||||
"github.com/dlorenc/scorecard/roundtripper"
|
||||
"github.com/google/go-github/v32/github"
|
||||
)
|
||||
|
||||
var repo = flag.String("repo", "", "url to the repo")
|
||||
var checksToRun = flag.String("checks", "", "specific checks to run, instead of all")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
split := strings.SplitN(*repo, "/", 3)
|
||||
host, owner, repo := split[0], split[1], split[2]
|
||||
|
||||
switch host {
|
||||
case "github.com":
|
||||
default:
|
||||
log.Fatalf("unsupported host: %s", host)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
rt := roundtripper.NewTransport(ctx)
|
||||
|
||||
client := &http.Client{
|
||||
Transport: rt,
|
||||
}
|
||||
ghClient := github.NewClient(client)
|
||||
|
||||
c := &checker.Checker{
|
||||
Ctx: ctx,
|
||||
Client: ghClient,
|
||||
HttpClient: client,
|
||||
Owner: owner,
|
||||
Repo: repo,
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
for _, check := range checks.AllChecks {
|
||||
check := check
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var r checks.CheckResult
|
||||
for retriesRemaining := 3; retriesRemaining > 0; retriesRemaining-- {
|
||||
r = check.Fn(c)
|
||||
if r.ShouldRetry {
|
||||
log.Println(r.Error)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
fmt.Println(check.Name, r.Confidence, r.Pass)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
}
|
118
roundtripper/roundtripper.go
Normal file
118
roundtripper/roundtripper.go
Normal file
@ -0,0 +1,118 @@
|
||||
package roundtripper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const GITHUB_AUTH_TOKEN = "GITHUB_AUTH_TOKEN"
|
||||
|
||||
// RateLimitRoundTripper is a rate-limit aware http.Transport for Github.
|
||||
type RateLimitRoundTripper struct {
|
||||
InnerTransport http.RoundTripper
|
||||
}
|
||||
|
||||
// NewTransport returns a configured http.Transport for use with GitHub
|
||||
func NewTransport(ctx context.Context) http.RoundTripper {
|
||||
token := os.Getenv(GITHUB_AUTH_TOKEN)
|
||||
|
||||
// Start with oauth
|
||||
transport := http.DefaultTransport
|
||||
if token != "" {
|
||||
ts := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: token},
|
||||
)
|
||||
transport = oauth2.NewClient(ctx, ts).Transport
|
||||
}
|
||||
|
||||
// Wrap that with the rate limiter
|
||||
rateLimit := &RateLimitRoundTripper{
|
||||
InnerTransport: transport,
|
||||
}
|
||||
|
||||
// Wrap that with the response cacher
|
||||
cache := &CachingRoundTripper{
|
||||
innerTransport: rateLimit,
|
||||
respCache: map[url.URL]*http.Response{},
|
||||
bodyCache: map[url.URL][]byte{},
|
||||
}
|
||||
|
||||
return cache
|
||||
}
|
||||
|
||||
// Roundtrip handles caching and ratelimiting of responses from GitHub.
|
||||
func (gh *RateLimitRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
resp, err := gh.InnerTransport.RoundTrip(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rateLimit := resp.Header.Get("X-RateLimit-Remaining")
|
||||
remaining, err := strconv.Atoi(rateLimit)
|
||||
if err != nil {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
if remaining <= 0 {
|
||||
reset, err := strconv.Atoi(resp.Header.Get("X-RateLimit-Reset"))
|
||||
if err != nil {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
duration := time.Until(time.Unix(int64(reset), 0))
|
||||
log.Printf("Rate limit exceeded. Waiting %s to retry...", duration)
|
||||
|
||||
// Retry
|
||||
time.Sleep(duration)
|
||||
log.Print("Rate limit exceeded. Retrying...")
|
||||
return gh.RoundTrip(r)
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
type CachingRoundTripper struct {
|
||||
innerTransport http.RoundTripper
|
||||
respCache map[url.URL]*http.Response
|
||||
bodyCache map[url.URL][]byte
|
||||
}
|
||||
|
||||
func (rt *CachingRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
// Check the cache
|
||||
resp, ok := rt.respCache[*r.URL]
|
||||
if ok {
|
||||
log.Printf("Cache hit on %s", r.URL.String())
|
||||
resp.Body = ioutil.NopCloser(bytes.NewReader(rt.bodyCache[*r.URL]))
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Get the real value
|
||||
resp, err := rt.innerTransport.RoundTrip(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add to cache
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rt.respCache[*r.URL] = resp
|
||||
rt.bodyCache[*r.URL] = body
|
||||
|
||||
resp.Body = ioutil.NopCloser(bytes.NewReader(body))
|
||||
}
|
||||
return resp, err
|
||||
}
|
Loading…
Reference in New Issue
Block a user