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,