Miscellaneous refactors to ease downstream consumption (#1645)

* checker: Add `NewLogger` constructor for `DetailLogger` impl
* checker: Add `NewRunner` constructor for `Runner`
* cmd: Update to use refactored packages
* cmd: Move command flags and validation into an `options` package
* cmd: Move client accessors to `githubrepo` package
* cmd: Move policy and enabled checks to `policy` package
* cmd: Move results formatting to `format` package
* checker: Prefer `Set` prefixes for setters
* checker: Use `DetailLogger` return value for `NewLogger()`
* checker: Add `GetClients` accessor
* Move `FormatResults` to `pkg/`
* checks: Add getter for all checks

Signed-off-by: Stephen Augustus <foo@auggie.dev>
This commit is contained in:
Stephen Augustus (he/him) 2022-02-26 21:09:21 -05:00 committed by GitHub
parent 76105194da
commit 7956ff4fe7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 537 additions and 331 deletions

View File

@ -36,6 +36,30 @@ type Runner struct {
CheckRequest CheckRequest
}
// NewRunner creates a new instance of `Runner`.
func NewRunner(checkName, repo string, checkReq *CheckRequest) *Runner {
return &Runner{
CheckName: checkName,
Repo: repo,
CheckRequest: *checkReq,
}
}
// SetCheckName sets the check name.
func (r *Runner) SetCheckName(check string) {
r.CheckName = check
}
// SetRepo sets the repository.
func (r *Runner) SetRepo(repo string) {
r.Repo = repo
}
// SetCheckRequest sets the check request.
func (r *Runner) SetCheckRequest(checkReq *CheckRequest) {
r.CheckRequest = *checkReq
}
// CheckFn defined for convenience.
type CheckFn func(*CheckRequest) CheckResult
@ -79,12 +103,11 @@ func (r *Runner) Run(ctx context.Context, c Check) CheckResult {
startTime := time.Now()
var res CheckResult
var l logger
l := NewLogger()
for retriesRemaining := checkRetries; retriesRemaining > 0; retriesRemaining-- {
checkRequest := r.CheckRequest
checkRequest.Ctx = ctx
l = logger{}
checkRequest.Dlogger = &l
checkRequest.Dlogger = l
res = c.Fn(&checkRequest)
if res.Error2 != nil && errors.Is(res.Error2, sce.ErrRepoUnreachable) {
checkRequest.Dlogger.Warn(&LogMessage{

66
checker/client.go Normal file
View File

@ -0,0 +1,66 @@
// Copyright 2022 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 checker
import (
"context"
"fmt"
"github.com/ossf/scorecard/v4/clients"
ghrepo "github.com/ossf/scorecard/v4/clients/githubrepo"
"github.com/ossf/scorecard/v4/clients/localdir"
"github.com/ossf/scorecard/v4/log"
)
// GetClients returns a list of clients for running scorecard checks.
// TODO(repo): Pass a `http.RoundTripper` here.
func GetClients(ctx context.Context, repoURI, localURI string, logger *log.Logger) (
clients.Repo, // repo
clients.RepoClient, // repoClient
clients.RepoClient, // ossFuzzClient
clients.CIIBestPracticesClient, // ciiClient
clients.VulnerabilitiesClient, // vulnClient
error) {
var githubRepo clients.Repo
if localURI != "" {
localRepo, errLocal := localdir.MakeLocalDirRepo(localURI)
return localRepo, /*repo*/
localdir.CreateLocalDirClient(ctx, logger), /*repoClient*/
nil, /*ossFuzzClient*/
nil, /*ciiClient*/
nil, /*vulnClient*/
fmt.Errorf("getting local directory client: %w", errLocal)
}
githubRepo, errGitHub := ghrepo.MakeGithubRepo(repoURI)
if errGitHub != nil {
return githubRepo,
nil,
nil,
nil,
nil,
fmt.Errorf("getting local directory client: %w", errGitHub)
}
ossFuzzRepoClient, errOssFuzz := ghrepo.CreateOssFuzzRepoClient(ctx, logger)
// TODO(repo): Should we be handling the OSS-Fuzz client error like this?
return githubRepo, /*repo*/
ghrepo.CreateGithubRepoClient(ctx, logger), /*repoClient*/
ossFuzzRepoClient, /*ossFuzzClient*/
clients.DefaultCIIBestPracticesClient(), /*ciiClient*/
clients.DefaultVulnerabilitiesClient(), /*vulnClient*/
fmt.Errorf("getting OSS-Fuzz repo client: %w", errOssFuzz)
}

View File

@ -14,10 +14,17 @@
package checker
// Logger is an implementation of the `DetailLogger` interface.
type logger struct {
logs []CheckDetail
}
// NewLogger creates a new instance of `DetailLogger`.
func NewLogger() DetailLogger {
return &logger{}
}
// Info emits info level logs.
func (l *logger) Info(msg *LogMessage) {
cd := CheckDetail{
Type: DetailInfo,
@ -26,6 +33,7 @@ func (l *logger) Info(msg *LogMessage) {
l.logs = append(l.logs, cd)
}
// Warn emits warn level logs.
func (l *logger) Warn(msg *LogMessage) {
cd := CheckDetail{
Type: DetailWarn,
@ -34,6 +42,7 @@ func (l *logger) Warn(msg *LogMessage) {
l.logs = append(l.logs, cd)
}
// Debug emits debug level logs.
func (l *logger) Debug(msg *LogMessage) {
cd := CheckDetail{
Type: DetailDebug,
@ -42,8 +51,14 @@ func (l *logger) Debug(msg *LogMessage) {
l.logs = append(l.logs, cd)
}
// Flush returns existing logs and resets the logger instance.
func (l *logger) Flush() []CheckDetail {
ret := l.logs
ret := l.Logs()
l.logs = nil
return ret
}
// Logs returns existing logs.
func (l *logger) Logs() []CheckDetail {
return l.logs
}

View File

@ -22,6 +22,14 @@ import (
// AllChecks is the list of all security checks that will be run.
var AllChecks = checker.CheckNameToFnMap{}
// GetAll returns the full list of checks, given any environment variable
// constraints.
// TODO(checks): Is this actually necessary given `AllChecks` exists?
func GetAll() checker.CheckNameToFnMap {
possibleChecks := AllChecks
return possibleChecks
}
func registerCheck(name string, fn checker.CheckFn, supportedRequestTypes []checker.RequestType) error {
if name == "" {
return errInternalNameCannotBeEmpty

View File

@ -228,7 +228,7 @@ func CreateGithubRepoClient(ctx context.Context, logger *log.Logger) clients.Rep
func CreateOssFuzzRepoClient(ctx context.Context, logger *log.Logger) (clients.RepoClient, error) {
ossFuzzRepo, err := MakeGithubRepo("google/oss-fuzz")
if err != nil {
return nil, fmt.Errorf("error during githubrepo.MakeGithubRepo: %w", err)
return nil, fmt.Errorf("error during MakeGithubRepo: %w", err)
}
ossFuzzRepoClient := CreateGithubRepoClient(ctx, logger)

View File

@ -1,31 +0,0 @@
// 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 cmd implements Scorecard commandline.
package cmd
var (
flagRepo string
flagLocal string
flagCommit string
flagChecksToRun []string
flagMetadata []string
flagLogLevel string
flagFormat string
flagNPM string
flagPyPI string
flagRubyGems string
flagShowDetails bool
flagPolicyFile string
)

View File

@ -28,23 +28,14 @@ import (
"github.com/ossf/scorecard/v4/checker"
"github.com/ossf/scorecard/v4/checks"
"github.com/ossf/scorecard/v4/clients"
"github.com/ossf/scorecard/v4/clients/githubrepo"
"github.com/ossf/scorecard/v4/clients/localdir"
docs "github.com/ossf/scorecard/v4/docs/checks"
sce "github.com/ossf/scorecard/v4/errors"
sclog "github.com/ossf/scorecard/v4/log"
"github.com/ossf/scorecard/v4/options"
"github.com/ossf/scorecard/v4/pkg"
spol "github.com/ossf/scorecard/v4/policy"
"github.com/ossf/scorecard/v4/policy"
)
const (
formatJSON = "json"
formatSarif = "sarif"
formatDefault = "default"
formatRaw = "raw"
cliEnableSarif = "ENABLE_SARIF"
scorecardLong = "A program that shows security scorecard for an open source software."
scorecardUse = `./scorecard [--repo=<repo_url>] [--local=folder] [--checks=check1,...]
[--show-details] or ./scorecard --{npm,pypi,rubygems}=<package_name>
@ -59,42 +50,45 @@ var rootCmd = &cobra.Command{
Run: scorecardCmd,
}
var opts = options.New()
//nolint:gochecknoinits
func init() {
rootCmd.Flags().StringVar(&flagRepo, "repo", "", "repository to check")
rootCmd.Flags().StringVar(&flagLocal, "local", "", "local folder to check")
rootCmd.Flags().StringVar(&flagCommit, "commit", clients.HeadSHA, "commit to analyze")
rootCmd.Flags().StringVar(&opts.Repo, "repo", "", "repository to check")
rootCmd.Flags().StringVar(&opts.Local, "local", "", "local folder to check")
rootCmd.Flags().StringVar(&opts.Commit, "commit", options.DefaultCommit, "commit to analyze")
rootCmd.Flags().StringVar(
&flagLogLevel,
&opts.LogLevel,
"verbosity",
sclog.DefaultLevel.String(),
options.DefaultLogLevel,
"set the log level",
)
rootCmd.Flags().StringVar(
&flagNPM, "npm", "",
&opts.NPM, "npm", "",
"npm package to check, given that the npm package has a GitHub repository")
rootCmd.Flags().StringVar(
&flagPyPI, "pypi", "",
&opts.PyPI, "pypi", "",
"pypi package to check, given that the pypi package has a GitHub repository")
rootCmd.Flags().StringVar(
&flagRubyGems, "rubygems", "",
&opts.RubyGems, "rubygems", "",
"rubygems package to check, given that the rubygems package has a GitHub repository")
rootCmd.Flags().StringSliceVar(
&flagMetadata, "metadata", []string{}, "metadata for the project. It can be multiple separated by commas")
rootCmd.Flags().BoolVar(&flagShowDetails, "show-details", false, "show extra details about each check")
&opts.Metadata, "metadata", []string{}, "metadata for the project. It can be multiple separated by commas")
rootCmd.Flags().BoolVar(&opts.ShowDetails, "show-details", false, "show extra details about each check")
checkNames := []string{}
for checkName := range getAllChecks() {
for checkName := range checks.GetAll() {
checkNames = append(checkNames, checkName)
}
rootCmd.Flags().StringSliceVar(&flagChecksToRun, "checks", []string{},
rootCmd.Flags().StringSliceVar(&opts.ChecksToRun, "checks", []string{},
fmt.Sprintf("Checks to run. Possible values are: %s", strings.Join(checkNames, ",")))
if isSarifEnabled() {
rootCmd.Flags().StringVar(&flagPolicyFile, "policy", "", "policy to enforce")
rootCmd.Flags().StringVar(&flagFormat, "format", formatDefault,
// TODO(cmd): Extract logic
if options.IsSarifEnabled() {
rootCmd.Flags().StringVar(&opts.PolicyFile, "policy", "", "policy to enforce")
rootCmd.Flags().StringVar(&opts.Format, "format", options.FormatDefault,
"output format allowed values are [default, sarif, json]")
} else {
rootCmd.Flags().StringVar(&flagFormat, "format", formatDefault,
rootCmd.Flags().StringVar(&opts.Format, "format", options.FormatDefault,
"output format allowed values are [default, json]")
}
}
@ -108,28 +102,39 @@ func Execute() {
}
func scorecardCmd(cmd *cobra.Command, args []string) {
validateCmdFlags()
RunScorecard(args)
}
// RunScorecard runs scorecard checks given a set of arguments.
// TODO(cmd): Is `args` required?
func RunScorecard(args []string) {
// TODO(cmd): Catch validation errors
valErrs := opts.Validate()
if len(valErrs) != 0 {
log.Panicf(
"the following validation errors occurred: %+v",
valErrs,
)
}
// Set `repo` from package managers.
pkgResp, err := fetchGitRepositoryFromPackageManagers(flagNPM, flagPyPI, flagRubyGems)
pkgResp, err := fetchGitRepositoryFromPackageManagers(opts.NPM, opts.PyPI, opts.RubyGems)
if err != nil {
log.Panic(err)
}
if pkgResp.exists {
if err := cmd.Flags().Set("repo", pkgResp.associatedRepo); err != nil {
log.Panic(err)
}
opts.Repo = pkgResp.associatedRepo
}
policy, err := readPolicy()
pol, err := policy.ParseFromFile(opts.PolicyFile)
if err != nil {
log.Panicf("readPolicy: %v", err)
}
ctx := context.Background()
logger := sclog.NewLogger(sclog.ParseLevel(flagLogLevel))
repoURI, repoClient, ossFuzzRepoClient, ciiClient, vulnsClient, err := getRepoAccessors(
ctx, flagRepo, flagLocal, logger)
logger := sclog.NewLogger(sclog.ParseLevel(opts.LogLevel))
repoURI, repoClient, ossFuzzRepoClient, ciiClient, vulnsClient, err := checker.GetClients(
ctx, opts.Repo, opts.Local, logger)
if err != nil {
log.Panic(err)
}
@ -145,279 +150,58 @@ func scorecardCmd(cmd *cobra.Command, args []string) {
}
var requiredRequestTypes []checker.RequestType
if flagLocal != "" {
if opts.Local != "" {
requiredRequestTypes = append(requiredRequestTypes, checker.FileBased)
}
if !strings.EqualFold(flagCommit, clients.HeadSHA) {
if !strings.EqualFold(opts.Commit, clients.HeadSHA) {
requiredRequestTypes = append(requiredRequestTypes, checker.CommitBased)
}
enabledChecks, err := getEnabledChecks(policy, flagChecksToRun, requiredRequestTypes)
enabledChecks, err := policy.GetEnabled(pol, opts.ChecksToRun, requiredRequestTypes)
if err != nil {
log.Panic(err)
}
if flagFormat == formatDefault {
if opts.Format == options.FormatDefault {
for checkName := range enabledChecks {
fmt.Fprintf(os.Stderr, "Starting [%s]\n", checkName)
}
}
repoResult, err := pkg.RunScorecards(ctx, repoURI, flagCommit, flagFormat == formatRaw, enabledChecks, repoClient,
ossFuzzRepoClient, ciiClient, vulnsClient)
repoResult, err := pkg.RunScorecards(
ctx,
repoURI,
opts.Commit,
opts.Format == options.FormatRaw,
enabledChecks,
repoClient,
ossFuzzRepoClient,
ciiClient,
vulnsClient,
)
if err != nil {
log.Panic(err)
}
repoResult.Metadata = append(repoResult.Metadata, flagMetadata...)
repoResult.Metadata = append(repoResult.Metadata, opts.Metadata...)
// Sort them by name
sort.Slice(repoResult.Checks, func(i, j int) bool {
return repoResult.Checks[i].Name < repoResult.Checks[j].Name
})
if flagFormat == formatDefault {
if opts.Format == options.FormatDefault {
for checkName := range enabledChecks {
fmt.Fprintf(os.Stderr, "Finished [%s]\n", checkName)
}
fmt.Println("\nRESULTS\n-------")
}
switch flagFormat {
case formatDefault:
err = repoResult.AsString(flagShowDetails, sclog.ParseLevel(flagLogLevel), checkDocs, os.Stdout)
case formatSarif:
// TODO: support config files and update checker.MaxResultScore.
err = repoResult.AsSARIF(flagShowDetails, sclog.ParseLevel(flagLogLevel), os.Stdout, checkDocs, policy)
case formatJSON:
err = repoResult.AsJSON2(flagShowDetails, sclog.ParseLevel(flagLogLevel), checkDocs, os.Stdout)
case formatRaw:
err = repoResult.AsRawJSON(os.Stdout)
default:
err = sce.WithMessage(sce.ErrScorecardInternal,
fmt.Sprintf("invalid format flag: %v. Expected [default, json]", flagFormat))
}
if err != nil {
resultsErr := pkg.FormatResults(
opts,
&repoResult,
checkDocs,
pol,
)
if resultsErr != nil {
log.Panicf("Failed to output results: %v", err)
}
}
func validateCmdFlags() {
// Validate exactly one of `--repo`, `--npm`, `--pypi`, `--rubygems`, `--local` is enabled.
if boolSum(flagRepo != "",
flagNPM != "",
flagPyPI != "",
flagRubyGems != "",
flagLocal != "") != 1 {
log.Panic("Exactly one of `--repo`, `--npm`, `--pypi`, `--rubygems` or `--local` must be set")
}
// Validate SARIF features are flag-guarded.
if !isSarifEnabled() {
if flagFormat == formatSarif {
log.Panic("sarif format not supported yet")
}
if flagPolicyFile != "" {
log.Panic("policy file not supported yet")
}
}
// Validate V6 features are flag-guarded.
if !isV6Enabled() {
if flagFormat == formatRaw {
log.Panic("raw option not supported yet")
}
if flagCommit != clients.HeadSHA {
log.Panic("--commit option not supported yet")
}
}
// Validate format.
if !validateFormat(flagFormat) {
log.Panicf("unsupported format '%s'", flagFormat)
}
// Validate `commit` is non-empty.
if flagCommit == "" {
log.Panic("commit should be non-empty")
}
}
func boolSum(bools ...bool) int {
sum := 0
for _, b := range bools {
if b {
sum++
}
}
return sum
}
func isSarifEnabled() bool {
// UPGRADEv4: remove.
var sarifEnabled bool
_, sarifEnabled = os.LookupEnv(cliEnableSarif)
return sarifEnabled
}
func isV6Enabled() bool {
var v6 bool
_, v6 = os.LookupEnv("SCORECARD_V6")
return v6
}
func validateFormat(format string) bool {
switch format {
case formatJSON, formatSarif, formatDefault, formatRaw:
return true
default:
return false
}
}
func readPolicy() (*spol.ScorecardPolicy, error) {
if flagPolicyFile != "" {
data, err := os.ReadFile(flagPolicyFile)
if err != nil {
return nil, sce.WithMessage(sce.ErrScorecardInternal,
fmt.Sprintf("os.ReadFile: %v", err))
}
sp, err := spol.ParseFromYAML(data)
if err != nil {
return nil,
sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("spol.ParseFromYAML: %v", err))
}
return sp, nil
}
return nil, nil
}
func checksHavePolicies(sp *spol.ScorecardPolicy, enabledChecks checker.CheckNameToFnMap) bool {
for checkName := range enabledChecks {
_, exists := sp.Policies[checkName]
if !exists {
log.Printf("check %s has no policy declared", checkName)
return false
}
}
return true
}
func isSupportedCheck(checkName string, requiredRequestTypes []checker.RequestType) bool {
unsupported := checker.ListUnsupported(
requiredRequestTypes,
checks.AllChecks[checkName].SupportedRequestTypes)
return len(unsupported) == 0
}
func getAllChecks() checker.CheckNameToFnMap {
// Returns the full list of checks, given any environment variable constraints.
possibleChecks := checks.AllChecks
return possibleChecks
}
func getEnabledChecks(sp *spol.ScorecardPolicy, argsChecks []string,
requiredRequestTypes []checker.RequestType) (checker.CheckNameToFnMap, error) {
enabledChecks := checker.CheckNameToFnMap{}
switch {
case len(argsChecks) != 0:
// Populate checks to run with the `--repo` CLI argument.
for _, checkName := range argsChecks {
if !isSupportedCheck(checkName, requiredRequestTypes) {
return enabledChecks,
sce.WithMessage(sce.ErrScorecardInternal,
fmt.Sprintf("Unsupported RequestType %s by check: %s",
fmt.Sprint(requiredRequestTypes), checkName))
}
if !enableCheck(checkName, &enabledChecks) {
return enabledChecks,
sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("invalid check: %s", checkName))
}
}
case sp != nil:
// Populate checks to run with policy file.
for checkName := range sp.GetPolicies() {
if !isSupportedCheck(checkName, requiredRequestTypes) {
// We silently ignore the check, like we do
// for the default case when no argsChecks
// or policy are present.
continue
}
if !enableCheck(checkName, &enabledChecks) {
return enabledChecks,
sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("invalid check: %s", checkName))
}
}
default:
// Enable all checks that are supported.
for checkName := range getAllChecks() {
if !isSupportedCheck(checkName, requiredRequestTypes) {
continue
}
if !enableCheck(checkName, &enabledChecks) {
return enabledChecks,
sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("invalid check: %s", checkName))
}
}
}
// If a policy was passed as argument, ensure all checks
// to run have a corresponding policy.
if sp != nil && !checksHavePolicies(sp, enabledChecks) {
return enabledChecks, sce.WithMessage(sce.ErrScorecardInternal, "checks don't have policies")
}
return enabledChecks, nil
}
func getRepoAccessors(ctx context.Context, repoURI, localURI string, logger *sclog.Logger) (
clients.Repo, // repo
clients.RepoClient, // repoClient
clients.RepoClient, // ossFuzzClient
clients.CIIBestPracticesClient, // ciiClient
clients.VulnerabilitiesClient, // vulnClient
error) {
var githubRepo clients.Repo
var errGitHub error
if localURI != "" {
localRepo, errLocal := localdir.MakeLocalDirRepo(localURI)
return localRepo, /*repo*/
localdir.CreateLocalDirClient(ctx, logger), /*repoClient*/
nil, /*ossFuzzClient*/
nil, /*ciiClient*/
nil, /*vulnClient*/
errLocal
}
githubRepo, errGitHub = githubrepo.MakeGithubRepo(repoURI)
if errGitHub != nil {
// nolint: wrapcheck
return githubRepo,
nil,
nil,
nil,
nil,
errGitHub
}
ossFuzzRepoClient, errOssFuzz := githubrepo.CreateOssFuzzRepoClient(ctx, logger)
return githubRepo, /*repo*/
githubrepo.CreateGithubRepoClient(ctx, logger), /*repoClient*/
ossFuzzRepoClient, /*ossFuzzClient*/
clients.DefaultCIIBestPracticesClient(), /*ciiClient*/
clients.DefaultVulnerabilitiesClient(), /*vulnClient*/
errOssFuzz
}
// Enables checks by name.
func enableCheck(checkName string, enabledChecks *checker.CheckNameToFnMap) bool {
if enabledChecks != nil {
for key, checkFn := range getAllChecks() {
if strings.EqualFold(key, checkName) {
(*enabledChecks)[key] = checkFn
return true
}
}
}
return false
}

View File

@ -40,7 +40,7 @@ var serveCmd = &cobra.Command{
Short: "Serve the scorecard program over http",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
logger := log.NewLogger(log.ParseLevel(flagLogLevel))
logger := log.NewLogger(log.ParseLevel(opts.LogLevel))
t, err := template.New("webpage").Parse(tpl)
if err != nil {
@ -79,7 +79,7 @@ var serveCmd = &cobra.Command{
}
if r.Header.Get("Content-Type") == "application/json" {
if err := repoResult.AsJSON(flagShowDetails, log.ParseLevel(flagLogLevel), rw); err != nil {
if err := repoResult.AsJSON(opts.ShowDetails, log.ParseLevel(opts.LogLevel), rw); err != nil {
// TODO(log): Improve error message
logger.Error(err, "")
rw.WriteHeader(http.StatusInternalServerError)

187
options/options.go Normal file
View File

@ -0,0 +1,187 @@
// 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 options implements Scorecard options.
package options
import (
"errors"
"os"
"github.com/ossf/scorecard/v4/clients"
"github.com/ossf/scorecard/v4/log"
)
// Options define common options for configuring scorecard.
type Options struct {
Repo string
Local string
Commit string
LogLevel string
Format string
NPM string
PyPI string
RubyGems string
PolicyFile string
ChecksToRun []string
Metadata []string
ShowDetails bool
}
// New creates a new instance of `Options`.
func New() *Options {
return &Options{}
}
const (
// DefaultCommit specifies the default commit reference to use.
DefaultCommit = clients.HeadSHA
// Formats.
// FormatJSON specifies that results should be output in JSON format.
FormatJSON = "json"
// FormatSarif specifies that results should be output in SARIF format.
FormatSarif = "sarif"
// FormatDefault specifies that results should be output in default format.
FormatDefault = "default"
// FormatRaw specifies that results should be output in raw format.
FormatRaw = "raw"
// Environment variables.
// EnvVarEnableSarif is the environment variable which controls enabling
// SARIF logging.
EnvVarEnableSarif = "ENABLE_SARIF"
// EnvVarScorecardV6 is the environment variable which enables scorecard v6
// options.
EnvVarScorecardV6 = "SCORECARD_V6"
)
var (
// DefaultLogLevel retrieves the default log level.
DefaultLogLevel = log.DefaultLevel.String()
errCommitIsEmpty = errors.New("commit should be non-empty")
errCommitOptionNotSupported = errors.New("commit option is not supported yet")
errFormatNotSupported = errors.New("unsupported format")
errPolicyFileNotSupported = errors.New("policy file is not supported yet")
errRawOptionNotSupported = errors.New("raw option is not supported yet")
errRepoOptionMustBeSet = errors.New(
"exactly one of `repo`, `npm`, `pypi`, `rubygems` or `local` must be set",
)
errSARIFNotSupported = errors.New("SARIF format is not supported yet")
)
// Validate validates scorecard configuration options.
// TODO(options): Cleanup error messages.
func (o *Options) Validate() []error {
var errs []error
// Validate exactly one of `--repo`, `--npm`, `--pypi`, `--rubygems`, `--local` is enabled.
if boolSum(o.Repo != "",
o.NPM != "",
o.PyPI != "",
o.RubyGems != "",
o.Local != "") != 1 {
errs = append(
errs,
errRepoOptionMustBeSet,
)
}
// Validate SARIF features are flag-guarded.
if !IsSarifEnabled() {
if o.Format == FormatSarif {
errs = append(
errs,
errSARIFNotSupported,
)
}
if o.PolicyFile != "" {
errs = append(
errs,
errPolicyFileNotSupported,
)
}
}
// Validate V6 features are flag-guarded.
if !isV6Enabled() {
if o.Format == FormatRaw {
errs = append(
errs,
errRawOptionNotSupported,
)
}
if o.Commit != clients.HeadSHA {
errs = append(
errs,
errCommitOptionNotSupported,
)
}
}
// Validate format.
if !validateFormat(o.Format) {
errs = append(
errs,
errFormatNotSupported,
)
}
// Validate `commit` is non-empty.
if o.Commit == "" {
errs = append(
errs,
errCommitIsEmpty,
)
}
return errs
}
func boolSum(bools ...bool) int {
sum := 0
for _, b := range bools {
if b {
sum++
}
}
return sum
}
// IsSarifEnabled returns true if `EnvVarEnableSarif` is specified.
// TODO(options): This probably doesn't need to be exported.
func IsSarifEnabled() bool {
// UPGRADEv4: remove.
var sarifEnabled bool
_, sarifEnabled = os.LookupEnv(EnvVarEnableSarif)
return sarifEnabled
}
func isV6Enabled() bool {
var v6 bool
_, v6 = os.LookupEnv(EnvVarScorecardV6)
return v6
}
func validateFormat(format string) bool {
switch format {
case FormatJSON, FormatSarif, FormatDefault, FormatRaw:
return true
default:
return false
}
}

View File

@ -48,11 +48,12 @@ func runEnabledChecks(ctx context.Context,
wg.Add(1)
go func() {
defer wg.Done()
runner := checker.Runner{
Repo: repo.URI(),
CheckName: checkName,
CheckRequest: request,
}
runner := checker.NewRunner(
checkName,
repo.URI(),
&request,
)
resultsCh <- runner.Run(ctx, checkFn)
}()
}

View File

@ -23,9 +23,11 @@ import (
"github.com/olekukonko/tablewriter"
"github.com/ossf/scorecard/v4/checker"
docs "github.com/ossf/scorecard/v4/docs/checks"
"github.com/ossf/scorecard/v4/docs/checks"
sce "github.com/ossf/scorecard/v4/errors"
"github.com/ossf/scorecard/v4/log"
"github.com/ossf/scorecard/v4/options"
spol "github.com/ossf/scorecard/v4/policy"
)
// ScorecardInfo contains information about the scorecard code that was run.
@ -58,7 +60,7 @@ func scoreToString(s float64) string {
}
// GetAggregateScore returns the aggregate score.
func (r *ScorecardResult) GetAggregateScore(checkDocs docs.Doc) (float64, error) {
func (r *ScorecardResult) GetAggregateScore(checkDocs checks.Doc) (float64, error) {
// TODO: calculate the score and make it a field
// of ScorecardResult
weights := map[string]float64{"Critical": 10, "High": 7.5, "Medium": 5, "Low": 2.5}
@ -97,9 +99,45 @@ func (r *ScorecardResult) GetAggregateScore(checkDocs docs.Doc) (float64, error)
return score / total, nil
}
// FormatResults formats scorecard results.
func FormatResults(
opts *options.Options,
results *ScorecardResult,
doc checks.Doc,
policy *spol.ScorecardPolicy,
) error {
var err error
switch opts.Format {
case options.FormatDefault:
err = results.AsString(opts.ShowDetails, log.ParseLevel(opts.LogLevel), doc, os.Stdout)
case options.FormatSarif:
// TODO: support config files and update checker.MaxResultScore.
err = results.AsSARIF(opts.ShowDetails, log.ParseLevel(opts.LogLevel), os.Stdout, doc, policy)
case options.FormatJSON:
err = results.AsJSON2(opts.ShowDetails, log.ParseLevel(opts.LogLevel), doc, os.Stdout)
case options.FormatRaw:
err = results.AsRawJSON(os.Stdout)
default:
err = sce.WithMessage(
sce.ErrScorecardInternal,
fmt.Sprintf(
"invalid format flag: %v. Expected [default, json]",
opts.Format,
),
)
}
if err != nil {
return fmt.Errorf("failed to output results: %w", err)
}
return nil
}
// AsString returns ScorecardResult in string format.
func (r *ScorecardResult) AsString(showDetails bool, logLevel log.Level,
checkDocs docs.Doc, writer io.Writer) error {
checkDocs checks.Doc, writer io.Writer) error {
data := make([][]string, len(r.Checks))
//nolint
for i, row := range r.Checks {

View File

@ -17,9 +17,13 @@ package policy
import (
"errors"
"fmt"
"log"
"os"
"strings"
"gopkg.in/yaml.v3"
"github.com/ossf/scorecard/v4/checker"
"github.com/ossf/scorecard/v4/checks"
sce "github.com/ossf/scorecard/v4/errors"
)
@ -62,9 +66,29 @@ func modeToProto(m string) CheckPolicy_Mode {
}
}
// ParseFromYAML parses a policy file and returns
// a scorecardPolicy.
func ParseFromYAML(b []byte) (*ScorecardPolicy, error) {
// ParseFromFile takes a policy file and returns a `ScorecardPolicy`.
func ParseFromFile(policyFile string) (*ScorecardPolicy, error) {
if policyFile != "" {
data, err := os.ReadFile(policyFile)
if err != nil {
return nil, sce.WithMessage(sce.ErrScorecardInternal,
fmt.Sprintf("os.ReadFile: %v", err))
}
sp, err := parseFromYAML(data)
if err != nil {
return nil,
sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("spol.ParseFromYAML: %v", err))
}
return sp, nil
}
return nil, nil
}
// parseFromYAML parses a policy file and returns a `ScorecardPolicy`.
func parseFromYAML(b []byte) (*ScorecardPolicy, error) {
// Internal golang for unmarshalling the policy file.
sp := scorecardPolicy{}
// Protobuf-defined policy (policy.proto and policy.pb.go).
@ -112,3 +136,94 @@ func ParseFromYAML(b []byte) (*ScorecardPolicy, error) {
return &retPolicy, nil
}
// GetEnabled returns the list of enabled checks.
func GetEnabled(
sp *ScorecardPolicy,
argsChecks []string,
requiredRequestTypes []checker.RequestType,
) (checker.CheckNameToFnMap, error) {
enabledChecks := checker.CheckNameToFnMap{}
switch {
case len(argsChecks) != 0:
// Populate checks to run with the `--repo` CLI argument.
for _, checkName := range argsChecks {
if !isSupportedCheck(checkName, requiredRequestTypes) {
return enabledChecks,
sce.WithMessage(sce.ErrScorecardInternal,
fmt.Sprintf("Unsupported RequestType %s by check: %s",
fmt.Sprint(requiredRequestTypes), checkName))
}
if !enableCheck(checkName, &enabledChecks) {
return enabledChecks,
sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("invalid check: %s", checkName))
}
}
case sp != nil:
// Populate checks to run with policy file.
for checkName := range sp.GetPolicies() {
if !isSupportedCheck(checkName, requiredRequestTypes) {
// We silently ignore the check, like we do
// for the default case when no argsChecks
// or policy are present.
continue
}
if !enableCheck(checkName, &enabledChecks) {
return enabledChecks,
sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("invalid check: %s", checkName))
}
}
default:
// Enable all checks that are supported.
for checkName := range checks.GetAll() {
if !isSupportedCheck(checkName, requiredRequestTypes) {
continue
}
if !enableCheck(checkName, &enabledChecks) {
return enabledChecks,
sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("invalid check: %s", checkName))
}
}
}
// If a policy was passed as argument, ensure all checks
// to run have a corresponding policy.
if sp != nil && !checksHavePolicies(sp, enabledChecks) {
return enabledChecks, sce.WithMessage(sce.ErrScorecardInternal, "checks don't have policies")
}
return enabledChecks, nil
}
func checksHavePolicies(sp *ScorecardPolicy, enabledChecks checker.CheckNameToFnMap) bool {
for checkName := range enabledChecks {
_, exists := sp.Policies[checkName]
if !exists {
log.Printf("check %s has no policy declared", checkName)
return false
}
}
return true
}
func isSupportedCheck(checkName string, requiredRequestTypes []checker.RequestType) bool {
unsupported := checker.ListUnsupported(
requiredRequestTypes,
checks.AllChecks[checkName].SupportedRequestTypes)
return len(unsupported) == 0
}
// Enables checks by name.
func enableCheck(checkName string, enabledChecks *checker.CheckNameToFnMap) bool {
if enabledChecks != nil {
for key, checkFn := range checks.GetAll() {
if strings.EqualFold(key, checkName) {
(*enabledChecks)[key] = checkFn
return true
}
}
}
return false
}

View File

@ -114,7 +114,7 @@ func TestPolicyRead(t *testing.T) {
t.Fatalf("cannot read file: %v", err)
}
p, err := ParseFromYAML(content)
p, err := parseFromYAML(content)
if !errors.Is(err, tt.err) {
t.Fatalf("%s: expected %v, got %v", tt.name, tt.err, err)