diff --git a/Makefile b/Makefile index 1e4880f0abc..fb27d9d387c 100644 --- a/Makefile +++ b/Makefile @@ -139,7 +139,8 @@ generate-mocks: clients/mockclients/repo_client.go \ clients/mockclients/repo.go \ clients/mockclients/cii_client.go \ checks/mockclients/vulnerabilities.go \ - cmd/packagemanager_mockclient.go + cmd/internal/packagemanager/packagemanager_mockclient.go \ + cmd/internal/nuget/nuget_mockclient.go clients/mockclients/repo_client.go: clients/repo_client.go | $(MOCKGEN) # Generating MockRepoClient $(MOCKGEN) -source=clients/repo_client.go -destination=clients/mockclients/repo_client.go -package=mockrepo -copyright_file=clients/mockclients/license.txt @@ -152,9 +153,12 @@ clients/mockclients/cii_client.go: clients/cii_client.go | $(MOCKGEN) checks/mockclients/vulnerabilities.go: clients/vulnerabilities.go | $(MOCKGEN) # Generating MockCIIClient $(MOCKGEN) -source=clients/vulnerabilities.go -destination=clients/mockclients/vulnerabilities.go -package=mockrepo -copyright_file=clients/mockclients/license.txt -cmd/packagemanager_mockclient.go: cmd/packagemanager_client.go | $(MOCKGEN) +cmd/internal/packagemanager/packagemanager_mockclient.go: cmd/internal/packagemanager/client.go | $(MOCKGEN) # Generating MockPackageManagerClient - $(MOCKGEN) -source=cmd/packagemanager_client.go -destination=cmd/packagemanager_mockclient.go -package=cmd -copyright_file=clients/mockclients/license.txt + $(MOCKGEN) -source=cmd/internal/packagemanager/client.go -destination=cmd/internal/packagemanager/packagemanager_mockclient.go -package=packagemanager -copyright_file=clients/mockclients/license.txt +cmd/internal/nuget/nuget_mockclient.go: cmd/internal/nuget/client.go | $(MOCKGEN) + # Generating MockNugetClient + $(MOCKGEN) -source=cmd/internal/nuget/client.go -destination=cmd/internal/nuget/nuget_mockclient.go -package=nuget -copyright_file=clients/mockclients/license.txt generate-docs: ## Generates docs generate-docs: validate-docs docs/checks.md diff --git a/README.md b/README.md index 2055a4751f7..0201b8fbc00 100644 --- a/README.md +++ b/README.md @@ -420,7 +420,7 @@ scorecard --repo=org/repo ##### Using a Package manager -For projects in the `--npm`, `--pypi`, or `--rubygems` ecosystems, you have the +For projects in the `--npm`, `--pypi`, `--rubygems`, or `--nuget` ecosystems, you have the option to run Scorecard using a package manager. Provide the package name to run the checks on the corresponding GitHub source code. diff --git a/cmd/internal/nuget/client.go b/cmd/internal/nuget/client.go new file mode 100644 index 00000000000..deb3d863e40 --- /dev/null +++ b/cmd/internal/nuget/client.go @@ -0,0 +1,275 @@ +// Copyright 2020 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 nuget implements Nuget API client. +package nuget + +import ( + "encoding/json" + "encoding/xml" + "fmt" + "io" + "net/http" + "regexp" + "strings" + + "golang.org/x/exp/slices" + + pmc "github.com/ossf/scorecard/v4/cmd/internal/packagemanager" + sce "github.com/ossf/scorecard/v4/errors" +) + +type indexResults struct { + Resources []indexResult `json:"resources"` +} + +func (n indexResults) findResourceByType(resultType string) (string, error) { + resourceIndex := slices.IndexFunc(n.Resources, + func(n indexResult) bool { return n.Type == resultType }) + if resourceIndex == -1 { + return "", sce.WithMessage(sce.ErrScorecardInternal, + fmt.Sprintf("failed to find %v URI at nuget index json", resultType)) + } + + return n.Resources[resourceIndex].ID, nil +} + +type indexResult struct { + ID string `json:"@id"` + Type string `json:"@type"` +} + +type packageRegistrationCatalogRoot struct { + Pages []packageRegistrationCatalogPage `json:"items"` +} + +func (n packageRegistrationCatalogRoot) latestVersion(manager pmc.Client) (string, error) { + for pageIndex := len(n.Pages) - 1; pageIndex >= 0; pageIndex-- { + page := n.Pages[pageIndex] + if page.Packages == nil { + err := decodeResponseFromClient(func() (*http.Response, error) { + //nolint: wrapcheck + return manager.GetURI(page.ID) + }, + func(rc io.ReadCloser) error { + //nolint: wrapcheck + return json.NewDecoder(rc).Decode(&page) + }, "nuget package registration page") + if err != nil { + return "", err + } + } + for packageIndex := len(page.Packages) - 1; packageIndex >= 0; packageIndex-- { + base, preReleaseSuffix := parseNugetSemVer(page.Packages[packageIndex].Entry.Version) + // skipping non listed and pre-releases + if page.Packages[packageIndex].Entry.Listed && len(strings.TrimSpace(preReleaseSuffix)) == 0 { + return base, nil + } + } + } + return "", sce.WithMessage(sce.ErrScorecardInternal, "failed to get a listed version for package") +} + +type packageRegistrationCatalogPage struct { + ID string `json:"@id"` + Packages []packageRegistrationCatalogItem `json:"items"` +} + +type packageRegistrationCatalogItem struct { + Entry packageRegistrationCatalogEntry `json:"catalogEntry"` +} + +type packageRegistrationCatalogEntry struct { + Version string `json:"version"` + Listed bool `json:"listed"` +} + +func (e *packageRegistrationCatalogEntry) UnmarshalJSON(text []byte) error { + type Alias packageRegistrationCatalogEntry + aux := Alias{ + Listed: true, // set the default value before parsing JSON + } + if err := json.Unmarshal(text, &aux); err != nil { + return fmt.Errorf("failed to unmarshal json: %w", err) + } + *e = packageRegistrationCatalogEntry(aux) + return nil +} + +type packageNuspec struct { + XMLName xml.Name `xml:"package"` + Metadata nuspecMetadata `xml:"metadata"` +} + +func (p *packageNuspec) projectURL(packageName string) (string, error) { + for _, projectURL := range []string{p.Metadata.Repository.URL, p.Metadata.ProjectURL} { + projectURL = strings.TrimSpace(projectURL) + if projectURL != "" && isSupportedProjectURL(projectURL) { + projectURL = strings.TrimSuffix(projectURL, "/") + projectURL = strings.TrimSuffix(projectURL, ".git") + return projectURL, nil + } + } + return "", sce.WithMessage(sce.ErrScorecardInternal, + fmt.Sprintf("source repo is not defined for nuget package %v", packageName)) +} + +type nuspecMetadata struct { + XMLName xml.Name `xml:"metadata"` + ProjectURL string `xml:"projectUrl"` + Repository nuspecRepository `xml:"repository"` +} + +type nuspecRepository struct { + XMLName xml.Name `xml:"repository"` + URL string `xml:"url,attr"` +} + +type Client interface { + GitRepositoryByPackageName(packageName string) (string, error) +} + +type NugetClient struct { + Manager pmc.Client +} + +func (c NugetClient) GitRepositoryByPackageName(packageName string) (string, error) { + packageBaseURL, registrationBaseURL, err := c.baseUrls() + if err != nil { + return "", err + } + + packageSpec, err := c.packageSpec(packageBaseURL, registrationBaseURL, packageName) + if err != nil { + return "", err + } + + packageURL, err := packageSpec.projectURL(packageName) + if err != nil { + return "", err + } + return packageURL, nil +} + +func (c *NugetClient) packageSpec(packageBaseURL, registrationBaseURL, packageName string) (packageNuspec, error) { + lowerCasePackageName := strings.ToLower(packageName) + lastPackageVersion, err := c.latestListedVersion(registrationBaseURL, + lowerCasePackageName) + if err != nil { + return packageNuspec{}, err + } + packageSpecResults := &packageNuspec{} + err = decodeResponseFromClient(func() (*http.Response, error) { + //nolint: wrapcheck + return c.Manager.Get( + packageBaseURL+"%[1]v/"+lastPackageVersion+"/%[1]v.nuspec", lowerCasePackageName) + }, + func(rc io.ReadCloser) error { + //nolint: wrapcheck + return xml.NewDecoder(rc).Decode(packageSpecResults) + }, "nuget package spec") + + if err != nil { + return packageNuspec{}, err + } + if packageSpecResults.Metadata == (nuspecMetadata{}) { + return packageNuspec{}, sce.WithMessage(sce.ErrScorecardInternal, + "Nuget nuspec xml Metadata is empty") + } + return *packageSpecResults, nil +} + +func (c *NugetClient) baseUrls() (string, string, error) { + indexURL := "https://api.nuget.org/v3/index.json" + indexResults := &indexResults{} + err := decodeResponseFromClient(func() (*http.Response, error) { + //nolint: wrapcheck + return c.Manager.GetURI(indexURL) + }, + func(rc io.ReadCloser) error { + //nolint: wrapcheck + return json.NewDecoder(rc).Decode(indexResults) + }, "nuget index json") + if err != nil { + return "", "", err + } + packageBaseURL, err := indexResults.findResourceByType("PackageBaseAddress/3.0.0") + if err != nil { + return "", "", err + } + registrationBaseURL, err := indexResults.findResourceByType("RegistrationsBaseUrl/3.6.0") + if err != nil { + return "", "", err + } + return packageBaseURL, registrationBaseURL, nil +} + +// Gets the latest listed nuget version of a package, based on the protocol defined at +// https://learn.microsoft.com/en-us/nuget/api/package-base-address-resource#enumerate-package-versions +func (c *NugetClient) latestListedVersion(baseURL, packageName string) (string, error) { + packageRegistrationCatalogRoot := &packageRegistrationCatalogRoot{} + err := decodeResponseFromClient(func() (*http.Response, error) { + //nolint: wrapcheck + return c.Manager.Get(baseURL+"%s/index.json", packageName) + }, + func(rc io.ReadCloser) error { + //nolint: wrapcheck + return json.NewDecoder(rc).Decode(packageRegistrationCatalogRoot) + }, "nuget package registration index json") + if err != nil { + return "", err + } + return packageRegistrationCatalogRoot.latestVersion(c.Manager) +} + +func isSupportedProjectURL(projectURL string) bool { + pattern := `^(?:https?://)?(?:www\.)?(?:github|gitlab)\.com/([A-Za-z0-9_\.-]+)/([A-Za-z0-9_\./-]+)$` + regex := regexp.MustCompile(pattern) + return regex.MatchString(projectURL) +} + +// Nuget semver diverges from Semantic Versioning. +// This method returns the Nuget represntation of version and pre release strings. +// nolint: lll // long URL +// more info: https://learn.microsoft.com/en-us/nuget/concepts/package-versioning#where-nugetversion-diverges-from-semantic-versioning +func parseNugetSemVer(versionString string) (base, preReleaseSuffix string) { + metadataAndVersion := strings.Split(versionString, "+") + prereleaseAndVersions := strings.Split(metadataAndVersion[0], "-") + if len(prereleaseAndVersions) == 1 { + return prereleaseAndVersions[0], "" + } + return prereleaseAndVersions[0], prereleaseAndVersions[1] +} + +func decodeResponseFromClient(getFunc func() (*http.Response, error), + decodeFunc func(io.ReadCloser) error, name string, +) error { + response, err := getFunc() + if err != nil { + return sce.WithMessage(sce.ErrScorecardInternal, + fmt.Sprintf("failed to get %s: %v", name, err)) + } + if response.StatusCode != http.StatusOK { + return sce.WithMessage(sce.ErrScorecardInternal, + fmt.Sprintf("failed to get %s with status: %v", name, response.Status)) + } + defer response.Body.Close() + + err = decodeFunc(response.Body) + if err != nil { + return sce.WithMessage(sce.ErrScorecardInternal, + fmt.Sprintf("failed to parse %s: %v", name, err)) + } + return nil +} diff --git a/cmd/internal/nuget/client_test.go b/cmd/internal/nuget/client_test.go new file mode 100644 index 00000000000..bb5f8c2cfec --- /dev/null +++ b/cmd/internal/nuget/client_test.go @@ -0,0 +1,623 @@ +// Copyright 2020 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 nuget implements Nuget API client. +package nuget + +import ( + "bytes" + "errors" + "fmt" + "io" + "net/http" + "os" + "strings" + "testing" + + "github.com/golang/mock/gomock" + "golang.org/x/exp/slices" + + pmc "github.com/ossf/scorecard/v4/cmd/internal/packagemanager" +) + +type resultPackagePage struct { + url string + response string +} +type nugetTestArgs struct { + inputPackageName string + expectedPackageName string + resultIndex string + resultPackageRegistrationIndex string + resultPackageSpec string + version string + resultPackageRegistrationPages []resultPackagePage +} +type nugetTest struct { + name string + want string + args nugetTestArgs + wantErr bool +} + +func Test_fetchGitRepositoryFromNuget(t *testing.T) { + t.Parallel() + + tests := []nugetTest{ + { + name: "find latest version in single page", + + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_single.json", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "package_spec.xml", + version: "4.0.1", + }, + want: "https://github.com/foo/foo.net", + wantErr: false, + }, + { + name: "find by lowercase package name", + + args: nugetTestArgs{ + inputPackageName: "Nuget-Package", + expectedPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_single.json", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "package_spec.xml", + version: "4.0.1", + }, + want: "https://github.com/foo/foo.net", + wantErr: false, + }, + { + name: "find and remove trailing slash", + + args: nugetTestArgs{ + inputPackageName: "Nuget-Package", + expectedPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_single.json", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "package_spec_trailing_slash.xml", + version: "4.0.1", + }, + want: "https://github.com/foo/foo.net", + wantErr: false, + }, + { + name: "find and remove git ending", + + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_single.json", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "package_spec_git_ending.xml", + version: "4.0.1", + }, + want: "https://github.com/foo/foo.net", + wantErr: false, + }, + { + name: "find and handle four digit version", + + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_four_digit_version.json", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "package_spec_four_digit_version.xml", + version: "1.60.0.2981", + }, + want: "https://github.com/foo/foo.net", + wantErr: false, + }, + { + name: "skip semver metadata", + + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_metadata_version.json", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "package_spec.xml", + version: "4.0.1", + }, + want: "https://github.com/foo/foo.net", + wantErr: false, + }, + { + name: "skip pre release", + + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_pre_release_version.json", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "package_spec.xml", + version: "4.0.1", + }, + want: "https://github.com/foo/foo.net", + wantErr: false, + }, + { + name: "skip pre release and metadata", + + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_pre_release_and_metadata_version.json", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "package_spec.xml", + version: "4.0.1", + }, + want: "https://github.com/foo/foo.net", + wantErr: false, + }, + { + name: "find in project url if repository missing", + + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_single.json", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "package_spec_project_url.xml", + version: "4.0.1", + }, + want: "https://github.com/foo/foo.net", + wantErr: false, + }, + { + name: "get github project url without git ending", + + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_single.json", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "package_spec_project_url_git_ending.xml", + version: "4.0.1", + }, + want: "https://github.com/foo/foo.net", + wantErr: false, + }, + { + name: "get gitlab project url if repository url missing", + + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_single.json", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "package_spec_project_url_gitlab.xml", + version: "4.0.1", + }, + want: "https://gitlab.com/foo/foo.net", + wantErr: false, + }, + { + name: "error if project url is not gitlab or github", + + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_single.json", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "package_spec_project_url_not_supported.xml", + version: "4.0.1", + }, + want: "internal error: source repo is not defined for nuget package nuget-package", + wantErr: true, + }, + { + name: "find latest version in first of multiple pages", + + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_multiple.json", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "package_spec.xml", + version: "4.0.1", + }, + want: "https://github.com/foo/foo.net", + wantErr: false, + }, + { + name: "find latest version in first of multiple remote pages", + + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_multiple_remote.json", + resultPackageRegistrationPages: []resultPackagePage{ + { + url: "https://api.nuget.org/v3/registration5-semver1/Foo.NET/page1/index.json", + response: "package_registration_page_one.json", + }, + { + url: "https://api.nuget.org/v3/registration5-semver1/Foo.NET/page2/index.json", + response: "package_registration_page_two.json", + }, + }, + resultPackageSpec: "package_spec.xml", + version: "4.0.1", + }, + want: "https://github.com/foo/foo.net", + wantErr: false, + }, + { + name: "find latest version in last of multiple pages", + + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_multiple_last.json", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "package_spec.xml", + version: "4.0.1", + }, + want: "https://github.com/foo/foo.net", + wantErr: false, + }, + { + name: "find latest version in last of remote multiple pages", + + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_multiple_remote.json", + resultPackageRegistrationPages: []resultPackagePage{ + { + url: "https://api.nuget.org/v3/registration5-semver1/Foo.NET/page1/index.json", + response: "package_registration_page_one.json", + }, + { + url: "https://api.nuget.org/v3/registration5-semver1/Foo.NET/page2/index.json", + response: "package_registration_page_two_not_listed.json", + }, + }, + resultPackageSpec: "package_spec.xml", + version: "3.5.2", + }, + want: "https://github.com/foo/foo.net", + wantErr: false, + }, + { + name: "find latest version with default listed value true", + + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_default_listed_true.json", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "package_spec.xml", + version: "4.0.1", + }, + want: "https://github.com/foo/foo.net", + wantErr: false, + }, + { + name: "skip not listed versions", + + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_with_not_listed.json", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "package_spec.xml", + version: "3.5.8", + }, + want: "https://github.com/foo/foo.net", + wantErr: false, + }, + { + name: "error if no listed version", + + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_all_not_listed.json", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "", + version: "", + }, + want: "internal error: failed to get a listed version for package", + wantErr: true, + }, + { + name: "error no index", + + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "", + resultPackageRegistrationIndex: "", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "", + }, + want: "internal error: failed to get nuget index json: error", + wantErr: true, + }, + { + name: "error bad index", + + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "text", + resultPackageRegistrationIndex: "", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "", + }, + want: "internal error: failed to parse nuget index json: invalid character 'e' in literal true (expecting 'r')", + wantErr: true, + }, + { + name: "error package registration index", + + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "", + }, + want: "internal error: failed to get nuget package registration index json: error", + wantErr: true, + }, + { + name: "error bad package index", + + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "text", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "", + }, + //nolint + want: "internal error: failed to parse nuget package registration index json: invalid character 'e' in literal true (expecting 'r')", + wantErr: true, + }, + { + name: "error package registration page", + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_multiple_remote.json", + resultPackageRegistrationPages: []resultPackagePage{ + { + url: "https://api.nuget.org/v3/registration5-semver1/Foo.NET/page1/index.json", + response: "", + }, + { + url: "https://api.nuget.org/v3/registration5-semver1/Foo.NET/page2/index.json", + response: "", + }, + }, + resultPackageSpec: "", + }, + want: "internal error: failed to get nuget package registration page: error", + wantErr: true, + }, + { + name: "error in package spec", + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_single.json", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "", + version: "4.0.1", + }, + want: "internal error: failed to get nuget package spec: error", + wantErr: true, + }, + { + name: "error bad package spec", + + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_multiple_remote.json", + resultPackageRegistrationPages: []resultPackagePage{ + { + url: "https://api.nuget.org/v3/registration5-semver1/Foo.NET/page2/index.json", + response: "text", + }, + }, + resultPackageSpec: "", + }, + //nolint + want: "internal error: failed to parse nuget package registration page: invalid character 'e' in literal true (expecting 'r')", + wantErr: true, + }, + { + name: "error package spec", + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_single.json", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "text", + version: "4.0.1", + }, + want: "internal error: failed to parse nuget package spec: EOF", + wantErr: true, + }, + { + name: "bad remote package page", + + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_multiple_remote.json", + resultPackageRegistrationPages: []resultPackagePage{ + { + url: "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json#page/1", + response: "text", + }, + }, + resultPackageSpec: "", + }, + want: "internal error: failed to get nuget package registration page: error", + wantErr: true, + }, + { + name: "error no registration url", + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index_bad_registration_base.json", + resultPackageRegistrationIndex: "", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "", + version: "4.0.1", + }, + want: "internal error: failed to find RegistrationsBaseUrl/3.6.0 URI at nuget index json", + wantErr: true, + }, + { + name: "error no package base url", + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index_bad_package_base.json", + resultPackageRegistrationIndex: "", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "", + version: "4.0.1", + }, + want: "internal error: failed to find PackageBaseAddress/3.0.0 URI at nuget index json", + wantErr: true, + }, + { + name: "error marhsal entry", + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_marshal_error.json", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "", + version: "", + }, + //nolint + want: "internal error: failed to parse nuget package registration index json: failed to unmarshal json: json: cannot unmarshal number into Go struct field Alias.listed of type bool", + wantErr: true, + }, + { + name: "empty package spec", + args: nugetTestArgs{ + inputPackageName: "nuget-package", + resultIndex: "index.json", + resultPackageRegistrationIndex: "package_registration_index_single.json", + resultPackageRegistrationPages: []resultPackagePage{}, + resultPackageSpec: "package_spec_error.xml", + version: "4.0.1", + }, + want: "internal error: source repo is not defined for nuget package nuget-package", + wantErr: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + p := pmc.NewMockClient(ctrl) + p.EXPECT().GetURI(gomock.Any()). + DoAndReturn(func(url string) (*http.Response, error) { + return nugetIndexOrPageTestResults(url, &tt) + }).AnyTimes() + expectedPackageName := tt.args.expectedPackageName + if strings.TrimSpace(expectedPackageName) == "" { + expectedPackageName = tt.args.inputPackageName + } + + p.EXPECT().Get(gomock.Any(), expectedPackageName). + DoAndReturn(func(url, inputPackageName string) (*http.Response, error) { + return nugetPackageIndexAndSpecResponse(t, url, &tt) + }).AnyTimes() + client := NugetClient{Manager: p} + got, err := client.GitRepositoryByPackageName(tt.args.inputPackageName) + if err != nil { + if !tt.wantErr { + t.Errorf("fetchGitRepositoryFromNuget() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err.Error() != tt.want { + t.Errorf("fetchGitRepositoryFromNuget() err.Error() = %v, wanted %v", err.Error(), tt.want) + return + } + return + } + + if got != tt.want { + t.Errorf("fetchGitRepositoryFromNuget() = %v, want %v", got, tt.want) + } + }) + } +} + +func nugetIndexOrPageTestResults(url string, test *nugetTest) (*http.Response, error) { + if url == "https://api.nuget.org/v3/index.json" { + return testResult(test.wantErr, test.args.resultIndex) + } + urlResponseIndex := slices.IndexFunc(test.args.resultPackageRegistrationPages, + func(page resultPackagePage) bool { return page.url == url }) + if urlResponseIndex == -1 { + //nolint + return nil, errors.New("error") + } + page := test.args.resultPackageRegistrationPages[urlResponseIndex] + return testResult(test.wantErr, page.response) +} + +func nugetPackageIndexAndSpecResponse(t *testing.T, url string, test *nugetTest) (*http.Response, error) { + t.Helper() + if strings.HasSuffix(url, "index.json") { + return testResult(test.wantErr, test.args.resultPackageRegistrationIndex) + } else if strings.HasSuffix(url, ".nuspec") { + if strings.Contains(url, fmt.Sprintf("/%v/", test.args.version)) { + return testResult(test.wantErr, test.args.resultPackageSpec) + } + t.Errorf("fetchGitRepositoryFromNuget() version = %v, expected version = %v", url, test.args.version) + } + //nolint + return nil, errors.New("error") +} + +func testResult(wantErr bool, responseFileName string) (*http.Response, error) { + if wantErr && responseFileName == "" { + //nolint + return nil, errors.New("error") + } + if wantErr && responseFileName == "text" { + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString("text")), + }, nil + } + content, err := os.ReadFile("./testdata/" + responseFileName) + if err != nil { + return nil, fmt.Errorf("%w", err) + } + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(string(content))), + }, nil +} diff --git a/cmd/internal/nuget/nuget_mockclient.go b/cmd/internal/nuget/nuget_mockclient.go new file mode 100644 index 00000000000..b02a9968e99 --- /dev/null +++ b/cmd/internal/nuget/nuget_mockclient.go @@ -0,0 +1,64 @@ +// Copyright 2021 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. +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: cmd/internal/nuget/client.go + +// Package nuget is a generated GoMock package. +package nuget + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockClient is a mock of Client interface. +type MockClient struct { + ctrl *gomock.Controller + recorder *MockClientMockRecorder +} + +// MockClientMockRecorder is the mock recorder for MockClient. +type MockClientMockRecorder struct { + mock *MockClient +} + +// NewMockClient creates a new mock instance. +func NewMockClient(ctrl *gomock.Controller) *MockClient { + mock := &MockClient{ctrl: ctrl} + mock.recorder = &MockClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClient) EXPECT() *MockClientMockRecorder { + return m.recorder +} + +// GitRepositoryByPackageName mocks base method. +func (m *MockClient) GitRepositoryByPackageName(packageName string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GitRepositoryByPackageName", packageName) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GitRepositoryByPackageName indicates an expected call of GitRepositoryByPackageName. +func (mr *MockClientMockRecorder) GitRepositoryByPackageName(packageName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GitRepositoryByPackageName", reflect.TypeOf((*MockClient)(nil).GitRepositoryByPackageName), packageName) +} diff --git a/cmd/internal/nuget/testdata/index.json b/cmd/internal/nuget/testdata/index.json new file mode 100644 index 00000000000..0229bf48aa2 --- /dev/null +++ b/cmd/internal/nuget/testdata/index.json @@ -0,0 +1,15 @@ +{ + "version": "3.0.0", + "resources": [ + { + "@id": "https://api.nuget.org/v3-flatcontainer/", + "@type": "PackageBaseAddress/3.0.0", + "comment": "Base URL of where NuGet packages are stored" + }, + { + "@id": "https://api.nuget.org/v3/registration5-gz-semver1/", + "@type": "RegistrationsBaseUrl/3.6.0", + "comment": "Base URL of Azure storage where NuGet package registration info." + } + ] +} diff --git a/cmd/internal/nuget/testdata/index_bad_package_base.json b/cmd/internal/nuget/testdata/index_bad_package_base.json new file mode 100644 index 00000000000..466aebc8845 --- /dev/null +++ b/cmd/internal/nuget/testdata/index_bad_package_base.json @@ -0,0 +1,15 @@ +{ + "version": "3.0.0", + "resources": [ + { + "@id": "https://api.nuget.org/v3-flatcontainer/", + "@type": "PackageBaseAddress/3.1.0", + "comment": "Base URL of where NuGet packages are stored, in the format ..." + }, + { + "@id": "https://api.nuget.org/v3/registration5-gz-semver1/", + "@type": "RegistrationsBaseUrl/3.6.0", + "comment": "Base URL of Azure storage where NuGet package registration info." + } + ] +} diff --git a/cmd/internal/nuget/testdata/index_bad_registration_base.json b/cmd/internal/nuget/testdata/index_bad_registration_base.json new file mode 100644 index 00000000000..3b0612a8dca --- /dev/null +++ b/cmd/internal/nuget/testdata/index_bad_registration_base.json @@ -0,0 +1,15 @@ +{ + "version": "3.0.0", + "resources": [ + { + "@id": "https://api.nuget.org/v3-flatcontainer/", + "@type": "PackageBaseAddress/3.0.0", + "comment": "Base URL of where NuGet packages are stored, in the format ..." + }, + { + "@id": "https://api.nuget.org/v3/registration5-gz-semver1/", + "@type": "RegistrationsBaseUrl/3.2.0", + "comment": "Base URL of Azure storage where NuGet package registration info." + } + ] +} diff --git a/cmd/internal/nuget/testdata/package_registration_index_all_not_listed.json b/cmd/internal/nuget/testdata/package_registration_index_all_not_listed.json new file mode 100644 index 00000000000..dd6f0c529f7 --- /dev/null +++ b/cmd/internal/nuget/testdata/package_registration_index_all_not_listed.json @@ -0,0 +1,33 @@ +{ + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json", + "count": 1, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json#page/1", + "@type": "catalog:CatalogPage", + "count": 2, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/3.5.8.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.3.5.8.json", + "@type": "PackageDetails", + "listed": false, + "version": "3.5.8" + } + }, + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/4.0.1.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.4.0.1.json", + "@type": "PackageDetails", + "listed": false, + "version": "4.0.1" + } + } + ] + } + ] +} diff --git a/cmd/internal/nuget/testdata/package_registration_index_default_listed_true.json b/cmd/internal/nuget/testdata/package_registration_index_default_listed_true.json new file mode 100644 index 00000000000..759aebd1e79 --- /dev/null +++ b/cmd/internal/nuget/testdata/package_registration_index_default_listed_true.json @@ -0,0 +1,32 @@ +{ + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json", + "count": 1, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json#page/1", + "@type": "catalog:CatalogPage", + "count": 2, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/3.5.8.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.3.5.8.json", + "@type": "PackageDetails", + "listed": true, + "version": "3.5.8" + } + }, + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/4.0.1.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.4.0.1.json", + "@type": "PackageDetails", + "version": "4.0.1" + } + } + ] + } + ] +} diff --git a/cmd/internal/nuget/testdata/package_registration_index_four_digit_version.json b/cmd/internal/nuget/testdata/package_registration_index_four_digit_version.json new file mode 100644 index 00000000000..5832accd249 --- /dev/null +++ b/cmd/internal/nuget/testdata/package_registration_index_four_digit_version.json @@ -0,0 +1,33 @@ +{ + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json", + "count": 1, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json#page/1", + "@type": "catalog:CatalogPage", + "count": 2, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/3.5.8.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022/Foo.NET.3.5.8.json", + "@type": "PackageDetails", + "listed": true, + "version": "3.5.8" + } + }, + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/1.60.0.2981.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022/Foo.NET.1.60.0.2981.json", + "@type": "PackageDetails", + "listed": true, + "version": "1.60.0.2981+metadata" + } + } + ] + } + ] +} diff --git a/cmd/internal/nuget/testdata/package_registration_index_marshal_error.json b/cmd/internal/nuget/testdata/package_registration_index_marshal_error.json new file mode 100644 index 00000000000..41d34fcd5f3 --- /dev/null +++ b/cmd/internal/nuget/testdata/package_registration_index_marshal_error.json @@ -0,0 +1,33 @@ +{ + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json", + "count": 1, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json#page/1", + "@type": "catalog:CatalogPage", + "count": 2, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/3.5.8.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.3.5.8.json", + "@type": "PackageDetails", + "listed": 123, + "version": "3.5.8" + } + }, + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/4.0.1.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.4.0.1.json", + "@type": "PackageDetails", + "listed": true, + "version": "4.0.1" + } + } + ] + } + ] +} diff --git a/cmd/internal/nuget/testdata/package_registration_index_metadata_version.json b/cmd/internal/nuget/testdata/package_registration_index_metadata_version.json new file mode 100644 index 00000000000..3450f7e2dd7 --- /dev/null +++ b/cmd/internal/nuget/testdata/package_registration_index_metadata_version.json @@ -0,0 +1,33 @@ +{ + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json", + "count": 1, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json#page/1", + "@type": "catalog:CatalogPage", + "count": 2, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/3.5.8.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.3.5.8.json", + "@type": "PackageDetails", + "listed": true, + "version": "3.5.8" + } + }, + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/4.0.1.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.4.0.1.json", + "@type": "PackageDetails", + "listed": true, + "version": "4.0.1+metadata" + } + } + ] + } + ] +} diff --git a/cmd/internal/nuget/testdata/package_registration_index_multiple.json b/cmd/internal/nuget/testdata/package_registration_index_multiple.json new file mode 100644 index 00000000000..4955d0c40d1 --- /dev/null +++ b/cmd/internal/nuget/testdata/package_registration_index_multiple.json @@ -0,0 +1,60 @@ +{ + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json", + "count": 2, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json#page/1", + "@type": "catalog:CatalogPage", + "count": 2, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/3.5.1.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.3.5.1.json", + "@type": "PackageDetails", + "listed": true, + "version": "3.5.1" + } + }, + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/3.5.2.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.3.5.2.json", + "@type": "PackageDetails", + "listed": true, + "version": "3.5.2" + } + } + ] + }, + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json#page/2", + "@type": "catalog:CatalogPage", + "count": 2, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/3.5.8.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.3.5.8.json", + "@type": "PackageDetails", + "listed": true, + "version": "3.5.8" + } + }, + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/4.0.1.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.4.0.1.json", + "@type": "PackageDetails", + "listed": true, + "version": "4.0.1" + } + } + ] + } + ] +} diff --git a/cmd/internal/nuget/testdata/package_registration_index_multiple_last.json b/cmd/internal/nuget/testdata/package_registration_index_multiple_last.json new file mode 100644 index 00000000000..5108054d428 --- /dev/null +++ b/cmd/internal/nuget/testdata/package_registration_index_multiple_last.json @@ -0,0 +1,60 @@ +{ + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json", + "count": 2, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json#page/1", + "@type": "catalog:CatalogPage", + "count": 2, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/3.5.8.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.3.5.8.json", + "@type": "PackageDetails", + "listed": true, + "version": "3.5.8" + } + }, + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/4.0.1.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.4.0.1.json", + "@type": "PackageDetails", + "listed": true, + "version": "4.0.1" + } + } + ] + }, + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json#page/2", + "@type": "catalog:CatalogPage", + "count": 2, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/4.1.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.4.1.json", + "@type": "PackageDetails", + "listed": false, + "version": "4.1" + } + }, + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/4.2.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.4.2.json", + "@type": "PackageDetails", + "listed": false, + "version": "4.2" + } + } + ] + } + ] +} diff --git a/cmd/internal/nuget/testdata/package_registration_index_multiple_remote.json b/cmd/internal/nuget/testdata/package_registration_index_multiple_remote.json new file mode 100644 index 00000000000..a9981079e1f --- /dev/null +++ b/cmd/internal/nuget/testdata/package_registration_index_multiple_remote.json @@ -0,0 +1,16 @@ +{ + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json", + "count": 2, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/page1/index.json", + "@type": "catalog:CatalogPage", + "count": 2 + }, + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/page2/index.json", + "@type": "catalog:CatalogPage", + "count": 2 + } + ] +} diff --git a/cmd/internal/nuget/testdata/package_registration_index_pre_release_and_metadata_version.json b/cmd/internal/nuget/testdata/package_registration_index_pre_release_and_metadata_version.json new file mode 100644 index 00000000000..8568e13a109 --- /dev/null +++ b/cmd/internal/nuget/testdata/package_registration_index_pre_release_and_metadata_version.json @@ -0,0 +1,33 @@ +{ + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json", + "count": 1, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json#page/1", + "@type": "catalog:CatalogPage", + "count": 2, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/3.5.8.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.3.5.8.json", + "@type": "PackageDetails", + "listed": true, + "version": "4.0.1+metadata" + } + }, + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/4.0.1.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.4.0.1.json", + "@type": "PackageDetails", + "listed": true, + "version": "4.0.1-beta+meta" + } + } + ] + } + ] +} diff --git a/cmd/internal/nuget/testdata/package_registration_index_pre_release_version.json b/cmd/internal/nuget/testdata/package_registration_index_pre_release_version.json new file mode 100644 index 00000000000..829faa8712a --- /dev/null +++ b/cmd/internal/nuget/testdata/package_registration_index_pre_release_version.json @@ -0,0 +1,33 @@ +{ + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json", + "count": 1, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json#page/1", + "@type": "catalog:CatalogPage", + "count": 2, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/3.5.8.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.3.5.8.json", + "@type": "PackageDetails", + "listed": true, + "version": "4.0.1" + } + }, + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/4.0.1.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.4.0.1.json", + "@type": "PackageDetails", + "listed": true, + "version": "4.0.1-beta" + } + } + ] + } + ] +} diff --git a/cmd/internal/nuget/testdata/package_registration_index_single.json b/cmd/internal/nuget/testdata/package_registration_index_single.json new file mode 100644 index 00000000000..4a45baaa483 --- /dev/null +++ b/cmd/internal/nuget/testdata/package_registration_index_single.json @@ -0,0 +1,33 @@ +{ + "@id": "https://api.nuget.org/v3/c-semver1/Foo.NET/index.json", + "count": 1, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json#page/1", + "@type": "catalog:CatalogPage", + "count": 2, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/3.5.8.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.3.5.8.json", + "@type": "PackageDetails", + "listed": true, + "version": "3.5.8" + } + }, + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/4.0.1.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.4.0.1.json", + "@type": "PackageDetails", + "listed": true, + "version": "4.0.1" + } + } + ] + } + ] +} diff --git a/cmd/internal/nuget/testdata/package_registration_index_with_not_listed.json b/cmd/internal/nuget/testdata/package_registration_index_with_not_listed.json new file mode 100644 index 00000000000..4303fc2a1c8 --- /dev/null +++ b/cmd/internal/nuget/testdata/package_registration_index_with_not_listed.json @@ -0,0 +1,33 @@ +{ + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json", + "count": 1, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json#page/1", + "@type": "catalog:CatalogPage", + "count": 2, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/3.5.8.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.3.5.8.json", + "@type": "PackageDetails", + "listed": true, + "version": "3.5.8" + } + }, + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/4.0.1.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.4.0.1.json", + "@type": "PackageDetails", + "listed": false, + "version": "4.0.1" + } + } + ] + } + ] +} diff --git a/cmd/internal/nuget/testdata/package_registration_page_one.json b/cmd/internal/nuget/testdata/package_registration_page_one.json new file mode 100644 index 00000000000..2043abb9c80 --- /dev/null +++ b/cmd/internal/nuget/testdata/package_registration_page_one.json @@ -0,0 +1,27 @@ +{ + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json#page/1", + "@type": "catalog:CatalogPage", + "count": 2, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/3.5.1.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.3.5.1.json", + "@type": "PackageDetails", + "listed": true, + "version": "3.5.1" + } + }, + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/3.5.2.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.3.5.2.json", + "@type": "PackageDetails", + "listed": true, + "version": "3.5.2" + } + } + ] +} diff --git a/cmd/internal/nuget/testdata/package_registration_page_two.json b/cmd/internal/nuget/testdata/package_registration_page_two.json new file mode 100644 index 00000000000..0a7b7aca0e3 --- /dev/null +++ b/cmd/internal/nuget/testdata/package_registration_page_two.json @@ -0,0 +1,27 @@ +{ + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json#page/2", + "@type": "catalog:CatalogPage", + "count": 2, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/3.5.8.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.3.5.8.json", + "@type": "PackageDetails", + "listed": true, + "version": "3.5.8" + } + }, + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/4.0.1.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.4.0.1.json", + "@type": "PackageDetails", + "listed": true, + "version": "4.0.1" + } + } + ] +} diff --git a/cmd/internal/nuget/testdata/package_registration_page_two_not_listed.json b/cmd/internal/nuget/testdata/package_registration_page_two_not_listed.json new file mode 100644 index 00000000000..9c9254668c5 --- /dev/null +++ b/cmd/internal/nuget/testdata/package_registration_page_two_not_listed.json @@ -0,0 +1,27 @@ +{ + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/index.json#page/2", + "@type": "catalog:CatalogPage", + "count": 2, + "items": [ + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/4.1.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.4.1.json", + "@type": "PackageDetails", + "listed": false, + "version": "4.1" + } + }, + { + "@id": "https://api.nuget.org/v3/registration5-semver1/Foo.NET/4.2.json", + "@type": "Package", + "catalogEntry": { + "@id": "https://api.nuget.org/v3/catalog0/data/2022.12.08.16.43.03/Foo.NET.4.2.json", + "@type": "PackageDetails", + "listed": false, + "version": "4.2" + } + } + ] +} diff --git a/cmd/internal/nuget/testdata/package_spec.xml b/cmd/internal/nuget/testdata/package_spec.xml new file mode 100644 index 00000000000..1ef5fa4c984 --- /dev/null +++ b/cmd/internal/nuget/testdata/package_spec.xml @@ -0,0 +1,9 @@ + + + Foo + 4.0.1 + Foo.NET + + foo + + \ No newline at end of file diff --git a/cmd/internal/nuget/testdata/package_spec_error.xml b/cmd/internal/nuget/testdata/package_spec_error.xml new file mode 100644 index 00000000000..4de5c445d85 --- /dev/null +++ b/cmd/internal/nuget/testdata/package_spec_error.xml @@ -0,0 +1,7 @@ + + + Foo + 4.0.1 + Foo.NET + + \ No newline at end of file diff --git a/cmd/internal/nuget/testdata/package_spec_four_digit_version.xml b/cmd/internal/nuget/testdata/package_spec_four_digit_version.xml new file mode 100644 index 00000000000..d93c743d93c --- /dev/null +++ b/cmd/internal/nuget/testdata/package_spec_four_digit_version.xml @@ -0,0 +1,9 @@ + + + Foo + 1.60.0.2981+metadat + Foo.NET + + foo + + \ No newline at end of file diff --git a/cmd/internal/nuget/testdata/package_spec_git_ending.xml b/cmd/internal/nuget/testdata/package_spec_git_ending.xml new file mode 100644 index 00000000000..006c4d7467a --- /dev/null +++ b/cmd/internal/nuget/testdata/package_spec_git_ending.xml @@ -0,0 +1,9 @@ + + + Foo + 4.0.1 + Foo.NET + + foo + + \ No newline at end of file diff --git a/cmd/internal/nuget/testdata/package_spec_project_url.xml b/cmd/internal/nuget/testdata/package_spec_project_url.xml new file mode 100644 index 00000000000..9124d683818 --- /dev/null +++ b/cmd/internal/nuget/testdata/package_spec_project_url.xml @@ -0,0 +1,8 @@ + + + Foo + 4.0.1 + Foo.NET + https://github.com/foo/foo.net + + \ No newline at end of file diff --git a/cmd/internal/nuget/testdata/package_spec_project_url_git_ending.xml b/cmd/internal/nuget/testdata/package_spec_project_url_git_ending.xml new file mode 100644 index 00000000000..ba4c5129615 --- /dev/null +++ b/cmd/internal/nuget/testdata/package_spec_project_url_git_ending.xml @@ -0,0 +1,8 @@ + + + Foo + 4.0.1 + Foo.NET + https://github.com/foo/foo.net.git + + \ No newline at end of file diff --git a/cmd/internal/nuget/testdata/package_spec_project_url_gitlab.xml b/cmd/internal/nuget/testdata/package_spec_project_url_gitlab.xml new file mode 100644 index 00000000000..94643d502b2 --- /dev/null +++ b/cmd/internal/nuget/testdata/package_spec_project_url_gitlab.xml @@ -0,0 +1,8 @@ + + + Foo + 4.0.1 + Foo.NET + https://gitlab.com/foo/foo.net + + \ No newline at end of file diff --git a/cmd/internal/nuget/testdata/package_spec_project_url_not_supported.xml b/cmd/internal/nuget/testdata/package_spec_project_url_not_supported.xml new file mode 100644 index 00000000000..ba94b9a86df --- /dev/null +++ b/cmd/internal/nuget/testdata/package_spec_project_url_not_supported.xml @@ -0,0 +1,8 @@ + + + Foo + 4.0.1 + Foo.NET + https://myserver.com/foo/foo.net + + \ No newline at end of file diff --git a/cmd/internal/nuget/testdata/package_spec_trailing_slash.xml b/cmd/internal/nuget/testdata/package_spec_trailing_slash.xml new file mode 100644 index 00000000000..61b3f4cc781 --- /dev/null +++ b/cmd/internal/nuget/testdata/package_spec_trailing_slash.xml @@ -0,0 +1,9 @@ + + + Foo + 4.0.1 + Foo.NET + + foo + + \ No newline at end of file diff --git a/cmd/packagemanager_client.go b/cmd/internal/packagemanager/client.go similarity index 58% rename from cmd/packagemanager_client.go rename to cmd/internal/packagemanager/client.go index f453d22cfd0..5e3ee1f00df 100644 --- a/cmd/packagemanager_client.go +++ b/cmd/internal/packagemanager/client.go @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -package cmd +// Package packagemanager implements a packagemanager client +package packagemanager import ( "fmt" @@ -20,18 +21,30 @@ import ( "time" ) -type packageManagerClient interface { +type Client interface { Get(URI string, packagename string) (*http.Response, error) + + GetURI(URI string) (*http.Response, error) } -type packageManager struct{} +type PackageManagerClient struct{} + +// nolint: noctx +func (c *PackageManagerClient) Get(url, packageName string) (*http.Response, error) { + return c.getRemoteURL(fmt.Sprintf(url, packageName)) +} + +// nolint: noctx +func (c *PackageManagerClient) GetURI(url string) (*http.Response, error) { + return c.getRemoteURL(url) +} // nolint: noctx -func (c *packageManager) Get(url, packageName string) (*http.Response, error) { +func (c *PackageManagerClient) getRemoteURL(url string) (*http.Response, error) { const timeout = 10 client := &http.Client{ Timeout: timeout * time.Second, } //nolint - return client.Get(fmt.Sprintf(url, packageName)) + return client.Get(url) } diff --git a/cmd/internal/packagemanager/client_test.go b/cmd/internal/packagemanager/client_test.go new file mode 100644 index 00000000000..97d9c915130 --- /dev/null +++ b/cmd/internal/packagemanager/client_test.go @@ -0,0 +1,131 @@ +// Copyright 2020 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 packagemanager implements a packagemanager client +package packagemanager + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" +) + +func Test_GetURI_calls_client_get_with_input(t *testing.T) { + t.Parallel() + type args struct { + inputURL string + } + tests := []struct { + name string + args args + wantURL string + wantResponse string + }{ + { + name: "GetURI_input_is_the_same_as_get_uri", + + args: args{ + inputURL: "test", + }, + wantURL: "/test", + wantResponse: "test", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != tt.wantURL { + t.Errorf("Expected to request '%s', got: %s", tt.wantURL, r.URL.Path) + } + // nolint + w.WriteHeader(http.StatusOK) + // nolint + w.Write([]byte(tt.wantResponse)) + })) + defer server.Close() + client := PackageManagerClient{} + got, err := client.GetURI(server.URL + "/" + tt.args.inputURL) + if err != nil { + t.Errorf("Test_GetURI_calls_client_get_with_input() error in Get= %v", err) + return + } + body, err := io.ReadAll(got.Body) + if err != nil { + t.Errorf("Test_GetURI_calls_client_get_with_input() error in ReadAll= %v", err) + return + } + if string(body) != tt.wantResponse { + t.Errorf("GetURI() = %v, want %v", got, tt.wantResponse) + } + }) + } +} + +func Test_Get_calls_client_get_with_input(t *testing.T) { + t.Parallel() + type args struct { + inputURL string + packageName string + } + tests := []struct { + name string + args args + wantURL string + wantResponse string + }{ + { + name: "Get_input_adds_package_name_for_get_uri", + + args: args{ + inputURL: "test-%s-test", + packageName: "test_package", + }, + wantURL: "/test-test_package-test", + wantResponse: "test", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != tt.wantURL { + t.Errorf("Expected to request '%s', got: %s", tt.wantURL, r.URL.Path) + } + // nolint + w.WriteHeader(http.StatusOK) + // nolint + w.Write([]byte(tt.wantResponse)) + })) + defer server.Close() + client := PackageManagerClient{} + got, err := client.Get(server.URL+"/"+tt.args.inputURL, tt.args.packageName) + if err != nil { + t.Errorf("Test_Get_calls_client_get_with_input() error in Get = %v", err) + return + } + body, err := io.ReadAll(got.Body) + if err != nil { + t.Errorf("Test_Get_calls_client_get_with_input() error in ReadAll = %v", err) + return + } + if string(body) != tt.wantResponse { + t.Errorf("GetURI() = %v, want %v", got, tt.wantResponse) + } + }) + } +} diff --git a/cmd/internal/packagemanager/packagemanager_mockclient.go b/cmd/internal/packagemanager/packagemanager_mockclient.go new file mode 100644 index 00000000000..9c7234661be --- /dev/null +++ b/cmd/internal/packagemanager/packagemanager_mockclient.go @@ -0,0 +1,80 @@ +// Copyright 2021 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. +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: cmd/internal/packagemanager/client.go + +// Package packagemanager is a generated GoMock package. +package packagemanager + +import ( + http "net/http" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockClient is a mock of Client interface. +type MockClient struct { + ctrl *gomock.Controller + recorder *MockClientMockRecorder +} + +// MockClientMockRecorder is the mock recorder for MockClient. +type MockClientMockRecorder struct { + mock *MockClient +} + +// NewMockClient creates a new mock instance. +func NewMockClient(ctrl *gomock.Controller) *MockClient { + mock := &MockClient{ctrl: ctrl} + mock.recorder = &MockClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClient) EXPECT() *MockClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockClient) Get(URI, packagename string) (*http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", URI, packagename) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockClientMockRecorder) Get(URI, packagename interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockClient)(nil).Get), URI, packagename) +} + +// GetURI mocks base method. +func (m *MockClient) GetURI(URI string) (*http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetURI", URI) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetURI indicates an expected call of GetURI. +func (mr *MockClientMockRecorder) GetURI(URI interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetURI", reflect.TypeOf((*MockClient)(nil).GetURI), URI) +} diff --git a/cmd/package_managers.go b/cmd/package_managers.go index a18ecbd25a8..67b4ab888bf 100644 --- a/cmd/package_managers.go +++ b/cmd/package_managers.go @@ -19,6 +19,8 @@ import ( "encoding/json" "fmt" + ngt "github.com/ossf/scorecard/v4/cmd/internal/nuget" + pmc "github.com/ossf/scorecard/v4/cmd/internal/packagemanager" sce "github.com/ossf/scorecard/v4/errors" ) @@ -27,8 +29,8 @@ type packageMangerResponse struct { exists bool } -func fetchGitRepositoryFromPackageManagers(npm, pypi, rubygems string, - manager packageManagerClient, +func fetchGitRepositoryFromPackageManagers(npm, pypi, rubygems, nuget string, + manager pmc.Client, ) (packageMangerResponse, error) { if npm != "" { gitRepo, err := fetchGitRepositoryFromNPM(npm, manager) @@ -51,6 +53,14 @@ func fetchGitRepositoryFromPackageManagers(npm, pypi, rubygems string, associatedRepo: gitRepo, }, err } + if nuget != "" { + nugetClient := ngt.NugetClient{Manager: manager} + gitRepo, err := fetchGitRepositoryFromNuget(nuget, nugetClient) + return packageMangerResponse{ + exists: true, + associatedRepo: gitRepo, + }, err + } return packageMangerResponse{}, nil } @@ -78,7 +88,7 @@ type rubyGemsSearchResults struct { } // Gets the GitHub repository URL for the npm package. -func fetchGitRepositoryFromNPM(packageName string, packageManager packageManagerClient) (string, error) { +func fetchGitRepositoryFromNPM(packageName string, packageManager pmc.Client) (string, error) { npmSearchURL := "https://registry.npmjs.org/-/v1/search?text=%s&size=1" resp, err := packageManager.Get(npmSearchURL, packageName) if err != nil { @@ -99,7 +109,7 @@ func fetchGitRepositoryFromNPM(packageName string, packageManager packageManager } // Gets the GitHub repository URL for the pypi package. -func fetchGitRepositoryFromPYPI(packageName string, manager packageManagerClient) (string, error) { +func fetchGitRepositoryFromPYPI(packageName string, manager pmc.Client) (string, error) { pypiSearchURL := "https://pypi.org/pypi/%s/json" resp, err := manager.Get(pypiSearchURL, packageName) if err != nil { @@ -120,7 +130,7 @@ func fetchGitRepositoryFromPYPI(packageName string, manager packageManagerClient } // Gets the GitHub repository URL for the rubygems package. -func fetchGitRepositoryFromRubyGems(packageName string, manager packageManagerClient) (string, error) { +func fetchGitRepositoryFromRubyGems(packageName string, manager pmc.Client) (string, error) { rubyGemsSearchURL := "https://rubygems.org/api/v1/gems/%s.json" resp, err := manager.Get(rubyGemsSearchURL, packageName) if err != nil { @@ -138,3 +148,13 @@ func fetchGitRepositoryFromRubyGems(packageName string, manager packageManagerCl } return v.SourceCodeURI, nil } + +// Gets the GitHub repository URL for the nuget package. +func fetchGitRepositoryFromNuget(packageName string, nugetClient ngt.Client) (string, error) { + repositoryURI, err := nugetClient.GitRepositoryByPackageName(packageName) + if err != nil { + return "", sce.WithMessage(sce.ErrScorecardInternal, + fmt.Sprintf("could not find source repo for nuget package: %v", err)) + } + return repositoryURI, nil +} diff --git a/cmd/package_managers_test.go b/cmd/package_managers_test.go index b4e398109f4..fe5ff0bf493 100644 --- a/cmd/package_managers_test.go +++ b/cmd/package_managers_test.go @@ -23,6 +23,9 @@ import ( "testing" "github.com/golang/mock/gomock" + + ngt "github.com/ossf/scorecard/v4/cmd/internal/nuget" + pmc "github.com/ossf/scorecard/v4/cmd/internal/packagemanager" ) func Test_fetchGitRepositoryFromNPM(t *testing.T) { @@ -133,7 +136,7 @@ func Test_fetchGitRepositoryFromNPM(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - p := NewMockpackageManagerClient(ctrl) + p := pmc.NewMockClient(ctrl) p.EXPECT().Get(gomock.Any(), tt.args.packageName). DoAndReturn(func(url, packageName string) (*http.Response, error) { if tt.wantErr && tt.args.result == "" { @@ -413,7 +416,7 @@ func Test_fetchGitRepositoryFromPYPI(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - p := NewMockpackageManagerClient(ctrl) + p := pmc.NewMockClient(ctrl) p.EXPECT().Get(gomock.Any(), tt.args.packageName). DoAndReturn(func(url, packageName string) (*http.Response, error) { if tt.wantErr && tt.args.result == "" { @@ -682,7 +685,7 @@ func Test_fetchGitRepositoryFromRubyGems(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) - p := NewMockpackageManagerClient(ctrl) + p := pmc.NewMockClient(ctrl) p.EXPECT().Get(gomock.Any(), tt.args.packageName). DoAndReturn(func(url, packageName string) (*http.Response, error) { if tt.wantErr && tt.args.result == "" { @@ -706,3 +709,65 @@ func Test_fetchGitRepositoryFromRubyGems(t *testing.T) { }) } } + +func Test_fetchGitRepositoryFromNuget(t *testing.T) { + t.Parallel() + type args struct { + packageName string + result string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "Return repository from nuget client", + //nolint + args: args{ + packageName: "nuget-package", + //nolint + result: "nuget", + }, + want: "nuget", + wantErr: false, + }, + { + name: "Error from nuget client", + //nolint + args: args{ + packageName: "nuget-package", + //nolint + result: "", + }, + want: "", + wantErr: true, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + n := ngt.NewMockClient(ctrl) + n.EXPECT().GitRepositoryByPackageName(tt.args.packageName). + DoAndReturn(func(packageName string) (string, error) { + if tt.wantErr && tt.args.result == "" { + //nolint + return "", errors.New("error") + } + + return tt.args.result, nil + }).AnyTimes() + got, err := fetchGitRepositoryFromNuget(tt.args.packageName, n) + if (err != nil) != tt.wantErr { + t.Errorf("fetchGitRepositoryFromNuget() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("fetchGitRepositoryFromNuget() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/packagemanager_mockclient.go b/cmd/packagemanager_mockclient.go deleted file mode 100644 index 40cc49d369f..00000000000 --- a/cmd/packagemanager_mockclient.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2021 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. -// - -// Code generated by MockGen. DO NOT EDIT. -// Source: cmd/packagemanager_client.go - -// Package cmd is a generated GoMock package. -package cmd - -import ( - http "net/http" - reflect "reflect" - - gomock "github.com/golang/mock/gomock" -) - -// MockpackageManagerClient is a mock of packageManagerClient interface. -type MockpackageManagerClient struct { - ctrl *gomock.Controller - recorder *MockpackageManagerClientMockRecorder -} - -// MockpackageManagerClientMockRecorder is the mock recorder for MockpackageManagerClient. -type MockpackageManagerClientMockRecorder struct { - mock *MockpackageManagerClient -} - -// NewMockpackageManagerClient creates a new mock instance. -func NewMockpackageManagerClient(ctrl *gomock.Controller) *MockpackageManagerClient { - mock := &MockpackageManagerClient{ctrl: ctrl} - mock.recorder = &MockpackageManagerClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockpackageManagerClient) EXPECT() *MockpackageManagerClientMockRecorder { - return m.recorder -} - -// Get mocks base method. -func (m *MockpackageManagerClient) Get(URI, packagename string) (*http.Response, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Get", URI, packagename) - ret0, _ := ret[0].(*http.Response) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Get indicates an expected call of Get. -func (mr *MockpackageManagerClientMockRecorder) Get(URI, packagename interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockpackageManagerClient)(nil).Get), URI, packagename) -} diff --git a/cmd/root.go b/cmd/root.go index da4cd0610d0..4dccf166d64 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -27,6 +27,7 @@ import ( "github.com/ossf/scorecard/v4/checker" "github.com/ossf/scorecard/v4/clients" + pmc "github.com/ossf/scorecard/v4/cmd/internal/packagemanager" docs "github.com/ossf/scorecard/v4/docs/checks" sce "github.com/ossf/scorecard/v4/errors" sclog "github.com/ossf/scorecard/v4/log" @@ -37,7 +38,7 @@ import ( const ( scorecardLong = "A program that shows the OpenSSF scorecard for an open source software." - scorecardUse = `./scorecard (--repo= | --local= | --{npm,pypi,rubygems}=) + scorecardUse = `./scorecard (--repo= | --local= | --{npm,pypi,rubygems,nuget}=) [--checks=check1,...] [--show-details]` scorecardShort = "OpenSSF Scorecard" ) @@ -72,9 +73,9 @@ func New(o *options.Options) *cobra.Command { // rootCmd runs scorecard checks given a set of arguments. func rootCmd(o *options.Options) error { - p := &packageManager{} + p := &pmc.PackageManagerClient{} // Set `repo` from package managers. - pkgResp, err := fetchGitRepositoryFromPackageManagers(o.NPM, o.PyPI, o.RubyGems, p) + pkgResp, err := fetchGitRepositoryFromPackageManagers(o.NPM, o.PyPI, o.RubyGems, o.Nuget, p) if err != nil { return fmt.Errorf("fetchGitRepositoryFromPackageManagers: %w", err) } diff --git a/options/flags.go b/options/flags.go index 66a48c2b85a..1652c9fd18a 100644 --- a/options/flags.go +++ b/options/flags.go @@ -45,6 +45,9 @@ const ( // FlagRubyGems is the flag name for specifying a RubyGems repository. FlagRubyGems = "rubygems" + // FlagNuget is the flag name for specifying a Nuget repository. + FlagNuget = "nuget" + // FlagMetadata is the flag name for specifying metadata for the project. FlagMetadata = "metadata" @@ -120,6 +123,13 @@ func (o *Options) AddFlags(cmd *cobra.Command) { "rubygems package to check, given that the rubygems package has a GitHub repository", ) + cmd.Flags().StringVar( + &o.Nuget, + FlagNuget, + o.Nuget, + "nuget package to check, given that the nuget package has a GitHub repository", + ) + cmd.Flags().StringSliceVar( &o.Metadata, FlagMetadata, diff --git a/options/options.go b/options/options.go index 164c356b821..5be1fda1feb 100644 --- a/options/options.go +++ b/options/options.go @@ -37,6 +37,7 @@ type Options struct { NPM string PyPI string RubyGems string + Nuget string PolicyFile string // TODO(action): Add logic for writing results to file ResultsFile string @@ -113,7 +114,7 @@ var ( 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", + "exactly one of `repo`, `npm`, `pypi`, `rubygems`, `nuget` or `local` must be set", ) errSARIFNotSupported = errors.New("SARIF format is not supported yet") errValidate = errors.New("some options could not be validated") @@ -124,11 +125,12 @@ var ( func (o *Options) Validate() error { var errs []error - // Validate exactly one of `--repo`, `--npm`, `--pypi`, `--rubygems`, `--local` is enabled. + // Validate exactly one of `--repo`, `--npm`, `--pypi`, `--rubygems`, `--nuget`, `--local` is enabled. if boolSum(o.Repo != "", o.NPM != "", o.PyPI != "", o.RubyGems != "", + o.Nuget != "", o.Local != "") != 1 { errs = append( errs, diff --git a/options/options_test.go b/options/options_test.go index b69d5c35e07..8098e8ebc90 100644 --- a/options/options_test.go +++ b/options/options_test.go @@ -21,7 +21,7 @@ import ( ) // Cannot run parallel tests because of the ENV variables. -//nolint +// nolint func TestOptions_Validate(t *testing.T) { type fields struct { Repo string @@ -32,6 +32,7 @@ func TestOptions_Validate(t *testing.T) { NPM string PyPI string RubyGems string + Nuget string PolicyFile string ResultsFile string ChecksToRun []string @@ -99,6 +100,7 @@ func TestOptions_Validate(t *testing.T) { NPM: tt.fields.NPM, PyPI: tt.fields.PyPI, RubyGems: tt.fields.RubyGems, + Nuget: tt.fields.Nuget, PolicyFile: tt.fields.PolicyFile, ResultsFile: tt.fields.ResultsFile, ChecksToRun: tt.fields.ChecksToRun,