cmd: Allow new scorecard to be instantiated with options (#1703)

* cmd: Allow new scorecard commands to be instantiated with options
* options: Default flags to struct field values
* options: Use constants for flag names
* options: Simplify SARIF check

Signed-off-by: Stephen Augustus <foo@auggie.dev>
This commit is contained in:
Stephen Augustus (he/him) 2022-03-02 20:38:34 -05:00 committed by GitHub
parent d192c8e3ac
commit 3070b3ca1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 115 additions and 61 deletions

View File

@ -35,8 +35,6 @@ import (
"github.com/ossf/scorecard/v4/policy" "github.com/ossf/scorecard/v4/policy"
) )
var opts = options.New()
const ( const (
scorecardLong = "A program that shows security scorecard for an open source software." scorecardLong = "A program that shows security scorecard for an open source software."
scorecardUse = `./scorecard [--repo=<repo_url>] [--local=folder] [--checks=check1,...] scorecardUse = `./scorecard [--repo=<repo_url>] [--local=folder] [--checks=check1,...]
@ -46,13 +44,13 @@ const (
) )
// New creates a new instance of the scorecard command. // New creates a new instance of the scorecard command.
func New() *cobra.Command { func New(o *options.Options) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: scorecardUse, Use: scorecardUse,
Short: scorecardShort, Short: scorecardShort,
Long: scorecardLong, Long: scorecardLong,
PreRunE: func(cmd *cobra.Command, args []string) error { PreRunE: func(cmd *cobra.Command, args []string) error {
err := opts.Validate() err := o.Validate()
if err != nil { if err != nil {
return fmt.Errorf("validating options: %w", err) return fmt.Errorf("validating options: %w", err)
} }
@ -61,38 +59,38 @@ func New() *cobra.Command {
}, },
// TODO(cmd): Consider using RunE here // TODO(cmd): Consider using RunE here
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
rootCmd(opts) rootCmd(o)
}, },
} }
opts.AddFlags(cmd) o.AddFlags(cmd)
// Add sub-commands. // Add sub-commands.
cmd.AddCommand(serveCmd()) cmd.AddCommand(serveCmd(o))
cmd.AddCommand(version.Version()) cmd.AddCommand(version.Version())
return cmd return cmd
} }
// rootCmd runs scorecard checks given a set of arguments. // rootCmd runs scorecard checks given a set of arguments.
func rootCmd(opts *options.Options) { func rootCmd(o *options.Options) {
// Set `repo` from package managers. // Set `repo` from package managers.
pkgResp, err := fetchGitRepositoryFromPackageManagers(opts.NPM, opts.PyPI, opts.RubyGems) pkgResp, err := fetchGitRepositoryFromPackageManagers(o.NPM, o.PyPI, o.RubyGems)
if err != nil { if err != nil {
log.Panic(err) log.Panic(err)
} }
if pkgResp.exists { if pkgResp.exists {
opts.Repo = pkgResp.associatedRepo o.Repo = pkgResp.associatedRepo
} }
pol, err := policy.ParseFromFile(opts.PolicyFile) pol, err := policy.ParseFromFile(o.PolicyFile)
if err != nil { if err != nil {
log.Panicf("readPolicy: %v", err) log.Panicf("readPolicy: %v", err)
} }
ctx := context.Background() ctx := context.Background()
logger := sclog.NewLogger(sclog.ParseLevel(opts.LogLevel)) logger := sclog.NewLogger(sclog.ParseLevel(o.LogLevel))
repoURI, repoClient, ossFuzzRepoClient, ciiClient, vulnsClient, err := checker.GetClients( repoURI, repoClient, ossFuzzRepoClient, ciiClient, vulnsClient, err := checker.GetClients(
ctx, opts.Repo, opts.Local, logger) ctx, o.Repo, o.Local, logger)
if err != nil { if err != nil {
log.Panic(err) log.Panic(err)
} }
@ -108,18 +106,18 @@ func rootCmd(opts *options.Options) {
} }
var requiredRequestTypes []checker.RequestType var requiredRequestTypes []checker.RequestType
if opts.Local != "" { if o.Local != "" {
requiredRequestTypes = append(requiredRequestTypes, checker.FileBased) requiredRequestTypes = append(requiredRequestTypes, checker.FileBased)
} }
if !strings.EqualFold(opts.Commit, clients.HeadSHA) { if !strings.EqualFold(o.Commit, clients.HeadSHA) {
requiredRequestTypes = append(requiredRequestTypes, checker.CommitBased) requiredRequestTypes = append(requiredRequestTypes, checker.CommitBased)
} }
enabledChecks, err := policy.GetEnabled(pol, opts.ChecksToRun, requiredRequestTypes) enabledChecks, err := policy.GetEnabled(pol, o.ChecksToRun, requiredRequestTypes)
if err != nil { if err != nil {
log.Panic(err) log.Panic(err)
} }
if opts.Format == options.FormatDefault { if o.Format == options.FormatDefault {
for checkName := range enabledChecks { for checkName := range enabledChecks {
fmt.Fprintf(os.Stderr, "Starting [%s]\n", checkName) fmt.Fprintf(os.Stderr, "Starting [%s]\n", checkName)
} }
@ -128,8 +126,8 @@ func rootCmd(opts *options.Options) {
repoResult, err := pkg.RunScorecards( repoResult, err := pkg.RunScorecards(
ctx, ctx,
repoURI, repoURI,
opts.Commit, o.Commit,
opts.Format == options.FormatRaw, o.Format == options.FormatRaw,
enabledChecks, enabledChecks,
repoClient, repoClient,
ossFuzzRepoClient, ossFuzzRepoClient,
@ -139,14 +137,14 @@ func rootCmd(opts *options.Options) {
if err != nil { if err != nil {
log.Panic(err) log.Panic(err)
} }
repoResult.Metadata = append(repoResult.Metadata, opts.Metadata...) repoResult.Metadata = append(repoResult.Metadata, o.Metadata...)
// Sort them by name // Sort them by name
sort.Slice(repoResult.Checks, func(i, j int) bool { sort.Slice(repoResult.Checks, func(i, j int) bool {
return repoResult.Checks[i].Name < repoResult.Checks[j].Name return repoResult.Checks[i].Name < repoResult.Checks[j].Name
}) })
if opts.Format == options.FormatDefault { if o.Format == options.FormatDefault {
for checkName := range enabledChecks { for checkName := range enabledChecks {
fmt.Fprintf(os.Stderr, "Finished [%s]\n", checkName) fmt.Fprintf(os.Stderr, "Finished [%s]\n", checkName)
} }
@ -154,7 +152,7 @@ func rootCmd(opts *options.Options) {
} }
resultsErr := pkg.FormatResults( resultsErr := pkg.FormatResults(
opts, o,
&repoResult, &repoResult,
checkDocs, checkDocs,
pol, pol,

View File

@ -27,17 +27,18 @@ import (
"github.com/ossf/scorecard/v4/clients" "github.com/ossf/scorecard/v4/clients"
"github.com/ossf/scorecard/v4/clients/githubrepo" "github.com/ossf/scorecard/v4/clients/githubrepo"
"github.com/ossf/scorecard/v4/log" "github.com/ossf/scorecard/v4/log"
"github.com/ossf/scorecard/v4/options"
"github.com/ossf/scorecard/v4/pkg" "github.com/ossf/scorecard/v4/pkg"
) )
// TODO(cmd): Determine if this should be exported. // TODO(cmd): Determine if this should be exported.
func serveCmd() *cobra.Command { func serveCmd(o *options.Options) *cobra.Command {
return &cobra.Command{ return &cobra.Command{
Use: "serve", Use: "serve",
Short: "Serve the scorecard program over http", Short: "Serve the scorecard program over http",
Long: ``, Long: ``,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
logger := log.NewLogger(log.ParseLevel(opts.LogLevel)) logger := log.NewLogger(log.ParseLevel(o.LogLevel))
t, err := template.New("webpage").Parse(tpl) t, err := template.New("webpage").Parse(tpl)
if err != nil { if err != nil {
@ -76,7 +77,7 @@ func serveCmd() *cobra.Command {
} }
if r.Header.Get("Content-Type") == "application/json" { if r.Header.Get("Content-Type") == "application/json" {
if err := repoResult.AsJSON(opts.ShowDetails, log.ParseLevel(opts.LogLevel), rw); err != nil { if err := repoResult.AsJSON(o.ShowDetails, log.ParseLevel(o.LogLevel), rw); err != nil {
// TODO(log): Improve error message // TODO(log): Improve error message
logger.Error(err, "") logger.Error(err, "")
rw.WriteHeader(http.StatusInternalServerError) rw.WriteHeader(http.StatusInternalServerError)

View File

@ -19,10 +19,12 @@ import (
"log" "log"
"github.com/ossf/scorecard/v4/cmd" "github.com/ossf/scorecard/v4/cmd"
"github.com/ossf/scorecard/v4/options"
) )
func main() { func main() {
if err := cmd.New().Execute(); err != nil { opts := options.New()
if err := cmd.New(opts).Execute(); err != nil {
log.Fatalf("error during command execution: %v", err) log.Fatalf("error during command execution: %v", err)
} }
} }

View File

@ -23,6 +23,44 @@ import (
"github.com/ossf/scorecard/v4/checks" "github.com/ossf/scorecard/v4/checks"
) )
const (
// FlagRepo is the flag name for specifying a repository.
FlagRepo = "repo"
// FlagLocal is the flag name for specifying a local run.
FlagLocal = "local"
// FlagCommit is the flag name for specifying a commit.
FlagCommit = "commit"
// FlagLogLevel is the flag name for specifying the log level.
FlagLogLevel = "verbosity"
// FlagNPM is the flag name for specifying a NPM repository.
FlagNPM = "npm"
// FlagPyPI is the flag name for specifying a PyPI repository.
FlagPyPI = "pypi"
// FlagRubyGems is the flag name for specifying a RubyGems repository.
FlagRubyGems = "rubygems"
// FlagMetadata is the flag name for specifying metadata for the project.
FlagMetadata = "metadata"
// FlagShowDetails is the flag name for outputting additional check info.
FlagShowDetails = "show-details"
// FlagChecks is the flag name for specifying which checks to run.
FlagChecks = "checks"
// FlagPolicyFile is the flag name for specifying a policy file.
FlagPolicyFile = "policy"
// FlagFormat is the flag name for specifying output format.
FlagFormat = "format"
)
// Command is an interface for handling options for command-line utilities. // Command is an interface for handling options for command-line utilities.
type Command interface { type Command interface {
// AddFlags adds this options' flags to the cobra command. // AddFlags adds this options' flags to the cobra command.
@ -33,65 +71,65 @@ type Command interface {
func (o *Options) AddFlags(cmd *cobra.Command) { func (o *Options) AddFlags(cmd *cobra.Command) {
cmd.Flags().StringVar( cmd.Flags().StringVar(
&o.Repo, &o.Repo,
"repo", FlagRepo,
"", o.Repo,
"repository to check", "repository to check",
) )
cmd.Flags().StringVar( cmd.Flags().StringVar(
&o.Local, &o.Local,
"local", FlagLocal,
"", o.Local,
"local folder to check", "local folder to check",
) )
// TODO(v5): Should this be behind a feature flag? // TODO(v5): Should this be behind a feature flag?
cmd.Flags().StringVar( cmd.Flags().StringVar(
&o.Commit, &o.Commit,
"commit", FlagCommit,
DefaultCommit, o.Commit,
"commit to analyze", "commit to analyze",
) )
cmd.Flags().StringVar( cmd.Flags().StringVar(
&o.LogLevel, &o.LogLevel,
"verbosity", FlagLogLevel,
DefaultLogLevel, o.LogLevel,
"set the log level", "set the log level",
) )
cmd.Flags().StringVar( cmd.Flags().StringVar(
&o.NPM, &o.NPM,
"npm", FlagNPM,
"", o.NPM,
"npm package to check, given that the npm package has a GitHub repository", "npm package to check, given that the npm package has a GitHub repository",
) )
cmd.Flags().StringVar( cmd.Flags().StringVar(
&o.PyPI, &o.PyPI,
"pypi", FlagPyPI,
"", o.PyPI,
"pypi package to check, given that the pypi package has a GitHub repository", "pypi package to check, given that the pypi package has a GitHub repository",
) )
cmd.Flags().StringVar( cmd.Flags().StringVar(
&o.RubyGems, &o.RubyGems,
"rubygems", FlagRubyGems,
"", o.RubyGems,
"rubygems package to check, given that the rubygems package has a GitHub repository", "rubygems package to check, given that the rubygems package has a GitHub repository",
) )
cmd.Flags().StringSliceVar( cmd.Flags().StringSliceVar(
&o.Metadata, &o.Metadata,
"metadata", FlagMetadata,
[]string{}, o.Metadata,
"metadata for the project. It can be multiple separated by commas", "metadata for the project. It can be multiple separated by commas",
) )
cmd.Flags().BoolVar( cmd.Flags().BoolVar(
&o.ShowDetails, &o.ShowDetails,
"show-details", FlagShowDetails,
false, o.ShowDetails,
"show extra details about each check", "show extra details about each check",
) )
@ -101,32 +139,35 @@ func (o *Options) AddFlags(cmd *cobra.Command) {
} }
cmd.Flags().StringSliceVar( cmd.Flags().StringSliceVar(
&o.ChecksToRun, &o.ChecksToRun,
"checks", FlagChecks,
[]string{}, o.ChecksToRun,
fmt.Sprintf("Checks to run. Possible values are: %s", strings.Join(checkNames, ",")), fmt.Sprintf("Checks to run. Possible values are: %s", strings.Join(checkNames, ",")),
) )
// TODO(options): Extract logic // TODO(options): Extract logic
allowedFormats := []string{
FormatDefault,
FormatJSON,
}
if o.isSarifEnabled() { if o.isSarifEnabled() {
cmd.Flags().StringVar( cmd.Flags().StringVar(
&o.PolicyFile, &o.PolicyFile,
"policy", FlagPolicyFile,
"", o.PolicyFile,
"policy to enforce", "policy to enforce",
) )
cmd.Flags().StringVar( allowedFormats = append(allowedFormats, FormatSarif)
&o.Format,
"format",
FormatDefault,
"output format allowed values are [default, sarif, json]",
)
} else {
cmd.Flags().StringVar(
&o.Format,
"format",
FormatDefault,
"output format allowed values are [default, json]",
)
} }
cmd.Flags().StringVar(
&o.Format,
FlagFormat,
o.Format,
fmt.Sprintf(
"output format. Possible values are: %s",
strings.Join(allowedFormats, ", "),
),
)
} }

View File

@ -58,6 +58,18 @@ func New() *Options {
fmt.Printf("could not parse env vars, using default options: %v", err) fmt.Printf("could not parse env vars, using default options: %v", err)
} }
// Defaulting.
// TODO(options): Consider moving this to a separate function/method.
if opts.Commit == "" {
opts.Commit = DefaultCommit
}
if opts.Format == "" {
opts.Format = FormatDefault
}
if opts.LogLevel == "" {
opts.LogLevel = DefaultLogLevel
}
return opts return opts
} }