diff --git a/api/v1/README.md b/api/v1/README.md index 410c045b..99fb142b 100644 --- a/api/v1/README.md +++ b/api/v1/README.md @@ -129,18 +129,18 @@ Server: clair { "Layer": { "Name": "17675ec01494d651e1ccf81dc9cf63959ebfeed4f978fddb1666b6ead008ed52", - "NamespaceName": "debian:8", + "Namespaces": [{"Name": "debian", "Version": "8"}], "ParentName": "140f9bdfeb9784cf8730e9dab5dd12fbd704151cf555ac8cae650451794e5ac2", "IndexedByVersion": 1, "Features": [ { "Name": "coreutils", - "NamespaceName": "debian:8", + "Namespace": {"Name": "debian", "Version": "8"}, "Version": "8.23-4", "Vulnerabilities": [ { "Name": "CVE-2014-9471", - "NamespaceName": "debian:8", + "Namespace": {"Name": "debian", "Version": "8"}, "Description": "The parse_datetime function in GNU coreutils allows remote attackers to cause a denial of service (crash) or possibly execute arbitrary code via a crafted date string, as demonstrated by the \"--date=TZ=\"123\"345\" @1\" string to the touch or date command.", "Link": "https://security-tracker.debian.org/tracker/CVE-2014-9471", "Severity": "Low", @@ -196,15 +196,15 @@ Server: clair { "Namespaces": [ - { "Name": "debian:8" }, - { "Name": "debian:9" } + { "ID": "gAAAAABXTQKgma_TLKq0wr1D6wVB507N3fi9wsWMUypYOSXTDVxQ8OR5L5S6PqZ9Wh0IzWojnVmlspyTL4cyjytyra7U9vAHMA==", "Name": "debian", "Version": "8" }, + { "ID": "gAAAAABXTQPZmOFlOR8zzuhv8Y2fD7HbUY8O6z_Py2vibB9uveWZoycSY1HDIkcf7lN_UDynom4kWubFS4h9KBCbWwjNIqacsw==", "Name": "debian", "Version": "9" } ] } ``` ## Vulnerabilities -#### GET /namespaces/`:nsName`/vulnerabilities +#### GET /namespaces/`:nsID`/vulnerabilities ###### Description @@ -220,7 +220,7 @@ The GET route for the Vulnerabilities resource displays the vulnerabilities data ###### Example Request ```json -GET http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities?limit=2 HTTP/1.1 +GET http://localhost:6060/v1/namespaces/gAAAAABXTQKgma_TLKq0wr1D6wVB507N3fi9wsWMUypYOSXTDVxQ8OR5L5S6PqZ9Wh0IzWojnVmlspyTL4cyjytyra7U9vAHMA==/vulnerabilities?limit=2 HTTP/1.1 ``` ###### Example Response @@ -234,14 +234,14 @@ Server: clair "Vulnerabilities": [ { "Name": "CVE-1999-1332", - "NamespaceName": "debian:8", + "Namespace": {"Name": "debian", "Version": "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", - "NamespaceName": "debian:8", + "Namespace": {"Name": "debian", "Version": "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", @@ -259,7 +259,7 @@ Server: clair } ``` -#### POST /namespaces/`:name`/vulnerabilities +#### POST /namespaces/`:nsID`/vulnerabilities ###### Description @@ -268,12 +268,12 @@ The POST route for the Vulnerabilities resource creates a new Vulnerability. ###### Example Request ```json -POST http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities HTTP/1.1 +POST http://localhost:6060/v1/namespaces/gAAAAABXTQKgma_TLKq0wr1D6wVB507N3fi9wsWMUypYOSXTDVxQ8OR5L5S6PqZ9Wh0IzWojnVmlspyTL4cyjytyra7U9vAHMA==/vulnerabilities HTTP/1.1 { "Vulnerability": { "Name": "CVE-2014-9471", - "NamespaceName": "debian:8", + "Namespace": {"Name": "debian", "Version": "8"}, "Link": "https://security-tracker.debian.org/tracker/CVE-2014-9471", "Description": "The parse_datetime function in GNU coreutils allows remote attackers to cause a denial of service (crash) or possibly execute arbitrary code via a crafted date string, as demonstrated by the \"--date=TZ=\"123\"345\" @1\" string to the touch or date command.", "Severity": "Low", @@ -288,7 +288,7 @@ POST http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities HTTP/1.1 "FixedIn": [ { "Name": "coreutils", - "NamespaceName": "debian:8", + "Namespace": {"Name": "debian", "Version": "8"}, "Version": "8.23-1" } ] @@ -306,7 +306,7 @@ Server: clair { "Vulnerability": { "Name": "CVE-2014-9471", - "NamespaceName": "debian:8", + "Namespace": {"Name": "debian", "Version": "8"}, "Link": "https://security-tracker.debian.org/tracker/CVE-2014-9471", "Description": "The parse_datetime function in GNU coreutils allows remote attackers to cause a denial of service (crash) or possibly execute arbitrary code via a crafted date string, as demonstrated by the \"--date=TZ=\"123\"345\" @1\" string to the touch or date command.", "Severity": "Low", @@ -321,7 +321,7 @@ Server: clair "FixedIn": [ { "Name": "coreutils", - "NamespaceName": "debian:8", + "Namespace": {"Name": "debian", "Version": "8"}, "Version": "8.23-1" } ] @@ -329,7 +329,7 @@ Server: clair } ``` -#### GET /namespaces/`:nsName`/vulnerabilities/`:vulnName` +#### GET /namespaces/`:nsID`/vulnerabilities/`:vulnName` ###### Description @@ -344,7 +344,7 @@ The GET route for the Vulnerabilities resource displays the current data for a g ###### Example Request ```json -GET http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities/CVE-2014-9471?fixedIn HTTP/1.1 +GET http://localhost:6060/v1/namespaces/gAAAAABXTQKgma_TLKq0wr1D6wVB507N3fi9wsWMUypYOSXTDVxQ8OR5L5S6PqZ9Wh0IzWojnVmlspyTL4cyjytyra7U9vAHMA==/vulnerabilities/CVE-2014-9471?fixedIn HTTP/1.1 ``` ###### Example Response @@ -357,7 +357,7 @@ Server: clair { "Vulnerability": { "Name": "CVE-2014-9471", - "NamespaceName": "debian:8", + "Namespace": {"Name": "debian", "Version": "8"}, "Link": "https://security-tracker.debian.org/tracker/CVE-2014-9471", "Description": "The parse_datetime function in GNU coreutils allows remote attackers to cause a denial of service (crash) or possibly execute arbitrary code via a crafted date string, as demonstrated by the \"--date=TZ=\"123\"345\" @1\" string to the touch or date command.", "Severity": "Low", @@ -372,7 +372,7 @@ Server: clair "FixedIn": [ { "Name": "coreutils", - "NamespaceName": "debian:8", + "Namespace": {"Name": "debian", "Version": "8"}, "Version": "8.23-1" } ] @@ -380,7 +380,7 @@ Server: clair } ``` -#### PUT /namespaces/`:nsName`/vulnerabilities/`:vulnName` +#### PUT /namespaces/`:nsID`/vulnerabilities/`:vulnName` ###### Description @@ -392,12 +392,12 @@ If this vulnerability was inserted by a Fetcher, changes may be lost when the Fe ###### Example Request ```json -PUT http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities/CVE-2014-9471 +PUT http://localhost:6060/v1/namespaces/gAAAAABXTQKgma_TLKq0wr1D6wVB507N3fi9wsWMUypYOSXTDVxQ8OR5L5S6PqZ9Wh0IzWojnVmlspyTL4cyjytyra7U9vAHMA==/vulnerabilities/CVE-2014-9471 { "Vulnerability": { "Name": "CVE-2014-9471", - "NamespaceName": "debian:8", + "Namespace": {"Name": "debian", "Version": "8"}, "Link": "https://security-tracker.debian.org/tracker/CVE-2014-9471", "Description": "The parse_datetime function in GNU coreutils allows remote attackers to cause a denial of service (crash) or possibly execute arbitrary code via a crafted date string, as demonstrated by the \"--date=TZ=\"123\"345\" @1\" string to the touch or date command.", "Severity": "Low", @@ -422,7 +422,7 @@ Server: clair { "Vulnerability": { "Name": "CVE-2014-9471", - "NamespaceName": "debian:8", + "Namespace": {"Name": "debian", "Version": "8"}, "Link": "https://security-tracker.debian.org/tracker/CVE-2014-9471", "Description": "The parse_datetime function in GNU coreutils allows remote attackers to cause a denial of service (crash) or possibly execute arbitrary code via a crafted date string, as demonstrated by the \"--date=TZ=\"123\"345\" @1\" string to the touch or date command.", "Severity": "Low", @@ -439,7 +439,7 @@ Server: clair ``` -#### DELETE /namespaces/`:nsName`/vulnerabilities/`:vulnName` +#### DELETE /namespaces/`:nsID`/vulnerabilities/`:vulnName` ###### Description @@ -449,7 +449,7 @@ If this vulnerability was inserted by a Fetcher, it may be re-inserted when the ###### Example Request ```json -GET http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities/CVE-2014-9471 HTTP/1.1 +GET http://localhost:6060/v1/namespaces/gAAAAABXTQKgma_TLKq0wr1D6wVB507N3fi9wsWMUypYOSXTDVxQ8OR5L5S6PqZ9Wh0IzWojnVmlspyTL4cyjytyra7U9vAHMA==/vulnerabilities/CVE-2014-9471 HTTP/1.1 ``` ###### Example Response @@ -461,7 +461,7 @@ Server: clair ## Fixes -#### GET /namespaces/`:nsName`/vulnerabilities/`:vulnName`/fixes +#### GET /namespaces/`:nsID`/vulnerabilities/`:vulnName`/fixes ###### Description @@ -470,7 +470,7 @@ The GET route for the Fixes resource displays the list of Features that fix the ###### Example Request ```json -GET http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities/CVE-2014-9471/fixes HTTP/1.1 +GET http://localhost:6060/v1/namespaces/gAAAAABXTQKgma_TLKq0wr1D6wVB507N3fi9wsWMUypYOSXTDVxQ8OR5L5S6PqZ9Wh0IzWojnVmlspyTL4cyjytyra7U9vAHMA==/vulnerabilities/CVE-2014-9471/fixes HTTP/1.1 ``` ###### Example Response @@ -484,14 +484,14 @@ Server: clair "Features": [ { "Name": "coreutils", - "NamespaceName": "debian:8", + "Namespace": {"Name": "debian", "Version": "8"}, "Version": "8.23-1" } ] } ``` -#### PUT /namespaces/`:nsName`/vulnerabilities/`:vulnName`/fixes/`:featureName` +#### PUT /namespaces/`:nsID`/vulnerabilities/`:vulnName`/fixes/`:featureName` ###### Description @@ -500,12 +500,12 @@ The PUT route for the Fixes resource updates a Feature that is the fix for a giv ###### Example Request ```json -PUT http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities/CVE-2014-9471/fixes/coreutils HTTP/1.1 +PUT http://localhost:6060/v1/namespaces/gAAAAABXTQKgma_TLKq0wr1D6wVB507N3fi9wsWMUypYOSXTDVxQ8OR5L5S6PqZ9Wh0IzWojnVmlspyTL4cyjytyra7U9vAHMA==/vulnerabilities/CVE-2014-9471/fixes/coreutils HTTP/1.1 { "Feature": { "Name": "coreutils", - "NamespaceName": "debian:8", + "Namespace": {"Name": "debian", "Version": "8"}, "Version": "4.24-9" } } @@ -520,13 +520,13 @@ Server: clair { "Feature": { "Name": "coreutils", - "NamespaceName": "debian:8", + "Namespace": {"Name": "debian", "Version": "8"}, "Version": "4.24-9" } } ``` -#### DELETE /namespaces/`:nsName`/vulnerabilities/`:vulnName`/fixes/`:featureName` +#### DELETE /namespaces/`:nsID`/vulnerabilities/`:vulnName`/fixes/`:featureName` ###### Description @@ -535,7 +535,7 @@ The DELETE route for the Fixes resource removes a Feature as fix for the given V ###### Example Request ```json -DELETE http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities/CVE-2014-9471/fixes/coreutils +DELETE http://localhost:6060/v1/namespaces/gAAAAABXTQKgma_TLKq0wr1D6wVB507N3fi9wsWMUypYOSXTDVxQ8OR5L5S6PqZ9Wh0IzWojnVmlspyTL4cyjytyra7U9vAHMA==/vulnerabilities/CVE-2014-9471/fixes/coreutils ``` ###### Example Response @@ -585,13 +585,13 @@ Server: clair "New": { "Vulnerability": { "Name": "CVE-TEST", - "NamespaceName": "debian:8", + "Namespace": {"Name": "debian", "Version": "8"}, "Description": "New CVE", "Severity": "Low", "FixedIn": [ { "Name": "grep", - "NamespaceName": "debian:8", + "Namespace": {"Name": "debian", "Version": "8"}, "Version": "2.25" } ] @@ -604,7 +604,7 @@ Server: clair "Old": { "Vulnerability": { "Name": "CVE-TEST", - "NamespaceName": "debian:8", + "Namespace": {"Name": "debian", "Version": "8"}, "Description": "New CVE", "Severity": "Low", "FixedIn": [] diff --git a/api/v1/models.go b/api/v1/models.go index 0d6a6383..43031642 100644 --- a/api/v1/models.go +++ b/api/v1/models.go @@ -35,7 +35,7 @@ type Error struct { type Layer struct { Name string `json:"Name,omitempty"` - NamespaceName string `json:"NamespaceName,omitempty"` + Namespaces []Namespace `json:"Namespaces,omitempty"` Path string `json:"Path,omitempty"` Headers map[string]string `json:"Headers,omitempty"` ParentName string `json:"ParentName,omitempty"` @@ -54,27 +54,27 @@ func LayerFromDatabaseModel(dbLayer database.Layer, withFeatures, withVulnerabil layer.ParentName = dbLayer.Parent.Name } - if dbLayer.Namespace != nil { - layer.NamespaceName = dbLayer.Namespace.Name + for _, namespace := range dbLayer.Namespaces { + layer.Namespaces = append(layer.Namespaces, Namespace{Name: namespace.Name, Version: namespace.Version.String()}) } if withFeatures || withVulnerabilities && dbLayer.Features != nil { for _, dbFeatureVersion := range dbLayer.Features { feature := Feature{ - Name: dbFeatureVersion.Feature.Name, - NamespaceName: dbFeatureVersion.Feature.Namespace.Name, - Version: dbFeatureVersion.Version.String(), - AddedBy: dbFeatureVersion.AddedBy.Name, + Name: dbFeatureVersion.Feature.Name, + Namespace: Namespace{Name: dbFeatureVersion.Feature.Namespace.Name, Version: dbFeatureVersion.Feature.Namespace.Version.String()}, + Version: dbFeatureVersion.Version.String(), + AddedBy: dbFeatureVersion.AddedBy.Name, } for _, dbVuln := range dbFeatureVersion.AffectedBy { vuln := Vulnerability{ - Name: dbVuln.Name, - NamespaceName: dbVuln.Namespace.Name, - Description: dbVuln.Description, - Link: dbVuln.Link, - Severity: string(dbVuln.Severity), - Metadata: dbVuln.Metadata, + Name: dbVuln.Name, + Namespace: Namespace{Name: dbVuln.Namespace.Name, Version: dbVuln.Namespace.Version.String()}, + Description: dbVuln.Description, + Link: dbVuln.Link, + Severity: string(dbVuln.Severity), + Metadata: dbVuln.Metadata, } if dbVuln.FixedBy != types.MaxVersion { @@ -90,18 +90,20 @@ func LayerFromDatabaseModel(dbLayer database.Layer, withFeatures, withVulnerabil } type Namespace struct { - Name string `json:"Name,omitempty"` + ID string `json:"ID,omitempty"` + Name string `json:"Name,omitempty"` + Version string `json:"Version,omitempty"` } type Vulnerability struct { - Name string `json:"Name,omitempty"` - NamespaceName string `json:"NamespaceName,omitempty"` - Description string `json:"Description,omitempty"` - Link string `json:"Link,omitempty"` - Severity string `json:"Severity,omitempty"` - Metadata map[string]interface{} `json:"Metadata,omitempty"` - FixedBy string `json:"FixedBy,omitempty"` - FixedIn []Feature `json:"FixedIn,omitempty"` + Name string `json:"Name,omitempty"` + Namespace Namespace `json:"Namespace,omitempty"` + Description string `json:"Description,omitempty"` + Link string `json:"Link,omitempty"` + Severity string `json:"Severity,omitempty"` + Metadata map[string]interface{} `json:"Metadata,omitempty"` + FixedBy string `json:"FixedBy,omitempty"` + FixedIn []Feature `json:"FixedIn,omitempty"` } func (v Vulnerability) DatabaseModel() (database.Vulnerability, error) { @@ -110,6 +112,11 @@ func (v Vulnerability) DatabaseModel() (database.Vulnerability, error) { return database.Vulnerability{}, errors.New("Invalid severity") } + version, err := types.NewVersion(v.Namespace.Version) + if err != nil { + return database.Vulnerability{}, errors.New("Invalid namespace version") + } + var dbFeatures []database.FeatureVersion for _, feature := range v.FixedIn { dbFeature, err := feature.DatabaseModel() @@ -122,7 +129,7 @@ func (v Vulnerability) DatabaseModel() (database.Vulnerability, error) { return database.Vulnerability{ Name: v.Name, - Namespace: database.Namespace{Name: v.NamespaceName}, + Namespace: database.Namespace{Name: v.Namespace.Name, Version: version}, Description: v.Description, Link: v.Link, Severity: severity, @@ -133,12 +140,12 @@ func (v Vulnerability) DatabaseModel() (database.Vulnerability, error) { func VulnerabilityFromDatabaseModel(dbVuln database.Vulnerability, withFixedIn bool) Vulnerability { vuln := Vulnerability{ - Name: dbVuln.Name, - NamespaceName: dbVuln.Namespace.Name, - Description: dbVuln.Description, - Link: dbVuln.Link, - Severity: string(dbVuln.Severity), - Metadata: dbVuln.Metadata, + Name: dbVuln.Name, + Namespace: Namespace{Name: dbVuln.Namespace.Name, Version: dbVuln.Namespace.Version.String()}, + Description: dbVuln.Description, + Link: dbVuln.Link, + Severity: string(dbVuln.Severity), + Metadata: dbVuln.Metadata, } if withFixedIn { @@ -152,7 +159,7 @@ func VulnerabilityFromDatabaseModel(dbVuln database.Vulnerability, withFixedIn b type Feature struct { Name string `json:"Name,omitempty"` - NamespaceName string `json:"NamespaceName,omitempty"` + Namespace Namespace `json:"Namespace,omitempty"` Version string `json:"Version,omitempty"` Vulnerabilities []Vulnerability `json:"Vulnerabilities,omitempty"` AddedBy string `json:"AddedBy,omitempty"` @@ -165,10 +172,10 @@ func FeatureFromDatabaseModel(dbFeatureVersion database.FeatureVersion) Feature } return Feature{ - Name: dbFeatureVersion.Feature.Name, - NamespaceName: dbFeatureVersion.Feature.Namespace.Name, - Version: versionStr, - AddedBy: dbFeatureVersion.AddedBy.Name, + Name: dbFeatureVersion.Feature.Name, + Namespace: Namespace{Name: dbFeatureVersion.Feature.Namespace.Name, Version: dbFeatureVersion.Feature.Namespace.Version.String()}, + Version: versionStr, + AddedBy: dbFeatureVersion.AddedBy.Name, } } @@ -184,10 +191,15 @@ func (f Feature) DatabaseModel() (database.FeatureVersion, error) { } } + nsVersion, err := types.NewVersion(f.Namespace.Version) + if err != nil { + return database.FeatureVersion{}, err + } + return database.FeatureVersion{ Feature: database.Feature{ Name: f.Name, - Namespace: database.Namespace{Name: f.NamespaceName}, + Namespace: database.Namespace{Name: f.Namespace.Name, Version: nsVersion}, }, Version: version, }, nil diff --git a/api/v1/router.go b/api/v1/router.go index 5a3d640e..e4ac5ac4 100644 --- a/api/v1/router.go +++ b/api/v1/router.go @@ -34,16 +34,16 @@ 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)) - router.DELETE("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName", context.HTTPHandler(deleteVulnerability, ctx)) + router.GET("/namespaces/:namespaceID/vulnerabilities", context.HTTPHandler(getVulnerabilities, ctx)) + router.POST("/namespaces/:namespaceID/vulnerabilities", context.HTTPHandler(postVulnerability, ctx)) + router.GET("/namespaces/:namespaceID/vulnerabilities/:vulnerabilityName", context.HTTPHandler(getVulnerability, ctx)) + router.PUT("/namespaces/:namespaceID/vulnerabilities/:vulnerabilityName", context.HTTPHandler(putVulnerability, ctx)) + router.DELETE("/namespaces/:namespaceID/vulnerabilities/:vulnerabilityName", context.HTTPHandler(deleteVulnerability, ctx)) // Fixes - router.GET("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes", context.HTTPHandler(getFixes, ctx)) - router.PUT("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes/:fixName", context.HTTPHandler(putFix, ctx)) - router.DELETE("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes/:fixName", context.HTTPHandler(deleteFix, ctx)) + router.GET("/namespaces/:namespaceID/vulnerabilities/:vulnerabilityName/fixes", context.HTTPHandler(getFixes, ctx)) + router.PUT("/namespaces/:namespaceID/vulnerabilities/:vulnerabilityName/fixes/:fixName", context.HTTPHandler(putFix, ctx)) + router.DELETE("/namespaces/:namespaceID/vulnerabilities/:vulnerabilityName/fixes/:fixName", context.HTTPHandler(deleteFix, ctx)) // Notifications router.GET("/notifications/:notificationName", context.HTTPHandler(getNotification, ctx)) diff --git a/api/v1/routes.go b/api/v1/routes.go index 482840bf..b1f6a30d 100644 --- a/api/v1/routes.go +++ b/api/v1/routes.go @@ -179,7 +179,12 @@ func getNamespaces(w http.ResponseWriter, r *http.Request, p httprouter.Params, } var namespaces []Namespace for _, dbNamespace := range dbNamespaces { - namespaces = append(namespaces, Namespace{Name: dbNamespace.Name}) + if namespaceIDBytes, err := tokenMarshal(dbNamespace.ID, ctx.Config.PaginationKey); err != nil { + writeResponse(w, r, http.StatusInternalServerError, NamespaceEnvelope{Error: &Error{err.Error()}}) + return getNamespacesRoute, http.StatusInternalServerError + } else { + namespaces = append(namespaces, Namespace{ID: string(namespaceIDBytes), Name: dbNamespace.Name, Version: dbNamespace.Version.String()}) + } } writeResponse(w, r, http.StatusOK, NamespaceEnvelope{Namespaces: &namespaces}) @@ -213,13 +218,13 @@ func getVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Par } } - namespace := p.ByName("namespaceName") - if namespace == "" { - writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"namespace should not be empty"}}) + namespaceID := 0 + if err = tokenUnmarshal(p.ByName("namespaceID"), ctx.Config.PaginationKey, &namespaceID); err != nil { + writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"invalid namespace id format: " + err.Error()}}) return getNotificationRoute, http.StatusBadRequest } - dbVulns, nextPage, err := ctx.Store.ListVulnerabilities(namespace, limit, page) + dbVulns, nextPage, err := ctx.Store.ListVulnerabilities(namespaceID, limit, page) if err == cerrors.ErrNotFound { writeResponse(w, r, http.StatusNotFound, VulnerabilityEnvelope{Error: &Error{err.Error()}}) return getVulnerabilityRoute, http.StatusNotFound @@ -286,7 +291,13 @@ func postVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Para func getVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) { _, withFixedIn := r.URL.Query()["fixedIn"] - dbVuln, err := ctx.Store.FindVulnerability(p.ByName("namespaceName"), p.ByName("vulnerabilityName")) + namespaceID := 0 + if err := tokenUnmarshal(p.ByName("namespaceID"), ctx.Config.PaginationKey, &namespaceID); err != nil { + writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"invalid namespace id format: " + err.Error()}}) + return getVulnerabilityRoute, http.StatusBadRequest + } + + dbVuln, err := ctx.Store.FindVulnerability(namespaceID, p.ByName("vulnerabilityName")) if err == cerrors.ErrNotFound { writeResponse(w, r, http.StatusNotFound, VulnerabilityEnvelope{Error: &Error{err.Error()}}) return getVulnerabilityRoute, http.StatusNotFound @@ -302,6 +313,12 @@ func getVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Param } func putVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) { + namespaceID := 0 + if err := tokenUnmarshal(p.ByName("namespaceID"), ctx.Config.PaginationKey, &namespaceID); err != nil { + writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"invalid namespace id format: " + err.Error()}}) + return putVulnerabilityRoute, http.StatusBadRequest + } + request := VulnerabilityEnvelope{} err := decodeJSON(r, &request) if err != nil { @@ -325,7 +342,7 @@ func putVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Param return putVulnerabilityRoute, http.StatusBadRequest } - vuln.Namespace.Name = p.ByName("namespaceName") + vuln.Namespace.ID = namespaceID vuln.Name = p.ByName("vulnerabilityName") err = ctx.Store.InsertVulnerabilities([]database.Vulnerability{vuln}, true) @@ -345,7 +362,13 @@ func putVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Param } func deleteVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) { - err := ctx.Store.DeleteVulnerability(p.ByName("namespaceName"), p.ByName("vulnerabilityName")) + namespaceID := 0 + if err := tokenUnmarshal(p.ByName("namespaceID"), ctx.Config.PaginationKey, &namespaceID); err != nil { + writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"invalid namespace id format: " + err.Error()}}) + return deleteVulnerabilityRoute, http.StatusBadRequest + } + + err := ctx.Store.DeleteVulnerability(namespaceID, p.ByName("vulnerabilityName")) if err == cerrors.ErrNotFound { writeResponse(w, r, http.StatusNotFound, VulnerabilityEnvelope{Error: &Error{err.Error()}}) return deleteVulnerabilityRoute, http.StatusNotFound @@ -359,7 +382,13 @@ func deleteVulnerability(w http.ResponseWriter, r *http.Request, p httprouter.Pa } func getFixes(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) { - dbVuln, err := ctx.Store.FindVulnerability(p.ByName("namespaceName"), p.ByName("vulnerabilityName")) + namespaceID := 0 + if err := tokenUnmarshal(p.ByName("namespaceID"), ctx.Config.PaginationKey, &namespaceID); err != nil { + writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"invalid namespace id format: " + err.Error()}}) + return getFixesRoute, http.StatusBadRequest + } + + dbVuln, err := ctx.Store.FindVulnerability(namespaceID, p.ByName("vulnerabilityName")) if err == cerrors.ErrNotFound { writeResponse(w, r, http.StatusNotFound, FeatureEnvelope{Error: &Error{err.Error()}}) return getFixesRoute, http.StatusNotFound @@ -374,6 +403,12 @@ func getFixes(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx * } func putFix(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) { + namespaceID := 0 + if err := tokenUnmarshal(p.ByName("namespaceID"), ctx.Config.PaginationKey, &namespaceID); err != nil { + writeResponse(w, r, http.StatusBadRequest, FeatureEnvelope{Error: &Error{"invalid namespace id format: " + err.Error()}}) + return putFixRoute, http.StatusBadRequest + } + request := FeatureEnvelope{} err := decodeJSON(r, &request) if err != nil { @@ -397,7 +432,7 @@ func putFix(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *co return putFixRoute, http.StatusBadRequest } - err = ctx.Store.InsertVulnerabilityFixes(p.ByName("vulnerabilityNamespace"), p.ByName("vulnerabilityName"), []database.FeatureVersion{dbFix}) + err = ctx.Store.InsertVulnerabilityFixes(namespaceID, p.ByName("vulnerabilityName"), []database.FeatureVersion{dbFix}) if err != nil { switch err.(type) { case *cerrors.ErrBadRequest: @@ -418,7 +453,13 @@ func putFix(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *co } func deleteFix(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) { - err := ctx.Store.DeleteVulnerabilityFix(p.ByName("vulnerabilityNamespace"), p.ByName("vulnerabilityName"), p.ByName("fixName")) + namespaceID := 0 + if err := tokenUnmarshal(p.ByName("namespaceID"), ctx.Config.PaginationKey, &namespaceID); err != nil { + writeResponse(w, r, http.StatusBadRequest, FeatureEnvelope{Error: &Error{"invalid namespace id format: " + err.Error()}}) + return deleteFixRoute, http.StatusBadRequest + } + + err := ctx.Store.DeleteVulnerabilityFix(namespaceID, p.ByName("vulnerabilityName"), p.ByName("fixName")) if err == cerrors.ErrNotFound { writeResponse(w, r, http.StatusNotFound, FeatureEnvelope{Error: &Error{err.Error()}}) return deleteFixRoute, http.StatusNotFound diff --git a/database/database.go b/database/database.go index ee2e59bc..ab4a5a0a 100644 --- a/database/database.go +++ b/database/database.go @@ -67,6 +67,8 @@ type Datastore interface { // # Namespace // ListNamespaces returns the entire list of known Namespaces. ListNamespaces() ([]Namespace, error) + // GetNamespaceID returns the id by name and version. + GetNamespaceID(Namespace) (int, error) // # Layer // InsertLayer stores a Layer in the database. @@ -93,7 +95,7 @@ type Datastore interface { // 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(namespace Namespace, limit int, page int) ([]Vulnerability, int, error) + ListVulnerabilities(namespaceID int, 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. @@ -110,22 +112,22 @@ type Datastore interface { InsertVulnerabilities(vulnerabilities []Vulnerability, createNotification bool) error // FindVulnerability retrieves a Vulnerability from the database, including the FixedIn list. - FindVulnerability(namespace Namespace, name string) (Vulnerability, error) + FindVulnerability(namespaceID int, name string) (Vulnerability, error) // DeleteVulnerability removes a Vulnerability from the database. // It has to create a Notification that will contain the old Vulnerability. - DeleteVulnerability(namespace Namespace, name string) error + DeleteVulnerability(namespaceID int, name string) error // InsertVulnerabilityFixes adds new FixedIn Feature or update the Versions of existing ones to // the specified Vulnerability in the database. // It has has to create a Notification that will contain the old and the updated Vulnerability. - InsertVulnerabilityFixes(vulnerabilityNamespace Namespace, vulnerabilityName string, fixes []FeatureVersion) error + InsertVulnerabilityFixes(vulnerabilityNamespaceID int, vulnerabilityName string, fixes []FeatureVersion) error // DeleteVulnerabilityFix removes a FixedIn Feature from the specified Vulnerability in the // database. It can be used to store the fact that a Vulnerability no longer affects the given // Feature in any Version. // It has has to create a Notification that will contain the old and the updated Vulnerability. - DeleteVulnerabilityFix(vulnerabilityNamespace Namespace, vulnerabilityName, featureName string) error + DeleteVulnerabilityFix(vulnerabilityNamespaceID int, vulnerabilityName, featureName string) error // # Notification // GetAvailableNotification returns the Name, Created, Notified and Deleted fields of a diff --git a/database/mock.go b/database/mock.go index 6d3c1076..6d5f503f 100644 --- a/database/mock.go +++ b/database/mock.go @@ -20,15 +20,16 @@ import "time" // The default behavior of each method is to simply panic. type MockDatastore struct { FctListNamespaces func() ([]Namespace, error) + FctGetNamespaceID func(namespace Namespace) (int, error) FctInsertLayer func(Layer) error FctFindLayer func(name string, withFeatures, withVulnerabilities bool) (Layer, error) FctDeleteLayer func(name string) error - FctListVulnerabilities func(namespace Namespace, limit int, page int) ([]Vulnerability, int, error) + FctListVulnerabilities func(namespaceID int, limit int, page int) ([]Vulnerability, int, error) FctInsertVulnerabilities func(vulnerabilities []Vulnerability, createNotification bool) error - FctFindVulnerability func(namespace Namespace, name string) (Vulnerability, error) - FctDeleteVulnerability func(namespace Namespace, name string) error - FctInsertVulnerabilityFixes func(vulnerabilityNamespace Namespace, vulnerabilityName string, fixes []FeatureVersion) error - FctDeleteVulnerabilityFix func(vulnerabilityNamespace Namespace, vulnerabilityName, featureName string) error + FctFindVulnerability func(namespaceID int, name string) (Vulnerability, error) + FctDeleteVulnerability func(namespaceID int, name string) error + FctInsertVulnerabilityFixes func(vulnerabilityNamespaceID int, vulnerabilityName string, fixes []FeatureVersion) error + FctDeleteVulnerabilityFix func(vulnerabilityNamespaceID int, vulnerabilityName, featureName string) error FctGetAvailableNotification func(renotifyInterval time.Duration) (VulnerabilityNotification, error) FctGetNotification func(name string, limit int, page VulnerabilityNotificationPageNumber) (VulnerabilityNotification, VulnerabilityNotificationPageNumber, error) FctSetNotificationNotified func(name string) error @@ -49,6 +50,13 @@ func (mds *MockDatastore) ListNamespaces() ([]Namespace, error) { panic("required mock function not implemented") } +func (mds *MockDatastore) GetNamespaceID(namespace Namespace) (int, error) { + if mds.FctGetNamespaceID != nil { + return mds.FctGetNamespaceID(namespace) + } + panic("required mock function not implemented") +} + func (mds *MockDatastore) InsertLayer(layer Layer) error { if mds.FctInsertLayer != nil { return mds.FctInsertLayer(layer) @@ -70,9 +78,9 @@ func (mds *MockDatastore) DeleteLayer(name string) error { panic("required mock function not implemented") } -func (mds *MockDatastore) ListVulnerabilities(namespace Namespace, limit int, page int) ([]Vulnerability, int, error) { +func (mds *MockDatastore) ListVulnerabilities(namespaceID int, limit int, page int) ([]Vulnerability, int, error) { if mds.FctListVulnerabilities != nil { - return mds.FctListVulnerabilities(namespace, limit, page) + return mds.FctListVulnerabilities(namespaceID, limit, page) } panic("required mock function not implemented") } @@ -84,30 +92,30 @@ func (mds *MockDatastore) InsertVulnerabilities(vulnerabilities []Vulnerability, panic("required mock function not implemented") } -func (mds *MockDatastore) FindVulnerability(namespace Namespace, name string) (Vulnerability, error) { +func (mds *MockDatastore) FindVulnerability(namespaceID int, name string) (Vulnerability, error) { if mds.FctFindVulnerability != nil { - return mds.FctFindVulnerability(namespace, name) + return mds.FctFindVulnerability(namespaceID, name) } panic("required mock function not implemented") } -func (mds *MockDatastore) DeleteVulnerability(namespace Namespace, name string) error { +func (mds *MockDatastore) DeleteVulnerability(namespaceID int, name string) error { if mds.FctDeleteVulnerability != nil { - return mds.FctDeleteVulnerability(namespace, name) + return mds.FctDeleteVulnerability(namespaceID, name) } panic("required mock function not implemented") } -func (mds *MockDatastore) InsertVulnerabilityFixes(vulnerabilityNamespace Namespace, vulnerabilityName string, fixes []FeatureVersion) error { +func (mds *MockDatastore) InsertVulnerabilityFixes(vulnerabilityNamespaceID int, vulnerabilityName string, fixes []FeatureVersion) error { if mds.FctInsertVulnerabilityFixes != nil { - return mds.FctInsertVulnerabilityFixes(vulnerabilityNamespace, vulnerabilityName, fixes) + return mds.FctInsertVulnerabilityFixes(vulnerabilityNamespaceID, vulnerabilityName, fixes) } panic("required mock function not implemented") } -func (mds *MockDatastore) DeleteVulnerabilityFix(vulnerabilityNamespace Namespace, vulnerabilityName, featureName string) error { +func (mds *MockDatastore) DeleteVulnerabilityFix(vulnerabilityNamespaceID int, vulnerabilityName, featureName string) error { if mds.FctDeleteVulnerabilityFix != nil { - return mds.FctDeleteVulnerabilityFix(vulnerabilityNamespace, vulnerabilityName, featureName) + return mds.FctDeleteVulnerabilityFix(vulnerabilityNamespaceID, vulnerabilityName, featureName) } panic("required mock function not implemented") } diff --git a/database/namespace_mapping.go b/database/namespace_mapping.go index 97ef21d9..83e5f58b 100644 --- a/database/namespace_mapping.go +++ b/database/namespace_mapping.go @@ -21,13 +21,13 @@ var DebianReleasesMapping = map[string]string{ "wheezy": "7", "jessie": "8", "stretch": "9", - "sid": "unstable", + "sid": "10", // Class names "oldstable": "7", "stable": "8", "testing": "9", - "unstable": "unstable", + "unstable": "10", } // UbuntuReleasesMapping translates Ubuntu code names to version numbers diff --git a/database/pgsql/layer.go b/database/pgsql/layer.go index c2386c58..4a22eadd 100644 --- a/database/pgsql/layer.go +++ b/database/pgsql/layer.go @@ -52,28 +52,6 @@ func (pgSQL *pgSQL) FindLayer(name string, withFeatures, withVulnerabilities boo Model: database.Model{ID: int(parentID.Int64)}, Name: parentName.String, } - - // Find its parent's namespaces - t = time.Now() - rows, err := pgSQL.Query(searchLayerNamespace, parentID) - observeQueryTime("FindLayer", "searchParentLayerNamespace", t) - if err != nil { - return layer, handleError("searchLayerNamespace", err) - } - defer rows.Close() - - for rows.Next() { - var pn database.Namespace - - err = rows.Scan(&pn.ID, &pn.Name, &pn.Version) - if err != nil { - return layer, handleError("searchLayerNamespace.Scan()", err) - } - layer.Parent.Namespaces = append(layer.Parent.Namespaces, pn) - } - if err = rows.Err(); err != nil { - return layer, handleError("searchLayerNamespace.Rows()", err) - } } // Find its namespaces diff --git a/database/pgsql/layer_test.go b/database/pgsql/layer_test.go index b1d005df..da9d3451 100644 --- a/database/pgsql/layer_test.go +++ b/database/pgsql/layer_test.go @@ -208,23 +208,29 @@ func testInsertLayerTree(t *testing.T, datastore database.Datastore) { }, // This layer changes the namespace and adds Features. { - Name: "TestInsertLayer3", - Parent: &database.Layer{Name: "TestInsertLayer2"}, + Name: "TestInsertLayer3", + Parent: &database.Layer{Name: "TestInsertLayer2", + Namespaces: []database.Namespace{testInsertLayerNamespace1}, + }, Namespaces: []database.Namespace{testInsertLayerNamespace2}, Features: []database.FeatureVersion{f1, f2, f3}, }, // This layer covers the case where the last layer doesn't provide any new Feature. { - Name: "TestInsertLayer4a", - Parent: &database.Layer{Name: "TestInsertLayer3"}, + Name: "TestInsertLayer4a", + Parent: &database.Layer{Name: "TestInsertLayer3", + Namespaces: []database.Namespace{testInsertLayerNamespace1, testInsertLayerNamespace2}, + }, Features: []database.FeatureVersion{f1, f2, f3}, }, // This layer covers the case where the last layer provides Features. // It also modifies the Namespace ("upgrade") but keeps some Features not upgraded, their // Namespaces should then remain unchanged. { - Name: "TestInsertLayer4b", - Parent: &database.Layer{Name: "TestInsertLayer3"}, + Name: "TestInsertLayer4b", + Parent: &database.Layer{Name: "TestInsertLayer3", + Namespaces: []database.Namespace{testInsertLayerNamespace1, testInsertLayerNamespace2}, + }, Namespaces: []database.Namespace{testInsertLayerNamespace3}, Features: []database.FeatureVersion{ // Deletes TestInsertLayerFeature1. @@ -295,6 +301,8 @@ func testInsertLayerUpdate(t *testing.T, datastore database.Datastore) { } l3, _ := datastore.FindLayer("TestInsertLayer3", true, false) + l3Parent, _ := datastore.FindLayer(l3.Parent.Name, false, false) + l3.Parent = &l3Parent l3u := database.Layer{ Name: l3.Name, Parent: l3.Parent, @@ -304,7 +312,7 @@ func testInsertLayerUpdate(t *testing.T, datastore database.Datastore) { l4u := database.Layer{ Name: "TestInsertLayer4", - Parent: &database.Layer{Name: "TestInsertLayer3"}, + Parent: &database.Layer{Name: "TestInsertLayer3", Namespaces: l3.Namespaces}, Features: []database.FeatureVersion{f7}, EngineVersion: 2, } diff --git a/database/pgsql/migrations/20160524162211_multiple_namespace.sql b/database/pgsql/migrations/20160524162211_multiple_namespace.sql index ae1f32d2..cb247c44 100644 --- a/database/pgsql/migrations/20160524162211_multiple_namespace.sql +++ b/database/pgsql/migrations/20160524162211_multiple_namespace.sql @@ -1,4 +1,4 @@ --- Copyright 2015 clair authors +-- Copyright 2016 clair authors -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ CREATE INDEX ON LayerNamespace (layer_id, namespace_id); INSERT INTO LayerNamespace(layer_id, namespace_id) SELECT id, namespace_id - from Layer; + from Layer WHERE namespace_id IS NOT NULL; -- ----------------------------------------------------- -- Layer table diff --git a/database/pgsql/namespace.go b/database/pgsql/namespace.go index d48c4910..befddc6e 100644 --- a/database/pgsql/namespace.go +++ b/database/pgsql/namespace.go @@ -50,6 +50,30 @@ func (pgSQL *pgSQL) insertNamespace(namespace database.Namespace) (int, error) { return id, nil } +func (pgSQL *pgSQL) GetNamespaceID(namespace database.Namespace) (int, error) { + if pgSQL.cache != nil { + promCacheQueriesTotal.WithLabelValues("namespace").Inc() + if id, found := pgSQL.cache.Get("namespace:" + namespace.Name + ":" + namespace.Version.String()); found { + promCacheHitsTotal.WithLabelValues("namespace").Inc() + return id.(int), nil + } + } + + defer observeQueryTime("getNamespaceID", "all", time.Now()) + + var id int + err := pgSQL.QueryRow(getNamespaceID, namespace.Name, namespace.Version.String()).Scan(&id) + if err != nil { + return 0, handleError("getNamespaceID", err) + } + + if pgSQL.cache != nil { + pgSQL.cache.Add("namespace:"+namespace.Name+":"+namespace.Version.String(), id) + } + + return id, nil +} + func (pgSQL *pgSQL) ListNamespaces() (namespaces []database.Namespace, err error) { rows, err := pgSQL.Query(listNamespace) if err != nil { diff --git a/database/pgsql/notification_test.go b/database/pgsql/notification_test.go index cd45fb3b..4d0e8875 100644 --- a/database/pgsql/notification_test.go +++ b/database/pgsql/notification_test.go @@ -118,7 +118,8 @@ func TestNotification(t *testing.T) { assert.Nil(t, datastore.insertVulnerability(v1, false, true)) // Get the notification associated to the previously inserted vulnerability. - notification, err := datastore.GetAvailableNotification(time.Second) + var notification database.VulnerabilityNotification + notification, err = datastore.GetAvailableNotification(time.Second) if assert.Nil(t, err) && assert.NotEmpty(t, notification.Name) { // Verify the renotify behaviour. @@ -206,8 +207,12 @@ func TestNotification(t *testing.T) { } } + namespaceID, err := datastore.GetNamespaceID(database.Namespace{ + Name: "TestNotificationNamespace", + Version: types.NewVersionUnsafe("1.0"), + }) // Delete a vulnerability and verify the notification. - if assert.Nil(t, datastore.DeleteVulnerability(v1b.Namespace, v1b.Name)) { + if assert.Nil(t, err) && assert.Nil(t, datastore.DeleteVulnerability(namespaceID, v1b.Name)) { notification, err = datastore.GetAvailableNotification(time.Second) assert.Nil(t, err) assert.NotEmpty(t, notification.Name) diff --git a/database/pgsql/queries.go b/database/pgsql/queries.go index 6f291a2b..a95bbca7 100644 --- a/database/pgsql/queries.go +++ b/database/pgsql/queries.go @@ -37,9 +37,9 @@ const ( SELECT id FROM Namespace WHERE name = $1 AND version = $2 UNION SELECT id FROM new_namespace` - - searchNamespace = `SELECT id FROM Namespace WHERE name = $1 AND version = $2` - listNamespace = `SELECT id, name, version FROM Namespace` + getNamespaceID = `SELECT id FROM Namespace WHERE name = $1 AND version = $2` + listNamespace = `SELECT id, name, version FROM Namespace where version IS NOT NULL` + getNamespaceByID = `SELECT name, version FROM Namespace WHERE id = $1` // feature.go soiFeature = ` @@ -161,10 +161,10 @@ const ( searchVulnerabilityBase = ` SELECT v.id, v.name, n.id, n.name, n.version, v.description, v.link, v.severity, v.metadata FROM Vulnerability v JOIN Namespace n ON v.namespace_id = n.id` - searchVulnerabilityForUpdate = ` FOR UPDATE OF v` - searchVulnerabilityByNamespaceAndName = ` WHERE n.name = $1 AND n.version = $2 AND v.name = $3 AND v.deleted_at IS NULL` - searchVulnerabilityByID = ` WHERE v.id = $1` - searchVulnerabilityByNamespaceID = ` WHERE n.id = $1 AND v.deleted_at IS NULL + searchVulnerabilityForUpdate = ` FOR UPDATE OF v` + searchVulnerabilityByNamespaceIDAndName = ` WHERE n.id = $1 AND v.name = $2 AND v.deleted_at IS NULL` + searchVulnerabilityByID = ` WHERE v.id = $1` + searchVulnerabilityByNamespaceID = ` WHERE n.id = $1 AND v.deleted_at IS NULL AND v.id >= $2 ORDER BY v.id LIMIT $3` @@ -189,8 +189,8 @@ const ( removeVulnerability = ` UPDATE Vulnerability SET deleted_at = CURRENT_TIMESTAMP - WHERE namespace_id = (SELECT id FROM Namespace WHERE name = $1 AND version = $2) - AND name = $3 + WHERE namespace_id = $1 + AND name = $2 AND deleted_at IS NULL RETURNING id` diff --git a/database/pgsql/vulnerability.go b/database/pgsql/vulnerability.go index 5a1eb32b..14564065 100644 --- a/database/pgsql/vulnerability.go +++ b/database/pgsql/vulnerability.go @@ -28,21 +28,12 @@ import ( "github.com/guregu/null/zero" ) -func (pgSQL *pgSQL) ListVulnerabilities(namespace database.Namespace, limit int, startID int) ([]database.Vulnerability, int, error) { +func (pgSQL *pgSQL) ListVulnerabilities(namespaceID int, limit int, startID int) ([]database.Vulnerability, int, error) { defer observeQueryTime("listVulnerabilities", "all", time.Now()) - // Query Namespace. - var id int - err := pgSQL.QueryRow(searchNamespace, namespace.Name, &namespace.Version).Scan(&id) - if err != nil { - return nil, -1, handleError("searchNamespace", err) - } else if id == 0 { - return nil, -1, cerrors.ErrNotFound - } - // Query. query := searchVulnerabilityBase + searchVulnerabilityByNamespaceID - rows, err := pgSQL.Query(query, id, startID, limit+1) + rows, err := pgSQL.Query(query, namespaceID, startID, limit+1) if err != nil { return nil, -1, handleError("searchVulnerabilityByNamespaceID", err) } @@ -84,21 +75,21 @@ func (pgSQL *pgSQL) ListVulnerabilities(namespace database.Namespace, limit int, return vulns, nextID, nil } -func (pgSQL *pgSQL) FindVulnerability(namespace database.Namespace, name string) (database.Vulnerability, error) { - return findVulnerability(pgSQL, namespace, name, false) +func (pgSQL *pgSQL) FindVulnerability(namespaceID int, name string) (database.Vulnerability, error) { + return findVulnerability(pgSQL, namespaceID, name, false) } -func findVulnerability(queryer Queryer, namespace database.Namespace, name string, forUpdate bool) (database.Vulnerability, error) { +func findVulnerability(queryer Queryer, namespaceID int, name string, forUpdate bool) (database.Vulnerability, error) { defer observeQueryTime("findVulnerability", "all", time.Now()) - queryName := "searchVulnerabilityBase+searchVulnerabilityByNamespaceAndName" - query := searchVulnerabilityBase + searchVulnerabilityByNamespaceAndName + queryName := "searchVulnerabilityBase+searchVulnerabilityByNamespaceIDAndName" + query := searchVulnerabilityBase + searchVulnerabilityByNamespaceIDAndName if forUpdate { queryName = queryName + "+searchVulnerabilityForUpdate" query = query + searchVulnerabilityForUpdate } - return scanVulnerability(queryer, queryName, queryer.QueryRow(query, namespace.Name, &namespace.Version, name)) + return scanVulnerability(queryer, queryName, queryer.QueryRow(query, namespaceID, name)) } func (pgSQL *pgSQL) findVulnerabilityByIDWithDeleted(id int) (database.Vulnerability, error) { @@ -195,9 +186,24 @@ func (pgSQL *pgSQL) insertVulnerability(vulnerability database.Vulnerability, on tf := time.Now() // Verify parameters - if vulnerability.Name == "" || vulnerability.Namespace.IsEmpty() { - return cerrors.NewBadRequestError("insertVulnerability needs at least the Name and the Namespace") + if vulnerability.Name == "" { + return cerrors.NewBadRequestError("insertVulnerability needs at least the Name") } + + if vulnerability.Namespace.ID <= 0 { + // Find or insert Vulnerability's Namespace. + namespaceID, err := pgSQL.insertNamespace(vulnerability.Namespace) + if err != nil { + return err + } + vulnerability.Namespace.ID = namespaceID + } else if vulnerability.Namespace.IsEmpty() { + err := pgSQL.QueryRow(getNamespaceByID, vulnerability.Namespace.ID).Scan(&vulnerability.Namespace.Name, &vulnerability.Namespace.Version) + if err != nil { + return cerrors.NewBadRequestError("could not get the related namespace") + } + } + if !onlyFixedIn && !vulnerability.Severity.IsValid() { msg := fmt.Sprintf("could not insert a vulnerability that has an invalid Severity: %s", vulnerability.Severity) log.Warning(msg) @@ -209,8 +215,7 @@ func (pgSQL *pgSQL) insertVulnerability(vulnerability database.Vulnerability, on if fifv.Feature.Namespace.IsEmpty() { // As there is no Namespace on that FixedIn FeatureVersion, set it to the Vulnerability's // Namespace. - fifv.Feature.Namespace.Name = vulnerability.Namespace.Name - fifv.Feature.Namespace.Version = vulnerability.Namespace.Version + fifv.Feature.Namespace = vulnerability.Namespace } else if !fifv.Feature.Namespace.Equal(vulnerability.Namespace) { msg := "could not insert an invalid vulnerability that contains FixedIn FeatureVersion that are not in the same namespace as the Vulnerability" log.Warning(msg) @@ -229,7 +234,7 @@ func (pgSQL *pgSQL) insertVulnerability(vulnerability database.Vulnerability, on } // Find existing vulnerability and its Vulnerability_FixedIn_Features (for update). - existingVulnerability, err := findVulnerability(tx, vulnerability.Namespace, vulnerability.Name, true) + existingVulnerability, err := findVulnerability(tx, vulnerability.Namespace.ID, vulnerability.Name, true) if err != nil && err != cerrors.ErrNotFound { tx.Rollback() return err @@ -267,7 +272,7 @@ func (pgSQL *pgSQL) insertVulnerability(vulnerability database.Vulnerability, on } // Mark the old vulnerability as non latest. - _, err = tx.Exec(removeVulnerability, vulnerability.Namespace.Name, &vulnerability.Namespace.Version, vulnerability.Name) + _, err = tx.Exec(removeVulnerability, vulnerability.Namespace.ID, vulnerability.Name) if err != nil { tx.Rollback() return handleError("removeVulnerability", err) @@ -284,16 +289,10 @@ func (pgSQL *pgSQL) insertVulnerability(vulnerability database.Vulnerability, on vulnerability.FixedIn = fixedIn } - // Find or insert Vulnerability's Namespace. - namespaceID, err := pgSQL.insertNamespace(vulnerability.Namespace) - if err != nil { - return err - } - // Insert vulnerability. err = tx.QueryRow( insertVulnerability, - namespaceID, + vulnerability.Namespace.ID, vulnerability.Name, vulnerability.Description, vulnerability.Link, @@ -500,38 +499,41 @@ func linkVulnerabilityToFeatureVersions(tx *sql.Tx, fixedInID, vulnerabilityID, return nil } -func (pgSQL *pgSQL) InsertVulnerabilityFixes(vulnerabilityNamespace database.Namespace, vulnerabilityName string, fixes []database.FeatureVersion) error { +func (pgSQL *pgSQL) InsertVulnerabilityFixes(vulnerabilityNamespaceID int, vulnerabilityName string, fixes []database.FeatureVersion) error { defer observeQueryTime("InsertVulnerabilityFixes", "all", time.Now()) + var vns database.Namespace + if err := pgSQL.QueryRow(getNamespaceByID, vulnerabilityNamespaceID).Scan(&vns.Name, &vns.Version); err != nil { + return handleError("getNamespaceByID", err) + } + vns.ID = vulnerabilityNamespaceID + v := database.Vulnerability{ - Name: vulnerabilityName, - Namespace: database.Namespace{ - Name: vulnerabilityNamespace.Name, - Version: vulnerabilityNamespace.Version, - }, - FixedIn: fixes, + Name: vulnerabilityName, + Namespace: vns, + FixedIn: fixes, } return pgSQL.insertVulnerability(v, true, true) } -func (pgSQL *pgSQL) DeleteVulnerabilityFix(vulnerabilityNamespace database.Namespace, vulnerabilityName, featureName string) error { +func (pgSQL *pgSQL) DeleteVulnerabilityFix(vulnerabilityNamespaceID int, vulnerabilityName, featureName string) error { defer observeQueryTime("DeleteVulnerabilityFix", "all", time.Now()) + var vns database.Namespace + if err := pgSQL.QueryRow(getNamespaceByID, vulnerabilityNamespaceID).Scan(&vns.Name, &vns.Version); err != nil { + return handleError("getNamespaceByID", err) + } + vns.ID = vulnerabilityNamespaceID + v := database.Vulnerability{ - Name: vulnerabilityName, - Namespace: database.Namespace{ - Name: vulnerabilityNamespace.Name, - Version: vulnerabilityNamespace.Version, - }, + Name: vulnerabilityName, + Namespace: vns, FixedIn: []database.FeatureVersion{ { Feature: database.Feature{ - Name: featureName, - Namespace: database.Namespace{ - Name: vulnerabilityNamespace.Name, - Version: vulnerabilityNamespace.Version, - }, + Name: featureName, + Namespace: vns, }, Version: types.MinVersion, }, @@ -541,7 +543,7 @@ func (pgSQL *pgSQL) DeleteVulnerabilityFix(vulnerabilityNamespace database.Names return pgSQL.insertVulnerability(v, true, true) } -func (pgSQL *pgSQL) DeleteVulnerability(namespace database.Namespace, name string) error { +func (pgSQL *pgSQL) DeleteVulnerability(namespaceID int, name string) error { defer observeQueryTime("DeleteVulnerability", "all", time.Now()) // Begin transaction. @@ -552,7 +554,7 @@ func (pgSQL *pgSQL) DeleteVulnerability(namespace database.Namespace, name strin } var vulnerabilityID int - err = tx.QueryRow(removeVulnerability, namespace.Name, &namespace.Version, name).Scan(&vulnerabilityID) + err = tx.QueryRow(removeVulnerability, namespaceID, name).Scan(&vulnerabilityID) if err != nil { tx.Rollback() return handleError("removeVulnerability", err) diff --git a/database/pgsql/vulnerability_test.go b/database/pgsql/vulnerability_test.go index ae92ea7f..193347c3 100644 --- a/database/pgsql/vulnerability_test.go +++ b/database/pgsql/vulnerability_test.go @@ -37,8 +37,12 @@ func TestFindVulnerability(t *testing.T) { Name: "debian", Version: types.NewVersionUnsafe("7"), } + var testExistNamespaceID int + testExistNamespaceID, err = datastore.GetNamespaceID(testExistNamespace) + assert.Nil(t, err) + // Find a vulnerability that does not exist. - _, err = datastore.FindVulnerability(database.Namespace{}, "") + _, err = datastore.FindVulnerability(-1, "") assert.Equal(t, cerrors.ErrNotFound, err) // Find a normal vulnerability. @@ -60,7 +64,7 @@ func TestFindVulnerability(t *testing.T) { }, } - v1f, err := datastore.FindVulnerability(testExistNamespace, "CVE-OPENSSL-1-DEB7") + v1f, err := datastore.FindVulnerability(testExistNamespaceID, "CVE-OPENSSL-1-DEB7") if assert.Nil(t, err) { equalsVuln(t, &v1, &v1f) } @@ -73,7 +77,7 @@ func TestFindVulnerability(t *testing.T) { Severity: types.Unknown, } - v2f, err := datastore.FindVulnerability(testExistNamespace, "CVE-NOPE") + v2f, err := datastore.FindVulnerability(testExistNamespaceID, "CVE-NOPE") if assert.Nil(t, err) { equalsVuln(t, &v2, &v2f) } @@ -91,20 +95,28 @@ func TestDeleteVulnerability(t *testing.T) { Name: "debian", Version: types.NewVersionUnsafe("7"), } + var testExistNamespaceID int + testExistNamespaceID, err = datastore.GetNamespaceID(testExistNamespace) + assert.Nil(t, err) + testNonExistNamespace := database.Namespace{ Name: "TestDeleteVulnerabilityNamespace", Version: types.NewVersionUnsafe("1.0"), } - // Delete non-existing Vulnerability. - err = datastore.DeleteVulnerability(testNonExistNamespace, "CVE-OPENSSL-1-DEB7") + var testNonExistNamespaceID int + testNonExistNamespaceID, err = datastore.GetNamespaceID(testNonExistNamespace) assert.Equal(t, cerrors.ErrNotFound, err) - err = datastore.DeleteVulnerability(testExistNamespace, "TestDeleteVulnerabilityVulnerability1") + + // Delete non-existing Vulnerability. + err = datastore.DeleteVulnerability(testNonExistNamespaceID, "CVE-OPENSSL-1-DEB7") + assert.Equal(t, cerrors.ErrNotFound, err) + err = datastore.DeleteVulnerability(testExistNamespaceID, "TestDeleteVulnerabilityVulnerability1") assert.Equal(t, cerrors.ErrNotFound, err) // Delete Vulnerability. - err = datastore.DeleteVulnerability(testExistNamespace, "CVE-OPENSSL-1-DEB7") + err = datastore.DeleteVulnerability(testExistNamespaceID, "CVE-OPENSSL-1-DEB7") if assert.Nil(t, err) { - _, err := datastore.FindVulnerability(testExistNamespace, "CVE-OPENSSL-1-DEB7") + _, err := datastore.FindVulnerability(testExistNamespaceID, "CVE-OPENSSL-1-DEB7") assert.Equal(t, cerrors.ErrNotFound, err) } } @@ -228,7 +240,11 @@ func TestInsertVulnerability(t *testing.T) { } err = datastore.InsertVulnerabilities([]database.Vulnerability{v1}, true) if assert.Nil(t, err) { - v1f, err := datastore.FindVulnerability(n1, v1.Name) + var n1ID int + var v1f database.Vulnerability + n1ID, err := datastore.GetNamespaceID(n1) + assert.Nil(t, err) + v1f, err = datastore.FindVulnerability(n1ID, v1.Name) if assert.Nil(t, err) { equalsVuln(t, &v1, &v1f) } @@ -244,7 +260,11 @@ func TestInsertVulnerability(t *testing.T) { err = datastore.InsertVulnerabilities([]database.Vulnerability{v1}, true) if assert.Nil(t, err) { - v1f, err := datastore.FindVulnerability(n1, v1.Name) + var n1ID int + var v1f database.Vulnerability + n1ID, err := datastore.GetNamespaceID(n1) + assert.Nil(t, err) + v1f, err = datastore.FindVulnerability(n1ID, v1.Name) if assert.Nil(t, err) { // We already had f1 before the update. // Add it to the struct for comparison. diff --git a/updater/fetchers/debian/debian.go b/updater/fetchers/debian/debian.go index 590819e1..5ed71c79 100644 --- a/updater/fetchers/debian/debian.go +++ b/updater/fetchers/debian/debian.go @@ -169,6 +169,7 @@ func parseDebianJSON(data *jsonData) (vulnerabilities []database.Vulnerability, // Determine the version of the package the vulnerability affects. var version types.Version + var nsVersion types.Version var err error if releaseNode.FixedVersion == "0" { // This means that the package is not affected by this vulnerability. @@ -187,13 +188,19 @@ func parseDebianJSON(data *jsonData) (vulnerabilities []database.Vulnerability, } } + nsVersion, err = types.NewVersion(database.DebianReleasesMapping[releaseName]) + if err != nil { + log.Warningf("could not parse namespace version '%s': %s. skipping", database.DebianReleasesMapping[releaseName], err.Error()) + continue + } + // Create and add the feature version. pkg := database.FeatureVersion{ Feature: database.Feature{ Name: pkgName, Namespace: database.Namespace{ Name: "debian", - Version: types.NewVersionUnsafe(database.DebianReleasesMapping[releaseName]), + Version: nsVersion, }, }, Version: version, diff --git a/updater/fetchers/debian/debian_test.go b/updater/fetchers/debian/debian_test.go index 4bfd83b0..a5162eb9 100644 --- a/updater/fetchers/debian/debian_test.go +++ b/updater/fetchers/debian/debian_test.go @@ -48,7 +48,7 @@ func TestDebianParser(t *testing.T) { }, { Feature: database.Feature{ - Namespace: database.Namespace{Name: "debian", Version: types.NewVersionUnsafe("unstable")}, + Namespace: database.Namespace{Name: "debian", Version: types.NewVersionUnsafe("10")}, Name: "aptdaemon", }, @@ -74,7 +74,7 @@ func TestDebianParser(t *testing.T) { }, { Feature: database.Feature{ - Namespace: database.Namespace{Name: "debian", Version: types.NewVersionUnsafe("unstable")}, + Namespace: database.Namespace{Name: "debian", Version: types.NewVersionUnsafe("10")}, Name: "aptdaemon", }, Version: types.NewVersionUnsafe("0.7.0"), diff --git a/updater/fetchers/rhel/rhel.go b/updater/fetchers/rhel/rhel.go index 099a5c42..23fc20f2 100644 --- a/updater/fetchers/rhel/rhel.go +++ b/updater/fetchers/rhel/rhel.go @@ -291,8 +291,13 @@ func toFeatureVersions(criteria criteria) []database.FeatureVersion { } if osVersion > firstConsideredRHEL { + nsVersion, err := types.NewVersion(strconv.Itoa(osVersion)) + if err != nil { + log.Warningf("could not parse namespace version '%s': %s. skipping", strconv.Itoa(osVersion), err.Error()) + continue + } featureVersion.Feature.Namespace.Name = "centos" - featureVersion.Feature.Namespace.Version = types.NewVersionUnsafe(strconv.Itoa(osVersion)) + featureVersion.Feature.Namespace.Version = nsVersion } else { continue } diff --git a/updater/fetchers/ubuntu/ubuntu.go b/updater/fetchers/ubuntu/ubuntu.go index fd3dc6f1..834416e8 100644 --- a/updater/fetchers/ubuntu/ubuntu.go +++ b/updater/fetchers/ubuntu/ubuntu.go @@ -373,10 +373,15 @@ func parseUbuntuCVE(fileContent io.Reader) (vulnerability database.Vulnerability continue } + nsVersion, err := types.NewVersion(database.UbuntuReleasesMapping[md["release"]]) + if err != nil { + log.Warningf("could not parse namespace version '%s': %s. skipping", database.UbuntuReleasesMapping[md["release"]], err) + } + // Create and add the new package. featureVersion := database.FeatureVersion{ Feature: database.Feature{ - Namespace: database.Namespace{Name: "ubuntu", Version: types.NewVersionUnsafe(database.UbuntuReleasesMapping[md["release"]])}, + Namespace: database.Namespace{Name: "ubuntu", Version: nsVersion}, Name: md["package"], }, Version: version, diff --git a/worker/detectors/features.go b/worker/detectors/features.go index 164d0de2..2ebaa6f5 100644 --- a/worker/detectors/features.go +++ b/worker/detectors/features.go @@ -68,8 +68,7 @@ func DetectFeatures(data map[string][]byte, namespaces []database.Namespace) ([] } // Ensure that every feature has a Namespace associated for i := 0; i < len(pkgs); i++ { - pkgs[i].Feature.Namespace.Name = namespace.Name - pkgs[i].Feature.Namespace.Version = namespace.Version + pkgs[i].Feature.Namespace = namespace } packages = append(packages, pkgs...) break diff --git a/worker/detectors/namespace/aptsources/aptsources.go b/worker/detectors/namespace/aptsources/aptsources.go index 2d9ebbb1..927a931a 100644 --- a/worker/detectors/namespace/aptsources/aptsources.go +++ b/worker/detectors/namespace/aptsources/aptsources.go @@ -76,7 +76,11 @@ func (detector *AptSourcesNamespaceDetector) Detect(data map[string][]byte) *dat } if OS != "" && version != "" { - return &database.Namespace{Name: OS, Version: types.NewVersionUnsafe(version)} + if nsVersion, err := types.NewVersion(version); err != nil { + return nil + } else { + return &database.Namespace{Name: OS, Version: nsVersion} + } } return nil } diff --git a/worker/detectors/namespace/aptsources/aptsources_test.go b/worker/detectors/namespace/aptsources/aptsources_test.go index 8d83aa3f..f90ae755 100644 --- a/worker/detectors/namespace/aptsources/aptsources_test.go +++ b/worker/detectors/namespace/aptsources/aptsources_test.go @@ -24,7 +24,7 @@ import ( var aptSourcesOSTests = []namespace.NamespaceTest{ { - ExpectedNamespace: database.Namespace{Name: "debian", Version: types.NewVersionUnsafe("unstable")}, + ExpectedNamespace: database.Namespace{Name: "debian", Version: types.NewVersionUnsafe("10")}, Data: map[string][]byte{ "etc/os-release": []byte( `PRETTY_NAME="Debian GNU/Linux stretch/sid" diff --git a/worker/detectors/namespace/lsbrelease/lsbrelease.go b/worker/detectors/namespace/lsbrelease/lsbrelease.go index b4ac9602..b18c4031 100644 --- a/worker/detectors/namespace/lsbrelease/lsbrelease.go +++ b/worker/detectors/namespace/lsbrelease/lsbrelease.go @@ -71,7 +71,11 @@ func (detector *LsbReleaseNamespaceDetector) Detect(data map[string][]byte) *dat } if OS != "" && version != "" { - return &database.Namespace{Name: OS, Version: types.NewVersionUnsafe(version)} + if nsVersion, err := types.NewVersion(version); err != nil { + return nil + } else { + return &database.Namespace{Name: OS, Version: nsVersion} + } } return nil } diff --git a/worker/detectors/namespace/osrelease/osrelease.go b/worker/detectors/namespace/osrelease/osrelease.go index 03c9e2d9..76d2e02f 100644 --- a/worker/detectors/namespace/osrelease/osrelease.go +++ b/worker/detectors/namespace/osrelease/osrelease.go @@ -66,7 +66,11 @@ func (detector *OsReleaseNamespaceDetector) Detect(data map[string][]byte) *data } if OS != "" && version != "" { - return &database.Namespace{Name: OS, Version: types.NewVersionUnsafe(version)} + if nsVersion, err := types.NewVersion(version); err != nil { + return nil + } else { + return &database.Namespace{Name: OS, Version: nsVersion} + } } return nil } diff --git a/worker/detectors/namespace/redhatrelease/redhatrelease.go b/worker/detectors/namespace/redhatrelease/redhatrelease.go index 6eb8e9f3..93a5d390 100644 --- a/worker/detectors/namespace/redhatrelease/redhatrelease.go +++ b/worker/detectors/namespace/redhatrelease/redhatrelease.go @@ -47,7 +47,11 @@ func (detector *RedhatReleaseNamespaceDetector) Detect(data map[string][]byte) * r := redhatReleaseRegexp.FindStringSubmatch(string(f)) if len(r) == 4 { - return &database.Namespace{Name: strings.ToLower(r[1]), Version: types.NewVersionUnsafe(r[3])} + if nsVersion, err := types.NewVersion(r[3]); err != nil { + return nil + } else { + return &database.Namespace{Name: strings.ToLower(r[1]), Version: nsVersion} + } } }