diff --git a/clients/localdir/client.go b/clients/localdir/client.go index 787ba945bc5..3ecc1d34960 100644 --- a/clients/localdir/client.go +++ b/clients/localdir/client.go @@ -32,10 +32,7 @@ import ( clients "github.com/ossf/scorecard/v3/clients" ) -var ( - errUnsupportedFeature = errors.New("unsupported feature") - errInputRepoType = errors.New("input repo should be of type repoLocal") -) +var errInputRepoType = errors.New("input repo should be of type repoLocal") //nolint:govet type localDirClient struct { @@ -66,7 +63,7 @@ func (client *localDirClient) URI() string { // IsArchived implements RepoClient.IsArchived. func (client *localDirClient) IsArchived() (bool, error) { - return false, fmt.Errorf("IsArchived: %w", errUnsupportedFeature) + return false, fmt.Errorf("IsArchived: %w", clients.ErrUnsupportedFeature) } func isDir(p string) (bool, error) { @@ -142,51 +139,51 @@ func (client *localDirClient) GetFileContent(filename string) ([]byte, error) { // ListMergedPRs implements RepoClient.ListMergedPRs. func (client *localDirClient) ListMergedPRs() ([]clients.PullRequest, error) { - return nil, fmt.Errorf("ListMergedPRs: %w", errUnsupportedFeature) + return nil, fmt.Errorf("ListMergedPRs: %w", clients.ErrUnsupportedFeature) } // ListBranches implements RepoClient.ListBranches. func (client *localDirClient) ListBranches() ([]*clients.BranchRef, error) { - return nil, fmt.Errorf("ListBranches: %w", errUnsupportedFeature) + return nil, fmt.Errorf("ListBranches: %w", clients.ErrUnsupportedFeature) } // GetDefaultBranch implements RepoClient.GetDefaultBranch. func (client *localDirClient) GetDefaultBranch() (*clients.BranchRef, error) { - return nil, fmt.Errorf("GetDefaultBranch: %w", errUnsupportedFeature) + return nil, fmt.Errorf("GetDefaultBranch: %w", clients.ErrUnsupportedFeature) } func (client *localDirClient) ListCommits() ([]clients.Commit, error) { - return nil, fmt.Errorf("ListCommits: %w", errUnsupportedFeature) + return nil, fmt.Errorf("ListCommits: %w", clients.ErrUnsupportedFeature) } // ListReleases implements RepoClient.ListReleases. func (client *localDirClient) ListReleases() ([]clients.Release, error) { - return nil, fmt.Errorf("ListReleases: %w", errUnsupportedFeature) + return nil, fmt.Errorf("ListReleases: %w", clients.ErrUnsupportedFeature) } // ListContributors implements RepoClient.ListContributors. func (client *localDirClient) ListContributors() ([]clients.Contributor, error) { - return nil, fmt.Errorf("ListContributors: %w", errUnsupportedFeature) + return nil, fmt.Errorf("ListContributors: %w", clients.ErrUnsupportedFeature) } // ListSuccessfulWorkflowRuns implements RepoClient.WorkflowRunsByFilename. func (client *localDirClient) ListSuccessfulWorkflowRuns(filename string) ([]clients.WorkflowRun, error) { - return nil, fmt.Errorf("ListSuccessfulWorkflowRuns: %w", errUnsupportedFeature) + return nil, fmt.Errorf("ListSuccessfulWorkflowRuns: %w", clients.ErrUnsupportedFeature) } // ListCheckRunsForRef implements RepoClient.ListCheckRunsForRef. func (client *localDirClient) ListCheckRunsForRef(ref string) ([]clients.CheckRun, error) { - return nil, fmt.Errorf("ListCheckRunsForRef: %w", errUnsupportedFeature) + return nil, fmt.Errorf("ListCheckRunsForRef: %w", clients.ErrUnsupportedFeature) } // ListStatuses implements RepoClient.ListStatuses. func (client *localDirClient) ListStatuses(ref string) ([]clients.Status, error) { - return nil, fmt.Errorf("ListStatuses: %w", errUnsupportedFeature) + return nil, fmt.Errorf("ListStatuses: %w", clients.ErrUnsupportedFeature) } // Search implements RepoClient.Search. func (client *localDirClient) Search(request clients.SearchRequest) (clients.SearchResponse, error) { - return clients.SearchResponse{}, fmt.Errorf("Search: %w", errUnsupportedFeature) + return clients.SearchResponse{}, fmt.Errorf("Search: %w", clients.ErrUnsupportedFeature) } func (client *localDirClient) Close() error { diff --git a/clients/localdir/client_test.go b/clients/localdir/client_test.go index 106a4249ec6..f976a46a057 100644 --- a/clients/localdir/client_test.go +++ b/clients/localdir/client_test.go @@ -39,13 +39,13 @@ func TestClient_CreationAndCaching(t *testing.T) { { name: "invalid fullpath", outputFiles: []string{}, - inputFolder: "/invalid/fullpath", + inputFolder: "file:///invalid/fullpath", err: os.ErrNotExist, }, { name: "invalid relative path", outputFiles: []string{}, - inputFolder: "invalid/relative/path", + inputFolder: "file://invalid/relative/path", err: os.ErrNotExist, }, { @@ -53,7 +53,7 @@ func TestClient_CreationAndCaching(t *testing.T) { outputFiles: []string{ "file0", "dir1/file1", "dir1/dir2/file2", }, - inputFolder: "testdata/repo0", + inputFolder: "file://testdata/repo0", err: nil, }, } diff --git a/clients/localdir/repo.go b/clients/localdir/repo.go index 992312f8a56..176a13f8651 100644 --- a/clients/localdir/repo.go +++ b/clients/localdir/repo.go @@ -21,11 +21,17 @@ import ( "fmt" "os" "path" + "strings" clients "github.com/ossf/scorecard/v3/clients" ) -var errNotDirectory = errors.New("not a directory") +var ( + errNotDirectory = errors.New("not a directory") + errInvalidURI = errors.New("invalid URI") +) + +var filePrefix = "file://" type repoLocal struct { path string @@ -77,7 +83,10 @@ func (r *repoLocal) IsScorecardRepo() bool { // MakeLocalDirRepo returns an implementation of clients.Repo interface. func MakeLocalDirRepo(pathfn string) (clients.Repo, error) { - p := path.Clean(pathfn) + if !strings.HasPrefix(pathfn, filePrefix) { + return nil, fmt.Errorf("%w", errInvalidURI) + } + p := path.Clean(pathfn[len(filePrefix):]) repo := &repoLocal{ path: p, } @@ -85,6 +94,5 @@ func MakeLocalDirRepo(pathfn string) (clients.Repo, error) { if err := repo.IsValid(); err != nil { return nil, fmt.Errorf("error in IsValid: %w", err) } - return repo, nil } diff --git a/clients/repo_client.go b/clients/repo_client.go index edf872e9077..8432cb60d23 100644 --- a/clients/repo_client.go +++ b/clients/repo_client.go @@ -15,6 +15,11 @@ // Package clients defines the interface for RepoClient and related structs. package clients +import "errors" + +// ErrUnsupportedFeature indicates an API that is not supported by the client. +var ErrUnsupportedFeature = errors.New("unsupported feature") + // RepoClient interface is used by Scorecard checks to access a repo. type RepoClient interface { InitRepo(repo Repo) error diff --git a/cmd/root.go b/cmd/root.go index 6bd592eee3a..0c607696680 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -61,6 +61,14 @@ const ( formatDefault = "default" ) +// These strings must be the same as the ones used in +// checks.yaml for the "repos" field. +const ( + repoTypeUnknown = "unknown" + repoTypeLocal = "local" + repoTypeGitHub = "GitHub" +) + const ( scorecardLong = "A program that shows security scorecard for an open source software." scorecardUse = `./scorecard --repo= [--checks=check1,...] [--show-details] [--policy=file] @@ -96,27 +104,76 @@ func checksHavePolicies(sp *spol.ScorecardPolicy, enabledChecks checker.CheckNam return true } -func getEnabledChecks(sp *spol.ScorecardPolicy, argsChecks []string) (checker.CheckNameToFnMap, error) { +func getSupportedChecks(r string, checkDocs docs.Doc) ([]string, error) { + allChecks := checks.AllChecks + supportedChecks := []string{} + for check := range allChecks { + c, e := checkDocs.GetCheck(check) + if e != nil { + return nil, fmt.Errorf("checkDocs.GetCheck: %w", e) + } + types := c.GetSupportedRepoTypes() + for _, t := range types { + if r == t { + supportedChecks = append(supportedChecks, c.GetName()) + } + } + } + return supportedChecks, nil +} + +func isSupportedCheck(names []string, name string) bool { + for _, n := range names { + if n == name { + return true + } + } + return false +} + +func getEnabledChecks(sp *spol.ScorecardPolicy, argsChecks []string, + supportedChecks []string, repoType string) (checker.CheckNameToFnMap, error) { enabledChecks := checker.CheckNameToFnMap{} switch { case len(argsChecks) != 0: // Populate checks to run with the CLI arguments. for _, checkName := range argsChecks { + if !isSupportedCheck(supportedChecks, checkName) { + return enabledChecks, + sce.WithMessage(sce.ErrScorecardInternal, + fmt.Sprintf("repo type %s: unsupported check: %s", repoType, checkName)) + } if !enableCheck(checkName, &enabledChecks) { return enabledChecks, - sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("Invalid check: %s", checkName)) + 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(supportedChecks, checkName) { + return enabledChecks, + sce.WithMessage(sce.ErrScorecardInternal, + fmt.Sprintf("repo type %s: unsupported check: %s", repoType, checkName)) + } + if !enableCheck(checkName, &enabledChecks) { return enabledChecks, - sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("Invalid check: %s", checkName)) + sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("invalid check: %s", checkName)) } } default: + // Enable all checks that are supported. + for checkName := range checks.AllChecks { + if !isSupportedCheck(supportedChecks, checkName) { + continue + } + if !enableCheck(checkName, &enabledChecks) { + return enabledChecks, + sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("invalid check: %s", checkName)) + } + } enabledChecks = checks.AllChecks } @@ -138,16 +195,22 @@ func validateFormat(format string) bool { } } -func getRepoAccessors(ctx context.Context, uri string, logger *zap.Logger) (clients.Repo, clients.RepoClient, error) { - if repo, err := localdir.MakeLocalDirRepo(uri); err == nil { +func getRepoAccessors(ctx context.Context, uri string, logger *zap.Logger) (clients.Repo, + clients.RepoClient, string, error) { + var repo clients.Repo + var errLocal error + var errGitHub error + if repo, errLocal = localdir.MakeLocalDirRepo(uri); errLocal == nil { // Local directory. - return repo, localdir.CreateLocalDirClient(ctx, logger), nil + return repo, localdir.CreateLocalDirClient(ctx, logger), repoTypeLocal, nil } - if repo, err := githubrepo.MakeGithubRepo(uri); err == nil { + + if repo, errGitHub = githubrepo.MakeGithubRepo(uri); errGitHub == nil { // GitHub URL. - return repo, githubrepo.CreateGithubRepoClient(ctx, logger), nil + return repo, githubrepo.CreateGithubRepoClient(ctx, logger), repoTypeGitHub, nil } - return nil, nil, sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("unspported URI: %s", uri)) + return nil, nil, repoTypeUnknown, + sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("unspported URI: %s: [%v, %v]", uri, errLocal, errGitHub)) } var rootCmd = &cobra.Command{ @@ -215,13 +278,24 @@ var rootCmd = &cobra.Command{ // nolint defer logger.Sync() // Flushes buffer, if any. - repoURI, repoClient, err := getRepoAccessors(ctx, repo, logger) + repoURI, repoClient, repoType, err := getRepoAccessors(ctx, repo, logger) if err != nil { log.Fatal(err) } defer repoClient.Close() - enabledChecks, err := getEnabledChecks(policy, checksToRun) + // Read docs. + checkDocs, err := docs.Read() + if err != nil { + log.Fatalf("cannot read yaml file: %v", err) + } + + supportedChecks, err := getSupportedChecks(repoType, checkDocs) + if err != nil { + log.Fatalf("cannot read supported checks: %v", err) + } + + enabledChecks, err := getEnabledChecks(policy, checksToRun, supportedChecks, repoType) if err != nil { panic(err) } @@ -250,12 +324,6 @@ var rootCmd = &cobra.Command{ fmt.Println("\nRESULTS\n-------") } - // TODO: move the doc inside Scorecard structure. - checkDocs, e := docs.Read() - if e != nil { - log.Fatalf("cannot read yaml file: %v", err) - } - switch format { case formatDefault: err = repoResult.AsString(showDetails, *logLevel, checkDocs, os.Stdout) diff --git a/cron/format/mock_doc.go b/cron/format/mock_doc.go index a2e105b2016..419d113220a 100644 --- a/cron/format/mock_doc.go +++ b/cron/format/mock_doc.go @@ -22,7 +22,7 @@ import ( type mockCheck struct { name, risk, short, description, url string - tags, remediation []string + tags, remediation, repos []string } func (c *mockCheck) GetName() string { @@ -53,6 +53,14 @@ func (c *mockCheck) GetTags() []string { return l } +func (c *mockCheck) GetSupportedRepoTypes() []string { + l := make([]string, len(c.repos)) + for i := range c.repos { + l[i] = strings.TrimSpace(c.repos[i]) + } + return l +} + func (c *mockCheck) GetDocumentationURL(commitish string) string { return c.url } diff --git a/docs/checks/doc.go b/docs/checks/doc.go index 47d9619ec0e..14961bac72e 100644 --- a/docs/checks/doc.go +++ b/docs/checks/doc.go @@ -30,5 +30,6 @@ type CheckDoc interface { GetDescription() string GetRemediation() []string GetTags() []string + GetSupportedRepoTypes() []string GetDocumentationURL(commitish string) string } diff --git a/docs/checks/impl.go b/docs/checks/impl.go index 57863bfdc7f..3bb4f4bb43b 100644 --- a/docs/checks/impl.go +++ b/docs/checks/impl.go @@ -108,6 +108,16 @@ func (c *CheckDocImpl) GetRemediation() []string { return c.internalCheck.Remediation } +// GetSupportedRepoTypes returns the list of repo +// types the check supports. +func (c *CheckDocImpl) GetSupportedRepoTypes() []string { + l := strings.Split(c.internalCheck.Repos, ",") + for i := range l { + l[i] = strings.TrimSpace(l[i]) + } + return l +} + // GetTags returns the list of tags or the check. func (c *CheckDocImpl) GetTags() []string { l := strings.Split(c.internalCheck.Tags, ",") diff --git a/docs/checks/internal/checks.yaml b/docs/checks/internal/checks.yaml index 8b881c3d799..04a68b0ae04 100644 --- a/docs/checks/internal/checks.yaml +++ b/docs/checks/internal/checks.yaml @@ -18,6 +18,7 @@ checks: Maintained: risk: High tags: supply-chain, security + repos: GitHub short: Determines if the project is "actively maintained". description: | Risk: `High` (possibly unpatched vulnerabilities) @@ -43,6 +44,7 @@ checks: Dependency-Update-Tool: risk: High tags: supply-chain, security, dependencies + repos: GitHub, local short: Determines if the project uses a dependency update tool. description: | Risk: `High` (possibly vulnerable to attacks on known flaws) @@ -73,6 +75,7 @@ checks: Binary-Artifacts: risk: High tags: supply-chain, security, dependencies + repos: GitHub, local short: Determines if the project has generated executable (binary) artifacts in the source repository. description: | Risk: `High` (non-reviewable code) @@ -123,6 +126,7 @@ checks: Branch-Protection: risk: High tags: supply-chain, security, source-code, code-reviews + repos: GitHub short: Determines if the default and release branches are protected with GitHub's branch protection settings. description: | Risk: `High` (vulnerable to intentional malicious code injection) @@ -177,6 +181,7 @@ checks: CI-Tests: risk: Low tags: supply-chain, testing + repos: GitHub short: Determines if the project runs tests before pull requests are merged. description: | Risk: `Low` (possible unknown vulnerabilities) @@ -210,6 +215,7 @@ checks: CII-Best-Practices: risk: Low tags: security-awareness, security-training, security + repos: GitHub short: Determines if the project has a CII Best Practices Badge. description: | Risk: `Low` (possibly not following security best practices) @@ -249,6 +255,7 @@ checks: Code-Review: risk: High tags: supply-chain, security, source-code, code-reviews + repos: GitHub short: Determines if the project requires code review before pull requests (aka merge requests) are merged. description: | Risk: `High` (unintentional vulnerabilities or possible injection of malicious @@ -306,6 +313,7 @@ checks: Contributors: risk: Low tags: source-code + repos: GitHub short: Determines if the project has a set of contributors from multiple organizations (e.g., companies). description: | Risk: `Low` (lower number of trusted code reviewers) @@ -336,6 +344,7 @@ checks: Fuzzing: risk: Medium tags: supply-chain, security, testing + repos: GitHub short: Determines if the project uses fuzzing. description: | Risk: `Medium` (possible vulnerabilities in code) @@ -361,6 +370,7 @@ checks: Packaging: risk: Medium tags: supply-chain, security, releases + repos: GitHub short: Determines if the project is published as a package that others can easily download, install, easily update, and uninstall. description: | Risk: `Medium` (users possibly missing security updates) @@ -404,6 +414,7 @@ checks: Pinned-Dependencies: risk: Medium tags: supply-chain, security, dependencies + repos: GitHub, local short: Determines if the project has declared and pinned its dependencies. description: | Risk: `Medium` (possible compromised dependencies) @@ -483,10 +494,10 @@ checks: Github's [dependabot](https://github.blog/2020-06-01-keep-all-your-packages-up-to-date-with-dependabot/) or [renovate bot](https://github.com/renovatebot/renovate). - SAST: risk: Medium tags: supply-chain, security, testing + repos: GitHub short: Determines if the project uses static code analysis. description: | Risk: `Medium` (possible unknown bugs) @@ -517,6 +528,7 @@ checks: Security-Policy: risk: Medium short: Determines if the project has published a security policy. + repos: GitHub tags: supply-chain, security, policy description: | Risk: `Medium` (possible insecure reporting of vulnerabilities) @@ -539,10 +551,10 @@ checks: - >- For GitHub, see more information [here](https://docs.github.com/en/code-security/getting-started/adding-a-security-policy-to-your-repository.). - Signed-Releases: risk: High tags: supply-chain, security, releases + repos: GitHub short: Determines if the project cryptographically signs release artifacts. description: | Risk: `High` (possibility of installing malicious releases) @@ -575,6 +587,7 @@ checks: Token-Permissions: risk: High tags: supply-chain, security, infrastructure + repos: GitHub, local short: Determines if the project's workflows follow the principle of least privilege. description: | Risk: `High` (vulnerable to malicious code additions) @@ -602,6 +615,7 @@ checks: Vulnerabilities: risk: High tags: supply-chain, security, vulnerabilities + repos: GitHub short: Determines if the project has open, known unfixed vulnerabilities. description: | Risk: `High` (known vulnerabilities) diff --git a/docs/checks/internal/reader.go b/docs/checks/internal/reader.go index f17f00a952f..0bf3d02443e 100644 --- a/docs/checks/internal/reader.go +++ b/docs/checks/internal/reader.go @@ -34,6 +34,7 @@ type Check struct { Short string `yaml:"short"` Description string `yaml:"description"` Tags string `yaml:"tags"` + Repos string `yaml:"repos"` Remediation []string `yaml:"remediation"` Name string `yaml:"-"` URL string `yaml:"-"` diff --git a/docs/checks/internal/validate/main.go b/docs/checks/internal/validate/main.go index b075d9ff57e..267e3ce7c0b 100644 --- a/docs/checks/internal/validate/main.go +++ b/docs/checks/internal/validate/main.go @@ -21,7 +21,10 @@ import ( docs "github.com/ossf/scorecard/v3/docs/checks" ) -var allowedRisks = map[string]bool{"Critical": true, "High": true, "Medium": true, "Low": true} +var ( + allowedRisks = map[string]bool{"Critical": true, "High": true, "Medium": true, "Low": true} + allowedRepoTypes = map[string]bool{"GitHub": true, "local": true} +) func main() { m, err := docs.Read() @@ -57,6 +60,17 @@ func main() { // nolint: goerr113 panic(fmt.Errorf("risk for checkName: %s is invalid: '%s'", check, r)) } + repoTypes := c.GetSupportedRepoTypes() + if len(repoTypes) == 0 { + // nolint: goerr113 + panic(fmt.Errorf("repos for checkName: %s is empty", check)) + } + for _, rt := range repoTypes { + if _, exists := allowedRepoTypes[rt]; !exists { + // nolint: goerr113 + panic(fmt.Errorf("repo type for checkName: %s is invalid: '%s'", check, rt)) + } + } } for _, check := range m.GetChecks() { if _, exists := allChecks[check.GetName()]; !exists { diff --git a/pkg/mock_doc.go b/pkg/mock_doc.go index d362845bfb4..5a9acf2906c 100644 --- a/pkg/mock_doc.go +++ b/pkg/mock_doc.go @@ -22,7 +22,7 @@ import ( type mockCheck struct { name, risk, short, description, url string - tags, remediation []string + tags, remediation, repos []string } func (c *mockCheck) GetName() string { @@ -53,6 +53,14 @@ func (c *mockCheck) GetTags() []string { return l } +func (c *mockCheck) GetSupportedRepoTypes() []string { + l := make([]string, len(c.repos)) + for i := range c.repos { + l[i] = strings.TrimSpace(c.repos[i]) + } + return l +} + func (c *mockCheck) GetDocumentationURL(commitish string) string { return c.url } diff --git a/pkg/scorecard.go b/pkg/scorecard.go index 2e8c65e909a..a0b44a56be5 100644 --- a/pkg/scorecard.go +++ b/pkg/scorecard.go @@ -17,6 +17,7 @@ package pkg import ( "context" + "errors" "fmt" "sync" "time" @@ -55,7 +56,7 @@ func runEnabledChecks(ctx context.Context, func getRepoCommitHash(r clients.RepoClient) (string, error) { commits, err := r.ListCommits() - if err != nil { + if err != nil && !errors.Is(err, clients.ErrUnsupportedFeature) { return "", sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("ListCommits:%v", err.Error())) }