From a541e964e07ea0e9a70f2ebee68897edf852bcba Mon Sep 17 00:00:00 2001 From: liangchenye Date: Fri, 26 Feb 2016 18:18:45 +0800 Subject: [PATCH] New API: list vulnerabilities by namespace Signed-off-by: liangchenye --- api/v1/README.md | 55 +++++++++++++++++++++++++++++++++ api/v1/models.go | 5 +-- api/v1/router.go | 1 + api/v1/routes.go | 48 ++++++++++++++++++++++++++++ database/database.go | 4 +++ database/pgsql/queries.go | 3 ++ database/pgsql/vulnerability.go | 44 ++++++++++++++++++++++++++ 7 files changed, 158 insertions(+), 2 deletions(-) diff --git a/api/v1/README.md b/api/v1/README.md index e71f8aec..f11929bf 100644 --- a/api/v1/README.md +++ b/api/v1/README.md @@ -8,6 +8,7 @@ - [Namespaces](#namespaces) - [GET](#get-namespaces) - [Vulnerabilities](#vulnerabilities) + - [List](#get-namespacesnsnamevulnerabilities) - [POST](#post-namespacesnamevulnerabilities) - [GET](#get-namespacesnsnamevulnerabilitiesvulnname) - [PUT](#put-namespacesnsnamevulnerabilitiesvulnname) @@ -196,6 +197,60 @@ Server: clair ## Vulnerabilities +#### GET /namespaces/`:nsName`/vulnerabilities + +###### Description + +The GET route for the Vulnerabilities resource displays the vulnerabilities data for a given namespace. + +###### Query Parameters + +| Name | Type | Required | Description | +|---------|------|----------|------------------------------------------------------------| +| limit | int | required | Limits the amount of the vunlerabilities data for a given namespace. | +| page | int | required | Displays the specific page of the vunlerabilities data for a given namespace. | + +###### Example Request + +```json +GET http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities?page=0&limit=2 HTTP/1.1 +``` + +###### Example Response + +```json +HTTP/1.1 200 OK +Content-Type: application/json;charset=utf-8 +Server: clair + +{ + "Vulnerabilities": [ + { + "Name": "CVE-1999-1332", + "Namespace": "debian:8", + "Description": "gzexe in the gzip package on Red Hat Linux 5.0 and earlier allows local users to overwrite files of other users via a symlink attack on a temporary file.", + "Link": "https://security-tracker.debian.org/tracker/CVE-1999-1332", + "Severity": "Low" + }, + { + "Name": "CVE-1999-1572", + "Namespace": "debian:8", + "Description": "cpio on FreeBSD 2.1.0, Debian GNU/Linux 3.0, and possibly other operating systems, uses a 0 umask when creating files using the -O (archive) or -F options, which creates the files with mode 0666 and allows local users to read or overwrite those files.", + "Link": "https://security-tracker.debian.org/tracker/CVE-1999-1572", + "Severity": "Low", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 2.1, + "Vectors": "AV:L/AC:L/Au:N/C:P/I:N" + } + } + } + } + ] +} +``` + #### POST /namespaces/`:name`/vulnerabilities ###### Description diff --git a/api/v1/models.go b/api/v1/models.go index d96dd589..456024e5 100644 --- a/api/v1/models.go +++ b/api/v1/models.go @@ -274,8 +274,9 @@ type NamespaceEnvelope struct { } type VulnerabilityEnvelope struct { - Vulnerability *Vulnerability `json:"Vulnerability,omitempty"` - Error *Error `json:"Error,omitempty"` + Vulnerability *Vulnerability `json:"Vulnerability,omitempty"` + Vulnerabilities *[]Vulnerability `json:"Vulnerabilities,omitempty"` + Error *Error `json:"Error,omitempty"` } type NotificationEnvelope struct { diff --git a/api/v1/router.go b/api/v1/router.go index c4c6662a..5a3d640e 100644 --- a/api/v1/router.go +++ b/api/v1/router.go @@ -34,6 +34,7 @@ func NewRouter(ctx *context.RouteContext) *httprouter.Router { router.GET("/namespaces", context.HTTPHandler(getNamespaces, ctx)) // Vulnerabilities + router.GET("/namespaces/:namespaceName/vulnerabilities", context.HTTPHandler(getVulnerabilities, ctx)) router.POST("/namespaces/:namespaceName/vulnerabilities", context.HTTPHandler(postVulnerability, ctx)) router.GET("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName", context.HTTPHandler(getVulnerability, ctx)) router.PUT("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName", context.HTTPHandler(putVulnerability, ctx)) diff --git a/api/v1/routes.go b/api/v1/routes.go index f380ac5c..f7357f2a 100644 --- a/api/v1/routes.go +++ b/api/v1/routes.go @@ -38,6 +38,7 @@ const ( getLayerRoute = "v1/getLayer" deleteLayerRoute = "v1/deleteLayer" getNamespacesRoute = "v1/getNamespaces" + getVulnerabilitiesRoute = "v1/getVulnerabilities" postVulnerabilityRoute = "v1/postVulnerability" getVulnerabilityRoute = "v1/getVulnerability" putVulnerabilityRoute = "v1/putVulnerability" @@ -184,6 +185,53 @@ func getNamespaces(w http.ResponseWriter, r *http.Request, p httprouter.Params, return getNamespacesRoute, http.StatusOK } +func getVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) { + query := r.URL.Query() + + limitStrs, limitExists := query["limit"] + if !limitExists { + writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"must provide limit query parameter"}}) + return getVulnerabilitiesRoute, http.StatusBadRequest + } + limit, err := strconv.Atoi(limitStrs[0]) + if err != nil { + writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"invalid limit format: " + err.Error()}}) + return getVulnerabilitiesRoute, http.StatusBadRequest + } else if limit <= 0 { + writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"limit value should not be less than zero"}}) + return getVulnerabilitiesRoute, http.StatusBadRequest + } + + pageStrs, pageExists := query["page"] + if !pageExists { + writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"must provide page query parameter"}}) + return getVulnerabilitiesRoute, http.StatusBadRequest + } + page, err := strconv.Atoi(pageStrs[0]) + if err != nil { + writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"invalid page format: " + err.Error()}}) + return getVulnerabilitiesRoute, http.StatusBadRequest + } else if page < 0 { + writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"page value should not be less than zero"}}) + return getVulnerabilitiesRoute, http.StatusBadRequest + } + + dbVulns, err := ctx.Store.ListVulnerabilities(p.ByName("namespaceName"), limit, page) + if err != nil { + writeResponse(w, r, http.StatusInternalServerError, VulnerabilityEnvelope{Error: &Error{err.Error()}}) + return getVulnerabilitiesRoute, http.StatusInternalServerError + } + + var vulns []Vulnerability + for _, dbVuln := range dbVulns { + vuln := VulnerabilityFromDatabaseModel(dbVuln, false) + vulns = append(vulns, vuln) + } + + writeResponse(w, r, http.StatusOK, VulnerabilityEnvelope{Vulnerabilities: &vulns}) + return getVulnerabilitiesRoute, http.StatusOK +} + func postVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) { request := VulnerabilityEnvelope{} err := decodeJSON(r, &request) diff --git a/database/database.go b/database/database.go index cf30cb82..cec45f0f 100644 --- a/database/database.go +++ b/database/database.go @@ -60,6 +60,10 @@ type Datastore interface { DeleteLayer(name string) error // # Vulnerability + // ListVulnerabilities returns the list of vulnerabilies of a certain Namespace. + // The Limit and page parameters are used to paginate the return list. + ListVulnerabilities(namespaceName string, limit int, page int) ([]Vulnerability, error) + // InsertVulnerabilities stores the given Vulnerabilities in the database, updating them if // necessary. A vulnerability is uniquely identified by its Namespace and its Name. // The FixedIn field may only contain a partial list of Features that are affected by the diff --git a/database/pgsql/queries.go b/database/pgsql/queries.go index 22ca33b8..a79f4a86 100644 --- a/database/pgsql/queries.go +++ b/database/pgsql/queries.go @@ -144,6 +144,9 @@ const ( searchVulnerabilityForUpdate = ` FOR UPDATE OF v` searchVulnerabilityByNamespaceAndName = ` WHERE n.name = $1 AND v.name = $2 AND v.deleted_at IS NULL` searchVulnerabilityByID = ` WHERE v.id = $1` + searchVulnerabilityByNamespace = ` WHERE n.name = $1 AND v.deleted_at IS NULL + ORDER BY v.name + LIMIT $2 offset $3` searchVulnerabilityFixedIn = ` SELECT vfif.version, f.id, f.Name diff --git a/database/pgsql/vulnerability.go b/database/pgsql/vulnerability.go index ca53801a..ade3a6f6 100644 --- a/database/pgsql/vulnerability.go +++ b/database/pgsql/vulnerability.go @@ -28,6 +28,50 @@ import ( "github.com/guregu/null/zero" ) +func (pgSQL *pgSQL) ListVulnerabilities(namespaceName string, limit int, page int) ([]database.Vulnerability, error) { + return listVulnerabilities(pgSQL, namespaceName, limit, page) +} + +func listVulnerabilities(queryer Queryer, namespaceName string, limit int, page int) ([]database.Vulnerability, error) { + defer observeQueryTime("listVulnerabilities", "all", time.Now()) + + // Query. + query := searchVulnerabilityBase + searchVulnerabilityByNamespace + rows, err := queryer.Query(query, namespaceName, limit, page*limit) + if err != nil { + return nil, handleError("searchVulnerabilityByNamespace", err) + } + defer rows.Close() + + var vulns []database.Vulnerability + // Scan query. + for rows.Next() { + var vulnerability database.Vulnerability + + err := rows.Scan( + &vulnerability.ID, + &vulnerability.Name, + &vulnerability.Namespace.ID, + &vulnerability.Namespace.Name, + &vulnerability.Description, + &vulnerability.Link, + &vulnerability.Severity, + &vulnerability.Metadata, + ) + if err != nil { + return nil, handleError("searchVulnerabilityByNamespace.Scan()", err) + } else { + vulns = append(vulns, vulnerability) + } + } + + if err := rows.Err(); err != nil { + return nil, handleError("searchVulnerabilityByNamespace.Rows()", err) + } + + return vulns, nil +} + func (pgSQL *pgSQL) FindVulnerability(namespaceName, name string) (database.Vulnerability, error) { return findVulnerability(pgSQL, namespaceName, name, false) }