diff --git a/cmd/root.go b/cmd/root.go index 6a9d8375951..1e960b5c2be 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -42,9 +42,12 @@ import ( "github.com/ossf/scorecard/v5/policy" ) -// errChecksFailed is returned when one or more checks produced a runtime -// error during execution. -var errChecksFailed = errors.New("one or more checks failed during execution") +var ( + // errChecksFailed is returned when one or more checks produced a runtime + // error during execution. + errChecksFailed = errors.New("one or more checks failed during execution") + errNoChecksSupported = errors.New("no checks support repository type") +) const ( scorecardLong = "A program that shows the OpenSSF scorecard for an open source software." @@ -235,6 +238,51 @@ func printCheckResults(repo string, enabledChecks checker.CheckNameToFnMap) { } } +func printSkippedChecks(repo string, skippedChecks []string) { + for _, checkName := range skippedChecks { + fmt.Fprintf(os.Stderr, "Skipping (%s) [%s]\n", repo, checkName) + } +} + +func filterUnsupportedChecks(enabledChecks checker.CheckNameToFnMap, repo clients.Repo) []string { + var skippedChecks []string + + repoType := scorecard.GetRepoType(repo) + if repoType == scorecard.RepoUnknown { + return skippedChecks + } + + checksDocs, err := docs.Read() + if err != nil { + return skippedChecks + } + + for checkName := range enabledChecks { + checkDoc, err := checksDocs.GetCheck(checkName) + if err != nil { + continue + } + + checkRepos := checkDoc.GetSupportedRepoTypes() + var supported bool + for _, cr := range checkRepos { + checkRepo := scorecard.RepoTypeFromString(cr) + + if checkRepo == repoType { + supported = true + break + } + } + + if !supported { + skippedChecks = append(skippedChecks, checkName) + delete(enabledChecks, checkName) + } + } + + return skippedChecks +} + // makeRepo helps turn a URI into the appropriate clients.Repo. // currently this is a decision between GitHub, GitLab, and Azure DevOps, // but may expand in the future. @@ -295,6 +343,23 @@ func processRepo( } } + if o.SkipUnsupportedChecks { + skippedChecks := filterUnsupportedChecks(enabledChecks, repo) + if len(enabledChecks) == 0 { + return nil, errNoChecksSupported + } + if o.Format == options.FormatDefault { + printSkippedChecks(uri, skippedChecks) + } + + filteredChecks := make([]string, 0, len(enabledChecks)) + for c := range enabledChecks { + filteredChecks = append(filteredChecks, c) + } + // overwrites enabled checks + opts = append(opts, scorecard.WithChecks(filteredChecks)) + } + // Start banners with repo uri (show banners in default format only) if o.Format == options.FormatDefault { if len(enabledProbes) > 0 { diff --git a/options/flags.go b/options/flags.go index 127ffdb6f72..49ec2256906 100644 --- a/options/flags.go +++ b/options/flags.go @@ -80,6 +80,10 @@ const ( FlagCommitDepth = "commit-depth" FlagProbes = "probes" + + // FlagSkipUnsupportedChecks is the flag name for skipping checks that are + // not supported by the target repo type. + FlagSkipUnsupportedChecks = "skip-unsupported-checks" ) // Command is an interface for handling options for command-line utilities. @@ -208,6 +212,13 @@ func (o *Options) AddFlags(cmd *cobra.Command) { "Probes to run.", ) + cmd.Flags().BoolVar( + &o.SkipUnsupportedChecks, + FlagSkipUnsupportedChecks, + o.SkipUnsupportedChecks, + "skip checks that don't support the repository type", + ) + // TODO(options): Extract logic allowedFormats := []string{ FormatDefault, diff --git a/options/flags_test.go b/options/flags_test.go index ba41f21d7e0..fb285f8fa06 100644 --- a/options/flags_test.go +++ b/options/flags_test.go @@ -31,19 +31,20 @@ func TestOptions_AddFlags(t *testing.T) { { name: "custom options", opts: &Options{ - Repo: "owner/repo", - Local: "/path/to/local", - Commit: "1234567890abcdef", - LogLevel: "debug", - NPM: "npm-package", - PyPI: "pypi-package", - RubyGems: "rubygems-package", - Metadata: []string{"key1=value1", "key2=value2"}, - ShowDetails: true, - ChecksToRun: []string{"check1", "check2"}, - PolicyFile: "policy-file", - Format: "json", - ResultsFile: "result.json", + Repo: "owner/repo", + Local: "/path/to/local", + Commit: "1234567890abcdef", + LogLevel: "debug", + NPM: "npm-package", + PyPI: "pypi-package", + RubyGems: "rubygems-package", + Metadata: []string{"key1=value1", "key2=value2"}, + ShowDetails: true, + ChecksToRun: []string{"check1", "check2"}, + PolicyFile: "policy-file", + Format: "json", + ResultsFile: "result.json", + SkipUnsupportedChecks: true, }, }, } @@ -114,6 +115,15 @@ func TestOptions_AddFlags(t *testing.T) { t.Errorf("expected ShorthandFlagResultsFile to be %q, but got %q", ShorthandFlagResultsFile, cmd.Flag(FlagResultsFile).Shorthand) } + + // check FlagSkipUnsupportedChecks + value, err := cmd.Flags().GetBool(FlagSkipUnsupportedChecks) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if tt.opts.SkipUnsupportedChecks != value { + t.Errorf("expected FlagSkipUnsupportedChecks to be %t, got %t", tt.opts.SkipUnsupportedChecks, value) + } }) } } diff --git a/options/options.go b/options/options.go index 6661b3efa8a..172f1e0b01a 100644 --- a/options/options.go +++ b/options/options.go @@ -30,26 +30,27 @@ import ( // Options define common options for configuring scorecard. type Options struct { - Repo string - Repos []string - Org string - Local string - Commit string - LogLevel string - Format string - NPM string - PyPI string - RubyGems string - Nuget string - PolicyFile string - ResultsFile string - FileMode string - ChecksToRun []string - ProbesToRun []string - Metadata []string - CommitDepth int - ShowDetails bool - ShowAnnotations bool + Repo string + Repos []string + Org string + Local string + Commit string + LogLevel string + Format string + NPM string + PyPI string + RubyGems string + Nuget string + PolicyFile string + ResultsFile string + FileMode string + ChecksToRun []string + ProbesToRun []string + Metadata []string + CommitDepth int + ShowDetails bool + ShowAnnotations bool + SkipUnsupportedChecks bool // Feature flags. EnableSarif bool `env:"ENABLE_SARIF"` EnableScorecardV6 bool `env:"SCORECARD_V6"` diff --git a/pkg/scorecard/repo.go b/pkg/scorecard/repo.go new file mode 100644 index 00000000000..ab3f03e8f96 --- /dev/null +++ b/pkg/scorecard/repo.go @@ -0,0 +1,47 @@ +package scorecard + +import ( + "strings" + + "github.com/ossf/scorecard/v5/clients" + "github.com/ossf/scorecard/v5/clients/azuredevopsrepo" + "github.com/ossf/scorecard/v5/clients/githubrepo" + "github.com/ossf/scorecard/v5/clients/gitlabrepo" + "github.com/ossf/scorecard/v5/clients/localdir" +) + +type RepoType string + +const ( + RepoUnknown RepoType = "unknown" + RepoLocal RepoType = "local" + RepoGitLocal RepoType = "git-local" // is not supported by any check yet. + RepoGitHub RepoType = "github" + RepoGitLab RepoType = "gitlab" + RepoAzureDevOPs RepoType = "azuredevops" +) + +func GetRepoType(repo clients.Repo) RepoType { + switch repo.(type) { + case *localdir.Repo: + return RepoLocal + case *githubrepo.Repo: + return RepoGitHub + case *gitlabrepo.Repo: + return RepoGitLab + case *azuredevopsrepo.Repo: + return RepoAzureDevOPs + default: + return RepoUnknown + } +} + +func RepoTypeFromString(repo string) RepoType { + rt := RepoType(strings.ToLower(strings.TrimSpace(repo))) + switch rt { + case RepoLocal, RepoGitLocal, RepoGitHub, RepoGitLab, RepoAzureDevOPs: + return rt + default: + return RepoUnknown + } +} diff --git a/pkg/scorecard/repo_test.go b/pkg/scorecard/repo_test.go new file mode 100644 index 00000000000..5a6a62725bb --- /dev/null +++ b/pkg/scorecard/repo_test.go @@ -0,0 +1,168 @@ +// Copyright 2026 OpenSSF 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 scorecard + +import ( + "testing" + + "github.com/ossf/scorecard/v5/clients" + "github.com/ossf/scorecard/v5/clients/azuredevopsrepo" + "github.com/ossf/scorecard/v5/clients/githubrepo" + "github.com/ossf/scorecard/v5/clients/gitlabrepo" + "github.com/ossf/scorecard/v5/clients/localdir" +) + +func TestGetRepoType(t *testing.T) { + t.Parallel() + tests := []struct { + name string + repo clients.Repo + want RepoType + }{ + { + name: "local directory repo", + repo: &localdir.Repo{}, + want: RepoLocal, + }, + { + name: "github repo", + repo: &githubrepo.Repo{}, + want: RepoGitHub, + }, + { + name: "gitlab repo", + repo: &gitlabrepo.Repo{}, + want: RepoGitLab, + }, + { + name: "azure devops repo", + repo: &azuredevopsrepo.Repo{}, + want: RepoAzureDevOPs, + }, + { + name: "unknown repo type", + repo: nil, + want: RepoUnknown, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := GetRepoType(tt.repo) + if got != tt.want { + t.Errorf("getRepoType() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRepoTypeFromString(t *testing.T) { + t.Parallel() + tests := []struct { + name string + repo string + want RepoType + }{ + { + name: "local lowercase", + repo: "local", + want: RepoLocal, + }, + { + name: "local uppercase", + repo: "LOCAL", + want: RepoLocal, + }, + { + name: "local mixed case", + repo: "LoCaL", + want: RepoLocal, + }, + { + name: "local with whitespace", + repo: " local ", + want: RepoLocal, + }, + { + name: "git-local", + repo: "git-local", + want: RepoGitLocal, + }, + { + name: "github lowercase", + repo: "github", + want: RepoGitHub, + }, + { + name: "github uppercase", + repo: "GITHUB", + want: RepoGitHub, + }, + { + name: "github mixedcase", + repo: "GitHub", + want: RepoGitHub, + }, + { + name: "gitlab lowercase", + repo: "gitlab", + want: RepoGitLab, + }, + { + name: "gitlab uppercase", + repo: "GITLAB", + want: RepoGitLab, + }, + { + name: "gitlab mixedcase", + repo: "GitLab", + want: RepoGitLab, + }, + { + name: "azuredevops lowercase", + repo: "azuredevops", + want: RepoAzureDevOPs, + }, + { + name: "azuredevops uppercase", + repo: "AZUREDEVOPS", + want: RepoAzureDevOPs, + }, + { + name: "unknown type", + repo: "unknown", + want: RepoUnknown, + }, + { + name: "empty string", + repo: "", + want: RepoUnknown, + }, + { + name: "invalid type", + repo: "bitbucket", + want: RepoUnknown, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := RepoTypeFromString(tt.repo) + if got != tt.want { + t.Errorf("repoTypeFromString() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/scorecard/scorecard.go b/pkg/scorecard/scorecard.go index 3c2528f884b..346fcd99831 100644 --- a/pkg/scorecard/scorecard.go +++ b/pkg/scorecard/scorecard.go @@ -387,13 +387,14 @@ func Run(ctx context.Context, repo clients.Repo, opts ...Option) (Result, error) var requiredRequestTypes []checker.RequestType var err error - switch repo.(type) { - case *localdir.Repo: + repoType := GetRepoType(repo) + switch repoType { + case RepoLocal: requiredRequestTypes = append(requiredRequestTypes, checker.FileBased) if c.client == nil { c.client = localdir.CreateLocalDirClient(ctx, logger) } - case *githubrepo.Repo: + case RepoGitHub: if c.client == nil { var opts []githubrepo.Option if c.gitMode { @@ -405,20 +406,21 @@ func Run(ctx context.Context, repo clients.Repo, opts ...Option) (Result, error) } c.client = client } - case *gitlabrepo.Repo: + case RepoGitLab: if c.client == nil { c.client, err = gitlabrepo.CreateGitlabClient(ctx, repo.Host()) if err != nil { return Result{}, fmt.Errorf("creating gitlab client: %w", err) } } - case *azuredevopsrepo.Repo: + case RepoAzureDevOPs: if c.client == nil { c.client, err = azuredevopsrepo.CreateAzureDevOpsClient(ctx, repo) if err != nil { return Result{}, fmt.Errorf("creating azure devops client: %w", err) } } + case RepoUnknown, RepoGitLocal: } if !strings.EqualFold(c.commit, clients.HeadSHA) {