diff --git a/README.md b/README.md index bb7e730..abd7c17 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Available Commands: deps Get info about a package's dependencies graph Generate a Graphviz compatible dependencies graph help Help about any command - info Get info about a package or a specific version of that + package Get info about a package or a specific version of that packages Get info about a project's package versions (GitHub, GitLab, or BitBucket) project Get info about a project (GitHub, GitLab, or BitBucket) query Get info about multiple package versions using a query @@ -88,7 +88,7 @@ For more information [read the API documentation](https://docs.deps.dev/api/v3). Get information about a package, including a list of its available versions, with the default version marked if known. ```console -depsdev info npm @colors/colors +depsdev package npm @colors/colors ```
@@ -96,7 +96,7 @@ depsdev info npm @colors/colors Get information about a specific package version including its licenses and any security advisories known to affect it. ```console -depsdev info npm @colors/colors 1.5.0 +depsdev package npm @colors/colors 1.5.0 ```
diff --git a/cmd/info.go b/cmd/package.go similarity index 88% rename from cmd/info.go rename to cmd/package.go index 6f2ffe4..7d28e1a 100644 --- a/cmd/info.go +++ b/cmd/package.go @@ -23,9 +23,9 @@ import ( "github.com/spf13/cobra" ) -// infoCmd represents the info command when called with info subcommand. -var infoCmd = &cobra.Command{ - Use: "info package-manager package-name [version]", +// packageCmd represents the package command when called with package subcommand. +var packageCmd = &cobra.Command{ + Use: "package package-manager package-name [version]", Short: "Get info about a package or a specific version of that", Long: `Get information about a package, including a list of its available versions, with the default version marked if known. @@ -56,7 +56,7 @@ including its licenses and any security advisories known to affect it.`, fmt.Println(vJSON) } else { - p, err := api.GetInfo(args[0], args[1]) + p, err := api.GetPackage(args[0], args[1]) if err != nil { log.Fatal(err) } diff --git a/cmd/packages.go b/cmd/packages.go index 5a98aed..48c1080 100644 --- a/cmd/packages.go +++ b/cmd/packages.go @@ -36,7 +36,7 @@ var packagesCmd = &cobra.Command{ return nil }, Run: func(cmd *cobra.Command, args []string) { - v, err := api.GetPackageVersions(args[0]) + v, err := api.GetProjectPackageVersions(args[0]) if err != nil { log.Fatal(err) } diff --git a/cmd/root.go b/cmd/root.go index 69c9769..9822bb0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -49,7 +49,7 @@ func Execute() { } func init() { - rootCmd.AddCommand(infoCmd) + rootCmd.AddCommand(packageCmd) rootCmd.AddCommand(depsCmd) rootCmd.AddCommand(advisoryCmd) rootCmd.AddCommand(projectCmd) diff --git a/pkg/depsdev/definitions/deps.go b/pkg/depsdev/definitions/deps.go index 723db0c..8996450 100644 --- a/pkg/depsdev/definitions/deps.go +++ b/pkg/depsdev/definitions/deps.go @@ -32,3 +32,9 @@ type Edge struct { ToNode int `json:"toNode,omitempty"` Requirement string `json:"requirement,omitempty"` } + +type Dependent struct { + DependentCount int `json:"dependentCount"` + DirectDependentCount int `json:"directDependentCount"` + IndirectDependentCount int `json:"indirectDependentCount"` +} diff --git a/pkg/depsdev/definitions/requirements.go b/pkg/depsdev/definitions/requirements.go index 6771f99..1e028df 100644 --- a/pkg/depsdev/definitions/requirements.go +++ b/pkg/depsdev/definitions/requirements.go @@ -114,9 +114,3 @@ type File struct { Exists string `json:"exists,omitempty"` Missing string `json:"missing,omitempty"` } - -type Dependent struct { - DependentCount int `json:"dependentCount"` - DirectDependentCount int `json:"directDependentCount"` - IndirectDependentCount int `json:"indirectDependentCount"` -} diff --git a/pkg/depsdev/v3/api.go b/pkg/depsdev/v3/api.go index b67ab44..a9b2305 100644 --- a/pkg/depsdev/v3/api.go +++ b/pkg/depsdev/v3/api.go @@ -34,10 +34,9 @@ func NewV3API() *APIv3 { } } -// GetInfo returns information about a package, -// including a list of its available versions, +// GetPackage returns information about a package, including a list of its available versions, // with the default version marked if known. -func (a *APIv3) GetInfo(packageManager, packageName string) (def.Package, error) { +func (a *APIv3) GetPackage(packageManager, packageName string) (def.Package, error) { if !input.IsValidPackageManager(packageManager, input.AllValidPackageManagers) { return def.Package{}, input.ErrInvalidPackageManager } @@ -77,8 +76,12 @@ func getVersion(c *client.Client, packageManager, packageName, version string) ( return response, nil } -// GetDependencies returns information about dependencies for a specific version of a package -// for a specific package manager. +// GetDependencies returns a resolved dependency graph for the given package version. +// Dependencies are currently available for npm, Cargo, Maven and PyPI. +// Dependencies are the resolution of the requirements (dependency constraints) specified by a version. +// The dependency graph should be similar to one produced by installing the package version on a generic 64-bit Linux system, +// with no other dependencies present. The precise meaning of this varies from system to system. +// Example: npm react 18.2.0. func (a *APIv3) GetDependencies(packageManager, packageName, version string) (def.Dependencies, error) { return getDependencies(a.client, packageManager, packageName, version) } @@ -94,7 +97,8 @@ func getDependencies(c *client.Client, packageManager, packageName, version stri return response, nil } -// GetProject returns information about a project (hosted on GitHub, GitLab or BitBucket). +// GetProject returns information about projects hosted by GitHub, GitLab, or BitBucket, when known to us. +// Example: github.com/facebook/react. func (a *APIv3) GetProject(projectName string) (def.Project, error) { return getProject(a.client, projectName) } @@ -110,7 +114,8 @@ func getProject(c *client.Client, projectName string) (def.Project, error) { return response, nil } -// GetAdvisory returns information about an advisory. +// GetAdvisory returns information about security advisories hosted by OSV. +// Example: GHSA-2qrg-x229-3v8q. func (a *APIv3) GetAdvisory(advisory string) (def.Advisory, error) { return getAdvisory(a.client, advisory) } @@ -126,7 +131,15 @@ func getAdvisory(c *client.Client, advisory string) (def.Advisory, error) { return response, nil } -// Query returns the result of the inputted query. +// Query returns information about multiple package versions, which can be specified by name, content hash, or both. +// If a hash was specified in the request, it returns the artifacts that matched the hash. +// Querying by content hash is currently supported for npm, Cargo, Maven, NuGet, PyPI and RubyGems. +// It is typical for hash queries to return many results; hashes are matched against multiple release artifacts +// (such as JAR files) that comprise package versions, and any given artifact may appear in several package versions. +// Examples: +// hash.type=SHA1&hash.value=ulXBPXrC%2FUTfnMgHRFVxmjPzdbk%3D +// versionKey.system=NPM&versionKey.name=react&versionKey.version=18.2.0 +// End of examples. func (a *APIv3) Query(query string) (def.Results, error) { return getQuery(a.client, query) } @@ -143,7 +156,9 @@ func getQuery(c *client.Client, query string) (def.Results, error) { } // GetRequirements returns the requirements for a given version in a system-specific format. -// Requirements are currently available for Maven, npm and NuGet. +// Requirements are currently available for Maven, npm, NuGet and RubyGems. +// Requirements are the dependency constraints specified by the version. +// Example: nuget castle.core 5.1.1. func (a *APIv3) GetRequirements(packageManager, packageName, version string) (def.Requirements, error) { var response def.Requirements @@ -155,10 +170,11 @@ func (a *APIv3) GetRequirements(packageManager, packageName, version string) (de return response, nil } -// GetPackageVersions returns the package versions which attest to being created from the specified -// source code repository (hosted on GitHub, GitLab or BitBucket). +// GetProjectPackageVersions returns known mappings between the requested project and package versions. // At most 1500 package versions are returned. -func (a *APIv3) GetPackageVersions(projectName string) (def.PackageVersions, error) { +// Mappings which were derived from attestations are served first. +// Example: github.com/facebook/react. +func (a *APIv3) GetProjectPackageVersions(projectName string) (def.PackageVersions, error) { var response def.PackageVersions var path = fmt.Sprintf(GetProjectPackageVersionsPath, url.PathEscape(projectName)) diff --git a/pkg/depsdev/v3/api_internal_test.go b/pkg/depsdev/v3/api_internal_test.go index b2105c7..53b92d6 100644 --- a/pkg/depsdev/v3/api_internal_test.go +++ b/pkg/depsdev/v3/api_internal_test.go @@ -51,7 +51,7 @@ func BenchmarkGetInfo(b *testing.B) { ) for i := 0; i < b.N; i++ { - info, err = api.GetInfo("npm", "react") + info, err = api.GetPackage("npm", "react") require.NoError(b, err) } diff --git a/pkg/depsdev/v3/api_test.go b/pkg/depsdev/v3/api_test.go index d8acf7b..785d160 100644 --- a/pkg/depsdev/v3/api_test.go +++ b/pkg/depsdev/v3/api_test.go @@ -29,18 +29,7 @@ var ( api = depsdev.NewV3API() ) -func TestGetProject(t *testing.T) { - t.Run("GetInfo npm defangjs", func(t *testing.T) { - got, err := api.GetProject("github.com/edoardottt/defangjs") - require.Nil(t, err) - - // no checking of the actual value because they can change over time - // we just ensure the call to the API and unmarshaling of the response works properly - assert.NotEmpty(t, got) - }) -} - -func TestGetInfo(t *testing.T) { +func TestGetPackage(t *testing.T) { result := `{ "packageKey": { "system": "NPM", @@ -115,8 +104,8 @@ func TestGetInfo(t *testing.T) { ] }` - t.Run("GetInfo npm defangjs", func(t *testing.T) { - got, err := api.GetInfo("npm", "defangjs") + t.Run("GetPackage npm defangjs", func(t *testing.T) { + got, err := api.GetPackage("npm", "defangjs") require.Nil(t, err) var d def.Package @@ -197,6 +186,176 @@ func TestGetVersion(t *testing.T) { }) } +func TestGetRequirements(t *testing.T) { + result := `{ + "npm": { + "dependencies": { + "dependencies": [ + { + "name": "fast-redact", + "requirement": "^3.0.0" + }, + { + "name": "fast-safe-stringify", + "requirement": "^2.0.8" + }, + { + "name": "flatstr", + "requirement": "^1.0.12" + }, + { + "name": "pino-std-serializers", + "requirement": "^3.1.0" + }, + { + "name": "process-warning", + "requirement": "^1.0.0" + }, + { + "name": "quick-format-unescaped", + "requirement": "^4.0.3" + }, + { + "name": "sonic-boom", + "requirement": "^1.0.2" + } + ], + "devDependencies": [ + { + "name": "airtap", + "requirement": "4.0.3" + }, + { + "name": "benchmark", + "requirement": "^2.1.4" + }, + { + "name": "bole", + "requirement": "^4.0.0" + }, + { + "name": "bunyan", + "requirement": "^1.8.14" + }, + { + "name": "docsify-cli", + "requirement": "^4.4.1" + }, + { + "name": "eslint", + "requirement": "^7.17.0" + }, + { + "name": "eslint-config-standard", + "requirement": "^16.0.2" + }, + { + "name": "eslint-plugin-import", + "requirement": "^2.22.1" + }, + { + "name": "eslint-plugin-node", + "requirement": "^11.1.0" + }, + { + "name": "eslint-plugin-promise", + "requirement": "^5.1.0" + }, + { + "name": "execa", + "requirement": "^5.0.0" + }, + { + "name": "fastbench", + "requirement": "^1.0.1" + }, + { + "name": "flush-write-stream", + "requirement": "^2.0.0" + }, + { + "name": "import-fresh", + "requirement": "^3.2.1" + }, + { + "name": "log", + "requirement": "^6.0.0" + }, + { + "name": "loglevel", + "requirement": "^1.6.7" + }, + { + "name": "pino-pretty", + "requirement": "^5.0.0" + }, + { + "name": "pre-commit", + "requirement": "^1.2.2" + }, + { + "name": "proxyquire", + "requirement": "^2.1.3" + }, + { + "name": "pump", + "requirement": "^3.0.0" + }, + { + "name": "semver", + "requirement": "^7.0.0" + }, + { + "name": "split2", + "requirement": "^3.1.1" + }, + { + "name": "steed", + "requirement": "^1.1.3" + }, + { + "name": "strip-ansi", + "requirement": "^6.0.0" + }, + { + "name": "tap", + "requirement": "^15.0.1" + }, + { + "name": "tape", + "requirement": "^5.0.0" + }, + { + "name": "through2", + "requirement": "^4.0.0" + }, + { + "name": "winston", + "requirement": "^3.3.3" + } + ], + "optionalDependencies": [], + "peerDependencies": [], + "bundleDependencies": [] + }, + "Bundled": [] + } + }` + + t.Run("GetRequirements npm pino 6.14.0", func(t *testing.T) { + got, err := api.GetRequirements("npm", "pino", "6.14.0") + require.Nil(t, err) + + var r def.Requirements + + if err := json.Unmarshal([]byte(result), &r); err != nil { + log.Fatal(err) + } + + require.Equal(t, r, got) + }) +} + func TestGetDependencies(t *testing.T) { result := `{ "nodes": [ @@ -338,6 +497,28 @@ func TestGetDependencies(t *testing.T) { }) } +func TestGetProject(t *testing.T) { + t.Run("GetProject npm defangjs", func(t *testing.T) { + got, err := api.GetProject("github.com/edoardottt/defangjs") + require.Nil(t, err) + + // no checking of the actual value because they can change over time + // we just ensure the call to the API and unmarshaling of the response works properly + assert.NotEmpty(t, got) + }) +} + +func TestGetProjectPackageVersions(t *testing.T) { + t.Run("GetProjectPackageVersions npm defangjs", func(t *testing.T) { + got, err := api.GetProjectPackageVersions("github.com/edoardottt/defangjs") + require.Nil(t, err) + + // no checking of the actual value because they can change over time + // we just ensure the call to the API and unmarshaling of the response works properly + assert.NotEmpty(t, got) + }) +} + func TestGetAdvisory(t *testing.T) { result := `{ "advisoryKey": { @@ -366,172 +547,76 @@ func TestGetAdvisory(t *testing.T) { }) } -func TestGetRequirements(t *testing.T) { +func TestQuery(t *testing.T) { result := `{ - "npm": { - "dependencies": { - "dependencies": [ - { - "name": "fast-redact", - "requirement": "^3.0.0" - }, - { - "name": "fast-safe-stringify", - "requirement": "^2.0.8" - }, - { - "name": "flatstr", - "requirement": "^1.0.12" - }, - { - "name": "pino-std-serializers", - "requirement": "^3.1.0" - }, - { - "name": "process-warning", - "requirement": "^1.0.0" - }, - { - "name": "quick-format-unescaped", - "requirement": "^4.0.3" - }, - { - "name": "sonic-boom", - "requirement": "^1.0.2" - } - ], - "devDependencies": [ - { - "name": "airtap", - "requirement": "4.0.3" - }, - { - "name": "benchmark", - "requirement": "^2.1.4" - }, - { - "name": "bole", - "requirement": "^4.0.0" - }, - { - "name": "bunyan", - "requirement": "^1.8.14" - }, - { - "name": "docsify-cli", - "requirement": "^4.4.1" - }, - { - "name": "eslint", - "requirement": "^7.17.0" - }, - { - "name": "eslint-config-standard", - "requirement": "^16.0.2" - }, - { - "name": "eslint-plugin-import", - "requirement": "^2.22.1" - }, - { - "name": "eslint-plugin-node", - "requirement": "^11.1.0" - }, - { - "name": "eslint-plugin-promise", - "requirement": "^5.1.0" - }, - { - "name": "execa", - "requirement": "^5.0.0" - }, - { - "name": "fastbench", - "requirement": "^1.0.1" - }, - { - "name": "flush-write-stream", - "requirement": "^2.0.0" - }, - { - "name": "import-fresh", - "requirement": "^3.2.1" - }, - { - "name": "log", - "requirement": "^6.0.0" - }, - { - "name": "loglevel", - "requirement": "^1.6.7" - }, - { - "name": "pino-pretty", - "requirement": "^5.0.0" - }, - { - "name": "pre-commit", - "requirement": "^1.2.2" - }, - { - "name": "proxyquire", - "requirement": "^2.1.3" - }, - { - "name": "pump", - "requirement": "^3.0.0" - }, - { - "name": "semver", - "requirement": "^7.0.0" - }, - { - "name": "split2", - "requirement": "^3.1.1" - }, - { - "name": "steed", - "requirement": "^1.1.3" - }, - { - "name": "strip-ansi", - "requirement": "^6.0.0" - }, - { - "name": "tap", - "requirement": "^15.0.1" - }, - { - "name": "tape", - "requirement": "^5.0.0" - }, - { - "name": "through2", - "requirement": "^4.0.0" - }, - { - "name": "winston", - "requirement": "^3.3.3" - } - ], - "optionalDependencies": [], - "peerDependencies": [], - "bundleDependencies": [] - }, - "Bundled": [] - } - }` + "results": [ + { + "version": { + "versionKey": { + "system": "NPM", + "name": "defangjs", + "version": "1.0.7" + }, + "publishedAt": "2023-05-16T09:48:31Z", + "isDefault": true, + "licenses": [ + "GPL-3.0" + ], + "advisoryKeys": [], + "links": [ + { + "label": "HOMEPAGE", + "url": "https://github.com/edoardottt/defangjs#readme" + }, + { + "label": "ISSUE_TRACKER", + "url": "https://github.com/edoardottt/defangjs/issues" + }, + { + "label": "ORIGIN", + "url": "https://registry.npmjs.org/defangjs/1.0.7" + }, + { + "label": "SOURCE_REPO", + "url": "git+https://github.com/edoardottt/defangjs.git" + } + ], + "slsaProvenances": [], + "attestations": [], + "registries": [ + "https://registry.npmjs.org/" + ], + "relatedProjects": [ + { + "projectKey": { + "id": "github.com/edoardottt/defangjs" + }, + "relationProvenance": "UNVERIFIED_METADATA", + "relationType": "ISSUE_TRACKER" + }, + { + "projectKey": { + "id": "github.com/edoardottt/defangjs" + }, + "relationProvenance": "UNVERIFIED_METADATA", + "relationType": "SOURCE_REPO" + } + ] + } + } + ] + }` - t.Run("GetRequirements npm pino 6.14.0", func(t *testing.T) { - got, err := api.GetRequirements("npm", "pino", "6.14.0") + t.Run("Query versionKey.system=NPM&versionKey.name=defangjs&versionKey.version=1.0.7", func(t *testing.T) { + got, err := api.Query("versionKey.system=NPM&versionKey.name=defangjs&versionKey.version=1.0.7") require.Nil(t, err) - var r def.Requirements + var a def.Results - if err := json.Unmarshal([]byte(result), &r); err != nil { + if err := json.Unmarshal([]byte(result), &a); err != nil { log.Fatal(err) } - require.Equal(t, r, got) + require.Equal(t, a, got) }) } diff --git a/pkg/depsdev/v3alpha/api.go b/pkg/depsdev/v3alpha/api.go index b8e9d72..18dc206 100644 --- a/pkg/depsdev/v3alpha/api.go +++ b/pkg/depsdev/v3alpha/api.go @@ -181,7 +181,7 @@ func (a *APIv3Alpha) GetRequirements(packageManager, packageName, version string // GetProjectPackageVersions returns known mappings between the requested project and package versions. // At most 1500 package versions are returned. // Mappings which were derived from attestations are served first. -func (a *APIv3Alpha) GetPackageVersions(projectName string) (def.PackageVersions, error) { +func (a *APIv3Alpha) GetProjectPackageVersions(projectName string) (def.PackageVersions, error) { var response def.PackageVersions var path = fmt.Sprintf(GetProjectPackageVersionsPath, url.PathEscape(projectName)) diff --git a/pkg/depsdev/v3alpha/api_test.go b/pkg/depsdev/v3alpha/api_test.go index 66a9bcd..cda27fa 100644 --- a/pkg/depsdev/v3alpha/api_test.go +++ b/pkg/depsdev/v3alpha/api_test.go @@ -30,7 +30,7 @@ var ( api = depsdev.NewV3AlphaAPI() ) -func TestGetInfo(t *testing.T) { +func TestGetPackage(t *testing.T) { result := `{ "packageKey":{ "system":"NPM", @@ -232,6 +232,176 @@ func TestGetVersion(t *testing.T) { }) } +func TestGetRequirements(t *testing.T) { + result := `{ + "npm": { + "dependencies": { + "dependencies": [ + { + "name": "fast-redact", + "requirement": "^3.0.0" + }, + { + "name": "fast-safe-stringify", + "requirement": "^2.0.8" + }, + { + "name": "flatstr", + "requirement": "^1.0.12" + }, + { + "name": "pino-std-serializers", + "requirement": "^3.1.0" + }, + { + "name": "process-warning", + "requirement": "^1.0.0" + }, + { + "name": "quick-format-unescaped", + "requirement": "^4.0.3" + }, + { + "name": "sonic-boom", + "requirement": "^1.0.2" + } + ], + "devDependencies": [ + { + "name": "airtap", + "requirement": "4.0.3" + }, + { + "name": "benchmark", + "requirement": "^2.1.4" + }, + { + "name": "bole", + "requirement": "^4.0.0" + }, + { + "name": "bunyan", + "requirement": "^1.8.14" + }, + { + "name": "docsify-cli", + "requirement": "^4.4.1" + }, + { + "name": "eslint", + "requirement": "^7.17.0" + }, + { + "name": "eslint-config-standard", + "requirement": "^16.0.2" + }, + { + "name": "eslint-plugin-import", + "requirement": "^2.22.1" + }, + { + "name": "eslint-plugin-node", + "requirement": "^11.1.0" + }, + { + "name": "eslint-plugin-promise", + "requirement": "^5.1.0" + }, + { + "name": "execa", + "requirement": "^5.0.0" + }, + { + "name": "fastbench", + "requirement": "^1.0.1" + }, + { + "name": "flush-write-stream", + "requirement": "^2.0.0" + }, + { + "name": "import-fresh", + "requirement": "^3.2.1" + }, + { + "name": "log", + "requirement": "^6.0.0" + }, + { + "name": "loglevel", + "requirement": "^1.6.7" + }, + { + "name": "pino-pretty", + "requirement": "^5.0.0" + }, + { + "name": "pre-commit", + "requirement": "^1.2.2" + }, + { + "name": "proxyquire", + "requirement": "^2.1.3" + }, + { + "name": "pump", + "requirement": "^3.0.0" + }, + { + "name": "semver", + "requirement": "^7.0.0" + }, + { + "name": "split2", + "requirement": "^3.1.1" + }, + { + "name": "steed", + "requirement": "^1.1.3" + }, + { + "name": "strip-ansi", + "requirement": "^6.0.0" + }, + { + "name": "tap", + "requirement": "^15.0.1" + }, + { + "name": "tape", + "requirement": "^5.0.0" + }, + { + "name": "through2", + "requirement": "^4.0.0" + }, + { + "name": "winston", + "requirement": "^3.3.3" + } + ], + "optionalDependencies": [], + "peerDependencies": [], + "bundleDependencies": [] + }, + "Bundled": [] + } + }` + + t.Run("GetRequirements npm pino 6.14.0", func(t *testing.T) { + got, err := api.GetRequirements("npm", "pino", "6.14.0") + require.Nil(t, err) + + var r def.Requirements + + if err := json.Unmarshal([]byte(result), &r); err != nil { + log.Fatal(err) + } + + require.Equal(t, r, got) + }) +} + func TestGetDependencies(t *testing.T) { result := `{ "nodes": [ @@ -373,6 +543,39 @@ func TestGetDependencies(t *testing.T) { }) } +func TestGetDependents(t *testing.T) { + t.Run("GetDependents npm pino 9.0.0", func(t *testing.T) { + got, err := api.GetDependents("npm", "pino", "9.0.0") + require.Nil(t, err) + + require.GreaterOrEqual(t, got.DependentCount, 0) + require.GreaterOrEqual(t, got.DirectDependentCount, 0) + require.GreaterOrEqual(t, got.IndirectDependentCount, 0) + }) +} + +func TestGetProject(t *testing.T) { + t.Run("GetProject npm defangjs", func(t *testing.T) { + got, err := api.GetProject("github.com/edoardottt/defangjs") + require.Nil(t, err) + + // no checking of the actual value because they can change over time + // we just ensure the call to the API and unmarshaling of the response works properly + assert.NotEmpty(t, got) + }) +} + +func TestGetProjectPackageVersions(t *testing.T) { + t.Run("GetProjectPackageVersions npm defangjs", func(t *testing.T) { + got, err := api.GetProjectPackageVersions("github.com/edoardottt/defangjs") + require.Nil(t, err) + + // no checking of the actual value because they can change over time + // we just ensure the call to the API and unmarshaling of the response works properly + assert.NotEmpty(t, got) + }) +} + func TestGetAdvisory(t *testing.T) { result := `{ "advisoryKey": { @@ -401,173 +604,116 @@ func TestGetAdvisory(t *testing.T) { }) } -func TestGetRequirements(t *testing.T) { - result := `{ - "npm": { - "dependencies": { - "dependencies": [ - { - "name": "fast-redact", - "requirement": "^3.0.0" - }, - { - "name": "fast-safe-stringify", - "requirement": "^2.0.8" - }, - { - "name": "flatstr", - "requirement": "^1.0.12" - }, - { - "name": "pino-std-serializers", - "requirement": "^3.1.0" - }, - { - "name": "process-warning", - "requirement": "^1.0.0" - }, - { - "name": "quick-format-unescaped", - "requirement": "^4.0.3" - }, - { - "name": "sonic-boom", - "requirement": "^1.0.2" - } +func TestPurlLookup(t *testing.T) { + t.Run("PurlLookup", func(t *testing.T) { + result := `{ + "version": { + "versionKey": { + "system": "NPM", + "name": "@colors/colors", + "version": "1.5.0" + }, + "purl": "pkg:npm/%40colors/colors@1.5.0", + "publishedAt": "2022-02-12T07:39:04Z", + "isDefault": false, + "isDeprecated": false, + "licenses": [ + "MIT" ], - "devDependencies": [ - { - "name": "airtap", - "requirement": "4.0.3" - }, - { - "name": "benchmark", - "requirement": "^2.1.4" - }, - { - "name": "bole", - "requirement": "^4.0.0" - }, - { - "name": "bunyan", - "requirement": "^1.8.14" - }, - { - "name": "docsify-cli", - "requirement": "^4.4.1" - }, - { - "name": "eslint", - "requirement": "^7.17.0" - }, - { - "name": "eslint-config-standard", - "requirement": "^16.0.2" - }, - { - "name": "eslint-plugin-import", - "requirement": "^2.22.1" - }, - { - "name": "eslint-plugin-node", - "requirement": "^11.1.0" - }, - { - "name": "eslint-plugin-promise", - "requirement": "^5.1.0" - }, - { - "name": "execa", - "requirement": "^5.0.0" - }, - { - "name": "fastbench", - "requirement": "^1.0.1" - }, - { - "name": "flush-write-stream", - "requirement": "^2.0.0" - }, - { - "name": "import-fresh", - "requirement": "^3.2.1" - }, - { - "name": "log", - "requirement": "^6.0.0" - }, - { - "name": "loglevel", - "requirement": "^1.6.7" - }, - { - "name": "pino-pretty", - "requirement": "^5.0.0" - }, - { - "name": "pre-commit", - "requirement": "^1.2.2" - }, - { - "name": "proxyquire", - "requirement": "^2.1.3" - }, - { - "name": "pump", - "requirement": "^3.0.0" - }, - { - "name": "semver", - "requirement": "^7.0.0" - }, - { - "name": "split2", - "requirement": "^3.1.1" - }, - { - "name": "steed", - "requirement": "^1.1.3" - }, - { - "name": "strip-ansi", - "requirement": "^6.0.0" - }, - { - "name": "tap", - "requirement": "^15.0.1" - }, - { - "name": "tape", - "requirement": "^5.0.0" - }, - { - "name": "through2", - "requirement": "^4.0.0" - }, - { - "name": "winston", - "requirement": "^3.3.3" - } + "licenseDetails": [ + { + "license": "MIT", + "spdx": "MIT" + } ], - "optionalDependencies": [], - "peerDependencies": [], - "bundleDependencies": [] - }, - "Bundled": [] + "advisoryKeys": [], + "links": [ + { + "label": "HOMEPAGE", + "url": "https://github.com/DABH/colors.js" + }, + { + "label": "ISSUE_TRACKER", + "url": "https://github.com/DABH/colors.js/issues" + }, + { + "label": "ORIGIN", + "url": "https://registry.npmjs.org/@colors%2Fcolors/1.5.0" + }, + { + "label": "SOURCE_REPO", + "url": "git+ssh://git@github.com/DABH/colors.js.git" + } + ], + "slsaProvenances": [], + "attestations": [], + "registries": [ + "https://registry.npmjs.org/" + ], + "relatedProjects": [ + { + "projectKey": { + "id": "github.com/dabh/colors.js" + }, + "relationProvenance": "UNVERIFIED_METADATA", + "relationType": "ISSUE_TRACKER" + }, + { + "projectKey": { + "id": "github.com/dabh/colors.js" + }, + "relationProvenance": "UNVERIFIED_METADATA", + "relationType": "SOURCE_REPO" + } + ], + "upstreamIdentifiers": [ + { + "packageName": "@colors/colors", + "versionString": "1.5.0", + "source": "NPM_NPMJS_ORG" + } + ] } - }` + }` - t.Run("GetRequirements npm pino 6.14.0", func(t *testing.T) { - got, err := api.GetRequirements("npm", "pino", "6.14.0") + got, err := api.PurlLookup("pkg:npm/%40colors/colors@1.5.0") require.Nil(t, err) - var r def.Requirements + var a def.Purl - if err := json.Unmarshal([]byte(result), &r); err != nil { + if err := json.Unmarshal([]byte(result), &a); err != nil { log.Fatal(err) } - require.Equal(t, r, got) + require.Equal(t, a, got) + }) + + t.Run("PurlLookup batch multi pages", func(t *testing.T) { + const N = 300 + + reqs := def.PurlBatchBody{ + Requests: []def.PurlBatchRequest{}, + PageToken: "", + } + + projects := make([]def.PurlBatchRequest, 0, N) + for i := 0; i < N; i++ { + projects = append(projects, def.PurlBatchRequest{Purl: "pkg:npm/%40colors/colors@1.5.0"}) + } + + reqs.Requests = projects + + iter, err := api.PurlLookupBatch(reqs) + + require.Nil(t, err) + assert.NotNil(t, iter) + + defer iter.Close() + + results, err := consumeIter(iter) + require.NoError(t, err) + + assert.Equal(t, N, len(results)) }) } @@ -781,7 +927,6 @@ func TestGetProjectBatch(t *testing.T) { assert.Equal(t, N, len(results)) }) } - func TestPurlLookupBatch(t *testing.T) { t.Run("PurlLookup batch", func(t *testing.T) { iter, err := api.PurlLookupBatch((def.PurlBatchBody{ @@ -836,6 +981,90 @@ func TestPurlLookupBatch(t *testing.T) { }) } +func TestQuery(t *testing.T) { + result := `{ + "results": [ + { + "version": { + "versionKey": { + "system": "NPM", + "name": "defangjs", + "version": "1.0.7" + }, + "purl": "pkg:npm/defangjs@1.0.7", + "publishedAt": "2023-05-16T09:48:31Z", + "isDefault": true, + "isDeprecated": false, + "licenses": [ + "GPL-3.0" + ], + "licenseDetails": [ + { + "license": "GPL-3.0", + "spdx": "GPL-3.0" + } + ], + "advisoryKeys": [], + "links": [ + { + "label": "HOMEPAGE", + "url": "https://github.com/edoardottt/defangjs#readme" + }, + { + "label": "ISSUE_TRACKER", + "url": "https://github.com/edoardottt/defangjs/issues" + }, + { + "label": "ORIGIN", + "url": "https://registry.npmjs.org/defangjs/1.0.7" + }, + { + "label": "SOURCE_REPO", + "url": "git+https://github.com/edoardottt/defangjs.git" + } + ], + "slsaProvenances": [], + "attestations": [], + "registries": [ + "https://registry.npmjs.org/" + ], + "relatedProjects": [ + { + "projectKey": { + "id": "github.com/edoardottt/defangjs" + }, + "relationProvenance": "UNVERIFIED_METADATA", + "relationType": "ISSUE_TRACKER" + }, + { + "projectKey": { + "id": "github.com/edoardottt/defangjs" + }, + "relationProvenance": "UNVERIFIED_METADATA", + "relationType": "SOURCE_REPO" + } + ], + "upstreamIdentifiers": [] + }, + "artifacts": [] + } + ] + }` + + t.Run("Query versionKey.system=NPM&versionKey.name=defangjs&versionKey.version=1.0.7", func(t *testing.T) { + got, err := api.Query("versionKey.system=NPM&versionKey.name=defangjs&versionKey.version=1.0.7") + require.Nil(t, err) + + var a def.Results + + if err := json.Unmarshal([]byte(result), &a); err != nil { + log.Fatal(err) + } + + require.Equal(t, a, got) + }) +} + func consumeIter[T any](iter *depsdev.Iterator[T]) ([]T, error) { l := []T{}