internal/api: support package-level vulnerability filtering Update the v1beta vulnerabilities endpoint to accept both module paths and package paths. When a package path is supplied, the handler resolves the containing module path via resolveModulePath and then passes the package path as a filter to vuln.VulnsForPackage. When a module path is supplied, it returns all vulnerabilities for that module. Also expose a `module` query parameter in VulnParams to allow resolving ambiguous package paths. Change-Id: I8666f98d1d05d9426eef67deb0e28ca193f50111 Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/780182 Reviewed-by: Jonathan Amsterdam <jba@google.com> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> kokoro-CI: kokoro <noreply+kokoro@google.com> Auto-Submit: Ethan Lee <ethanalee@google.com>
diff --git a/internal/api/api.go b/internal/api/api.go index b7c2f23..a0c2b5c 100644 --- a/internal/api/api.go +++ b/internal/api/api.go
@@ -566,9 +566,9 @@ return serveJSON(w, http.StatusOK, resp, shortCacheDur) } -// ServeVulnerabilities handles requests for the v1beta module vulnerabilities endpoint. +// ServeVulnerabilities handles requests for the v1beta vulnerabilities endpoint. // api:route /v1beta/vulns/{path} -// api:desc Vulnerabilities of the module at {path}, from +// api:desc Vulnerabilities of the module or package at {path}, from // api:desc the Go vulnerability database (https://vuln.go.dev). // api:desc Only results whose ID or details // api:desc matches the regexp in the filter query parameter are returned. @@ -577,10 +577,10 @@ return func(w http.ResponseWriter, r *http.Request, ds internal.DataSource) (err error) { defer derrors.Wrap(&err, "ServeVulnerabilities") - modulePath := trimPath(r, "/v1beta/vulns/") - if modulePath == "" { - return BadRequest("missing module path", - "the module path must be provided after '/vulns/'") + path := trimPath(r, "/v1beta/vulns/") + if path == "" { + return BadRequest("missing path", + "the package or module path must be provided after '/vulns/'") } // api:params VulnParams @@ -598,19 +598,20 @@ requestedVersion = version.Latest } - // Verify module existence and resolve version. - um, err := ds.GetUnitMeta(r.Context(), modulePath, internal.UnknownModulePath, requestedVersion) + // Verify package or module existence and resolve containing module. + um, err := resolveModulePath(r, ds, path, params.Module, requestedVersion) if err != nil { return err } - if err := checkModulePath(modulePath, um.ModulePath); err != nil { - return err + var pkgPath string + if path != um.ModulePath { + pkgPath = path } - // Use VulnsForPackage from internal/vuln to get vulnerabilities for the module. - // Passing an empty packagePath gets all vulns for the module. - vulns := vuln.VulnsForPackage(r.Context(), um.ModulePath, um.Version, "", vc) + // Use VulnsForPackage from internal/vuln to get vulnerabilities. + // If pkgPath is non-empty, it filters vulnerabilities to only that package. + vulns := vuln.VulnsForPackage(r.Context(), um.ModulePath, um.Version, pkgPath, vc) vulns, err = filter(vulns, params.Filter, func(v vuln.Vuln) []string { return []string{v.ID, v.Details}
diff --git a/internal/api/api_test.go b/internal/api/api_test.go index d6cd9d5..d99de33 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go
@@ -33,6 +33,36 @@ }, }, }, + { + ID: "VULN-2", + Summary: "Vulnerability 2", + Affected: []osv.Affected{ + { + Module: osv.Module{Path: "example.com"}, + Ranges: []osv.Range{{Type: osv.RangeTypeSemver, Events: []osv.RangeEvent{{Introduced: "0"}, {Fixed: "1.1.0"}}}}, + EcosystemSpecific: osv.EcosystemSpecific{ + Packages: []osv.Package{ + {Path: "example.com/pkg"}, + }, + }, + }, + }, + }, + { + ID: "VULN-3", + Summary: "Vulnerability 3", + Affected: []osv.Affected{ + { + Module: osv.Module{Path: "example.com"}, + Ranges: []osv.Range{{Type: osv.RangeTypeSemver, Events: []osv.RangeEvent{{Introduced: "0"}, {Fixed: "1.1.0"}}}}, + EcosystemSpecific: osv.EcosystemSpecific{ + Packages: []osv.Package{ + {Path: "example.com/other"}, + }, + }, + }, + }, + }, }) if err != nil { t.Fatal(err) @@ -64,6 +94,15 @@ }, }, }, + { + UnitMeta: internal.UnitMeta{ + Path: "example.com/other", + ModuleInfo: internal.ModuleInfo{ + ModulePath: "example.com", + Version: v, + }, + }, + }, }, }) } @@ -81,7 +120,7 @@ name: "all vulns", url: "/v1beta/vulns/example.com?version=v1.0.0", wantStatus: http.StatusOK, - wantCount: 1, + wantCount: 3, }, { name: "no vulns", @@ -92,7 +131,14 @@ { name: "package path in vulns endpoint", url: "/v1beta/vulns/example.com/pkg?version=v1.0.0", - wantStatus: http.StatusBadRequest, + wantStatus: http.StatusOK, + wantCount: 2, + }, + { + name: "another package path", + url: "/v1beta/vulns/example.com/other?version=v1.0.0", + wantStatus: http.StatusOK, + wantCount: 2, }, } { t.Run(test.name, func(t *testing.T) {
diff --git a/internal/api/openapi.yaml b/internal/api/openapi.yaml index c9f07b1..2ec7eca 100644 --- a/internal/api/openapi.yaml +++ b/internal/api/openapi.yaml
@@ -436,6 +436,14 @@ "operationId": "getVulns", "parameters": [ { + "description": "Module path.", + "in": "query", + "name": "module", + "schema": { + "type": "string" + } + }, + { "description": "Module version: semantic version, 'latest', or default branches 'master' or 'main'.\n(Latest if empty).", "in": "query", "name": "version", @@ -480,7 +488,7 @@ "description": "Successful response" } }, - "summary": "Vulnerabilities of the module at {path}, from\nthe Go vulnerability database (https://vuln.go.dev).\nOnly results whose ID or details\nmatches the regexp in the filter query parameter are returned." + "summary": "Vulnerabilities of the module or package at {path}, from\nthe Go vulnerability database (https://vuln.go.dev).\nOnly results whose ID or details\nmatches the regexp in the filter query parameter are returned." } } },
diff --git a/internal/api/params.go b/internal/api/params.go index 2fc66ea..32b8403 100644 --- a/internal/api/params.go +++ b/internal/api/params.go
@@ -101,8 +101,10 @@ ListParams } -// VulnParams are query parameters for /v1beta/vulns/{module}. +// VulnParams are query parameters for /v1beta/vulns/{path}. type VulnParams struct { + // Module path. + Module string `form:"module"` // Module version: semantic version, 'latest', or default branches 'master' or 'main'. // (Latest if empty). Version string `form:"version"`