diff --git a/api/v1/README.md b/api/v1/README.md index e71f8aec..3370b185 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,61 @@ 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?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" + } + } + } + } + ], + "NextPage":"gAAAAABW1ABiOlm6KMDKYFE022bEy_IFJdm4ExxTNuJZMN0Eycn0Sut2tOH9bDB4EWGy5s6xwATUHiG-6JXXaU5U32sBs6_DmA==" +} +``` + #### POST /namespaces/`:name`/vulnerabilities ###### Description diff --git a/api/v1/models.go b/api/v1/models.go index d96dd589..f3b3c5e0 100644 --- a/api/v1/models.go +++ b/api/v1/models.go @@ -215,7 +215,8 @@ func NotificationFromDatabaseModel(dbNotification database.VulnerabilityNotifica var nextPageStr string if nextPage != database.NoVulnerabilityNotificationPage { - nextPageStr = pageNumberToToken(nextPage, key) + nextPageBytes, _ := tokenMarshal(nextPage, key) + nextPageStr = string(nextPageBytes) } var created, notified, deleted string @@ -274,8 +275,10 @@ 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"` + NextPage string `json:"NextPage,omitempty"` + Error *Error `json:"Error,omitempty"` } type NotificationEnvelope struct { @@ -289,30 +292,23 @@ type FeatureEnvelope struct { Error *Error `json:"Error,omitempty"` } -func tokenToPageNumber(token, key string) (database.VulnerabilityNotificationPageNumber, error) { +func tokenUnmarshal(token string, key string, v interface{}) error { k, _ := fernet.DecodeKey(key) msg := fernet.VerifyAndDecrypt([]byte(token), time.Hour, []*fernet.Key{k}) if msg == nil { - return database.VulnerabilityNotificationPageNumber{}, errors.New("invalid or expired pagination token") + return errors.New("invalid or expired pagination token") } - page := database.VulnerabilityNotificationPageNumber{} - err := json.NewDecoder(bytes.NewBuffer(msg)).Decode(&page) - return page, err + return json.NewDecoder(bytes.NewBuffer(msg)).Decode(&v) } -func pageNumberToToken(page database.VulnerabilityNotificationPageNumber, key string) string { +func tokenMarshal(v interface{}, key string) ([]byte, error) { var buf bytes.Buffer - err := json.NewEncoder(&buf).Encode(page) + err := json.NewEncoder(&buf).Encode(v) if err != nil { - log.Fatal("failed to encode VulnerabilityNotificationPageNumber") + return nil, err } k, _ := fernet.DecodeKey(key) - tokenBytes, err := fernet.EncryptAndSign(buf.Bytes(), k) - if err != nil { - log.Fatal("failed to encrypt VulnerabilityNotificationpageNumber") - } - - return string(tokenBytes) + return fernet.EncryptAndSign(buf.Bytes(), k) } 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..3edbe769 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,68 @@ 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 + } + + page := 0 + pageStrs, pageExists := query["page"] + if pageExists { + err = tokenUnmarshal(pageStrs[0], ctx.Config.PaginationKey, &page) + if err != nil { + writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"invalid page format: " + err.Error()}}) + return getNotificationRoute, http.StatusBadRequest + } + } + + namespace := p.ByName("namespaceName") + if namespace == "" { + writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"namespace should not be empty"}}) + return getNotificationRoute, http.StatusBadRequest + } + + dbVulns, nextPage, err := ctx.Store.ListVulnerabilities(namespace, limit, page) + if err == cerrors.ErrNotFound { + writeResponse(w, r, http.StatusNotFound, VulnerabilityEnvelope{Error: &Error{err.Error()}}) + return getVulnerabilityRoute, http.StatusNotFound + } else 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) + } + + var nextPageStr string + if nextPage != -1 { + nextPageBytes, err := tokenMarshal(nextPage, ctx.Config.PaginationKey) + if err != nil { + writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"failed to marshal token: " + err.Error()}}) + return getNotificationRoute, http.StatusBadRequest + } + nextPageStr = string(nextPageBytes) + } + + writeResponse(w, r, http.StatusOK, VulnerabilityEnvelope{Vulnerabilities: &vulns, NextPage: nextPageStr}) + 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) @@ -385,14 +448,19 @@ func getNotification(w http.ResponseWriter, r *http.Request, p httprouter.Params page := database.VulnerabilityNotificationFirstPage pageStrs, pageExists := query["page"] if pageExists { - page, err = tokenToPageNumber(pageStrs[0], ctx.Config.PaginationKey) + err := tokenUnmarshal(pageStrs[0], ctx.Config.PaginationKey, &page) if err != nil { writeResponse(w, r, http.StatusBadRequest, NotificationEnvelope{Error: &Error{"invalid page format: " + err.Error()}}) return getNotificationRoute, http.StatusBadRequest } pageToken = pageStrs[0] } else { - pageToken = pageNumberToToken(page, ctx.Config.PaginationKey) + pageTokenBytes, err := tokenMarshal(page, ctx.Config.PaginationKey) + if err != nil { + writeResponse(w, r, http.StatusBadRequest, NotificationEnvelope{Error: &Error{"failed to marshal token: " + err.Error()}}) + return getNotificationRoute, http.StatusBadRequest + } + pageToken = string(pageTokenBytes) } dbNotification, nextPage, err := ctx.Store.GetNotification(p.ByName("notificationName"), limit, page) diff --git a/database/database.go b/database/database.go index cf30cb82..0c84f99e 100644 --- a/database/database.go +++ b/database/database.go @@ -60,6 +60,12 @@ 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. + // The first given page should be 0. The function will then return the next available page. + // If there is no more page, -1 has to be returned. + ListVulnerabilities(namespaceName string, limit int, page int) ([]Vulnerability, int, 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..aba20d70 100644 --- a/database/pgsql/queries.go +++ b/database/pgsql/queries.go @@ -38,7 +38,8 @@ const ( UNION SELECT id FROM new_namespace` - listNamespace = `SELECT id, name FROM Namespace` + searchNamespace = `SELECT id FROM Namespace WHERE name = $1` + listNamespace = `SELECT id, name FROM Namespace` // feature.go soiFeature = ` @@ -144,6 +145,10 @@ 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 + AND v.id >= $2 + ORDER BY v.id + LIMIT $3` searchVulnerabilityFixedIn = ` SELECT vfif.version, f.id, f.Name diff --git a/database/pgsql/vulnerability.go b/database/pgsql/vulnerability.go index ca53801a..74ee9828 100644 --- a/database/pgsql/vulnerability.go +++ b/database/pgsql/vulnerability.go @@ -28,6 +28,61 @@ import ( "github.com/guregu/null/zero" ) +func (pgSQL *pgSQL) ListVulnerabilities(namespaceName string, limit int, startID int) ([]database.Vulnerability, int, error) { + defer observeQueryTime("listVulnerabilities", "all", time.Now()) + + // Query Namespace. + var id int + err := pgSQL.QueryRow(searchNamespace, namespaceName).Scan(&id) + if err != nil { + return nil, -1, handleError("searchNamespace", err) + } else if id == 0 { + return nil, -1, cerrors.ErrNotFound + } + + // Query. + query := searchVulnerabilityBase + searchVulnerabilityByNamespace + rows, err := pgSQL.Query(query, namespaceName, startID, limit+1) + if err != nil { + return nil, -1, handleError("searchVulnerabilityByNamespace", err) + } + defer rows.Close() + + var vulns []database.Vulnerability + nextID := -1 + size := 0 + // 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, -1, handleError("searchVulnerabilityByNamespace.Scan()", err) + } + size++ + if size > limit { + nextID = vulnerability.ID + } else { + vulns = append(vulns, vulnerability) + } + } + + if err := rows.Err(); err != nil { + return nil, -1, handleError("searchVulnerabilityByNamespace.Rows()", err) + } + + return vulns, nextID, nil +} + func (pgSQL *pgSQL) FindVulnerability(namespaceName, name string) (database.Vulnerability, error) { return findVulnerability(pgSQL, namespaceName, name, false) }