// Copyright 2015 clair authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package pgsql import ( "database/sql" "encoding/json" "fmt" "reflect" "time" "github.com/coreos/clair/database" "github.com/coreos/clair/utils" cerrors "github.com/coreos/clair/utils/errors" "github.com/coreos/clair/utils/types" "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) } func findVulnerability(queryer Queryer, namespaceName, name string, forUpdate bool) (database.Vulnerability, error) { defer observeQueryTime("findVulnerability", "all", time.Now()) queryName := "searchVulnerabilityBase+searchVulnerabilityByNamespaceAndName" query := searchVulnerabilityBase + searchVulnerabilityByNamespaceAndName if forUpdate { queryName = queryName + "+searchVulnerabilityForUpdate" query = query + searchVulnerabilityForUpdate } return scanVulnerability(queryer, queryName, queryer.QueryRow(query, namespaceName, name)) } func (pgSQL *pgSQL) findVulnerabilityByIDWithDeleted(id int) (database.Vulnerability, error) { defer observeQueryTime("findVulnerabilityByIDWithDeleted", "all", time.Now()) queryName := "searchVulnerabilityBase+searchVulnerabilityByID" query := searchVulnerabilityBase + searchVulnerabilityByID return scanVulnerability(pgSQL, queryName, pgSQL.QueryRow(query, id)) } func scanVulnerability(queryer Queryer, queryName string, vulnerabilityRow *sql.Row) (database.Vulnerability, error) { var vulnerability database.Vulnerability err := vulnerabilityRow.Scan( &vulnerability.ID, &vulnerability.Name, &vulnerability.Namespace.ID, &vulnerability.Namespace.Name, &vulnerability.Description, &vulnerability.Link, &vulnerability.Severity, &vulnerability.Metadata, ) if err != nil { return vulnerability, handleError(queryName+".Scan()", err) } if vulnerability.ID == 0 { return vulnerability, cerrors.ErrNotFound } // Query the FixedIn FeatureVersion now. rows, err := queryer.Query(searchVulnerabilityFixedIn, vulnerability.ID) if err != nil { return vulnerability, handleError("searchVulnerabilityFixedIn.Scan()", err) } defer rows.Close() for rows.Next() { var featureVersionID zero.Int var featureVersionVersion zero.String var featureVersionFeatureName zero.String err := rows.Scan( &featureVersionVersion, &featureVersionID, &featureVersionFeatureName, ) if err != nil { return vulnerability, handleError("searchVulnerabilityFixedIn.Scan()", err) } if !featureVersionID.IsZero() { // Note that the ID we fill in featureVersion is actually a Feature ID, and not // a FeatureVersion ID. featureVersion := database.FeatureVersion{ Model: database.Model{ID: int(featureVersionID.Int64)}, Feature: database.Feature{ Model: database.Model{ID: int(featureVersionID.Int64)}, Namespace: vulnerability.Namespace, Name: featureVersionFeatureName.String, }, Version: types.NewVersionUnsafe(featureVersionVersion.String), } vulnerability.FixedIn = append(vulnerability.FixedIn, featureVersion) } } if err := rows.Err(); err != nil { return vulnerability, handleError("searchVulnerabilityFixedIn.Rows()", err) } return vulnerability, nil } // FixedIn.Namespace are not necessary, they are overwritten by the vuln. // By setting the fixed version to minVersion, we can say that the vuln does'nt affect anymore. func (pgSQL *pgSQL) InsertVulnerabilities(vulnerabilities []database.Vulnerability, generateNotifications bool) error { for _, vulnerability := range vulnerabilities { err := pgSQL.insertVulnerability(vulnerability, false, generateNotifications) if err != nil { fmt.Printf("%#v\n", vulnerability) return err } } return nil } func (pgSQL *pgSQL) insertVulnerability(vulnerability database.Vulnerability, onlyFixedIn, generateNotification bool) error { tf := time.Now() // Verify parameters if vulnerability.Name == "" || vulnerability.Namespace.Name == "" { return cerrors.NewBadRequestError("insertVulnerability needs at least the Name and the 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) return cerrors.NewBadRequestError(msg) } for i := 0; i < len(vulnerability.FixedIn); i++ { fifv := &vulnerability.FixedIn[i] if fifv.Feature.Namespace.Name == "" { // As there is no Namespace on that FixedIn FeatureVersion, set it to the Vulnerability's // Namespace. fifv.Feature.Namespace.Name = vulnerability.Namespace.Name } else if fifv.Feature.Namespace.Name != vulnerability.Namespace.Name { msg := "could not insert an invalid vulnerability that contains FixedIn FeatureVersion that are not in the same namespace as the Vulnerability" log.Warning(msg) return cerrors.NewBadRequestError(msg) } } // We do `defer observeQueryTime` here because we don't want to observe invalid vulnerabilities. defer observeQueryTime("insertVulnerability", "all", tf) // Begin transaction. tx, err := pgSQL.Begin() if err != nil { tx.Rollback() return handleError("insertVulnerability.Begin()", err) } // Find existing vulnerability and its Vulnerability_FixedIn_Features (for update). existingVulnerability, err := findVulnerability(tx, vulnerability.Namespace.Name, vulnerability.Name, true) if err != nil && err != cerrors.ErrNotFound { tx.Rollback() return err } if onlyFixedIn { // Because this call tries to update FixedIn FeatureVersion, import all other data from the // existing one. if existingVulnerability.ID == 0 { return cerrors.ErrNotFound } fixedIn := vulnerability.FixedIn vulnerability = existingVulnerability vulnerability.FixedIn = fixedIn } if existingVulnerability.ID != 0 { updateMetadata := vulnerability.Description != existingVulnerability.Description || vulnerability.Link != existingVulnerability.Link || vulnerability.Severity != existingVulnerability.Severity || !reflect.DeepEqual(castMetadata(vulnerability.Metadata), existingVulnerability.Metadata) // Construct the entire list of FixedIn FeatureVersion, by using the // the FixedIn list of the old vulnerability. // // TODO(Quentin-M): We could use !updateFixedIn to just copy FixedIn/Affects rows from the // existing vulnerability in order to make metadata updates much faster. var updateFixedIn bool vulnerability.FixedIn, updateFixedIn = applyFixedInDiff(existingVulnerability.FixedIn, vulnerability.FixedIn) if !updateMetadata && !updateFixedIn { tx.Commit() return nil } // Mark the old vulnerability as non latest. _, err = tx.Exec(removeVulnerability, vulnerability.Namespace.Name, vulnerability.Name) if err != nil { tx.Rollback() return handleError("removeVulnerability", err) } } else { // The vulnerability is new, we don't want to have any types.MinVersion as they are only used // for diffing existing vulnerabilities. var fixedIn []database.FeatureVersion for _, fv := range vulnerability.FixedIn { if fv.Version != types.MinVersion { fixedIn = append(fixedIn, fv) } } 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.Name, vulnerability.Description, vulnerability.Link, &vulnerability.Severity, &vulnerability.Metadata, ).Scan(&vulnerability.ID) if err != nil { tx.Rollback() return handleError("insertVulnerability", err) } // Update Vulnerability_FixedIn_Feature and Vulnerability_Affects_FeatureVersion now. err = pgSQL.insertVulnerabilityFixedInFeatureVersions(tx, vulnerability.ID, vulnerability.FixedIn) if err != nil { tx.Rollback() return err } // Create a notification. if generateNotification { err = createNotification(tx, existingVulnerability.ID, vulnerability.ID) if err != nil { return err } } // Commit transaction. err = tx.Commit() if err != nil { tx.Rollback() return handleError("insertVulnerability.Commit()", err) } return nil } // castMetadata marshals the given database.MetadataMap and unmarshals it again to make sure that // everything has the interface{} type. // It is required when comparing crafted MetadataMap against MetadataMap that we get from the // database. func castMetadata(m database.MetadataMap) database.MetadataMap { c := make(database.MetadataMap) j, _ := json.Marshal(m) json.Unmarshal(j, &c) return c } // applyFixedInDiff applies a FeatureVersion diff on a FeatureVersion list and returns the result. func applyFixedInDiff(currentList, diff []database.FeatureVersion) ([]database.FeatureVersion, bool) { currentMap, currentNames := createFeatureVersionNameMap(currentList) diffMap, diffNames := createFeatureVersionNameMap(diff) addedNames := utils.CompareStringLists(diffNames, currentNames) inBothNames := utils.CompareStringListsInBoth(diffNames, currentNames) different := false for _, name := range addedNames { if diffMap[name].Version == types.MinVersion { // MinVersion only makes sense when a Feature is already fixed in some version, // in which case we would be in the "inBothNames". continue } currentMap[name] = diffMap[name] different = true } for _, name := range inBothNames { fv := diffMap[name] if fv.Version == types.MinVersion { // MinVersion means that the Feature doesn't affect the Vulnerability anymore. delete(currentMap, name) different = true } else if fv.Version != currentMap[name].Version { // The version got updated. currentMap[name] = diffMap[name] different = true } } // Convert currentMap to a slice and return it. var newList []database.FeatureVersion for _, fv := range currentMap { newList = append(newList, fv) } return newList, different } func createFeatureVersionNameMap(features []database.FeatureVersion) (map[string]database.FeatureVersion, []string) { m := make(map[string]database.FeatureVersion, 0) s := make([]string, 0, len(features)) for i := 0; i < len(features); i++ { featureVersion := features[i] m[featureVersion.Feature.Name] = featureVersion s = append(s, featureVersion.Feature.Name) } return m, s } // insertVulnerabilityFixedInFeatureVersions populates Vulnerability_FixedIn_Feature for the given // vulnerability with the specified database.FeatureVersion list and uses // linkVulnerabilityToFeatureVersions to propagate the changes on Vulnerability_FixedIn_Feature to // Vulnerability_Affects_FeatureVersion. func (pgSQL *pgSQL) insertVulnerabilityFixedInFeatureVersions(tx *sql.Tx, vulnerabilityID int, fixedIn []database.FeatureVersion) error { defer observeQueryTime("insertVulnerabilityFixedInFeatureVersions", "all", time.Now()) // Insert or find the Features. // TODO(Quentin-M): Batch me. var err error var features []*database.Feature for i := 0; i < len(fixedIn); i++ { features = append(features, &fixedIn[i].Feature) } for _, feature := range features { if feature.ID == 0 { if feature.ID, err = pgSQL.insertFeature(*feature); err != nil { return err } } } // Lock Vulnerability_Affects_FeatureVersion exclusively. // We want to prevent InsertFeatureVersion to modify it. promConcurrentLockVAFV.Inc() defer promConcurrentLockVAFV.Dec() t := time.Now() _, err = tx.Exec(lockVulnerabilityAffects) observeQueryTime("insertVulnerability", "lock", t) if err != nil { tx.Rollback() return handleError("insertVulnerability.lockVulnerabilityAffects", err) } for _, fv := range fixedIn { var fixedInID int var created bool // Find or create entry in Vulnerability_FixedIn_Feature. err = tx.QueryRow( soiVulnerabilityFixedInFeature, vulnerabilityID, fv.Feature.ID, &fv.Version, ).Scan(&created, &fixedInID) if err != nil { return handleError("insertVulnerabilityFixedInFeature", err) } if !created { // The relationship between the feature and the vulnerability already // existed, no need to update Vulnerability_Affects_FeatureVersion. continue } // Insert Vulnerability_Affects_FeatureVersion. err = linkVulnerabilityToFeatureVersions(tx, fixedInID, vulnerabilityID, fv.Feature.ID, fv.Version) if err != nil { return err } } return nil } func linkVulnerabilityToFeatureVersions(tx *sql.Tx, fixedInID, vulnerabilityID, featureID int, fixedInVersion types.Version) error { // Find every FeatureVersions of the Feature that the vulnerability affects. // TODO(Quentin-M): LIMIT rows, err := tx.Query(searchFeatureVersionByFeature, featureID) if err != nil { return handleError("searchFeatureVersionByFeature", err) } defer rows.Close() var affecteds []database.FeatureVersion for rows.Next() { var affected database.FeatureVersion err := rows.Scan(&affected.ID, &affected.Version) if err != nil { return handleError("searchFeatureVersionByFeature.Scan()", err) } if affected.Version.Compare(fixedInVersion) < 0 { // The version of the FeatureVersion is lower than the fixed version of this vulnerability, // thus, this FeatureVersion is affected by it. affecteds = append(affecteds, affected) } } if err = rows.Err(); err != nil { return handleError("searchFeatureVersionByFeature.Rows()", err) } rows.Close() // Insert into Vulnerability_Affects_FeatureVersion. for _, affected := range affecteds { // TODO(Quentin-M): Batch me. _, err := tx.Exec(insertVulnerabilityAffectsFeatureVersion, vulnerabilityID, affected.ID, fixedInID) if err != nil { return handleError("insertVulnerabilityAffectsFeatureVersion", err) } } return nil } func (pgSQL *pgSQL) InsertVulnerabilityFixes(vulnerabilityNamespace, vulnerabilityName string, fixes []database.FeatureVersion) error { defer observeQueryTime("InsertVulnerabilityFixes", "all", time.Now()) v := database.Vulnerability{ Name: vulnerabilityName, Namespace: database.Namespace{ Name: vulnerabilityNamespace, }, FixedIn: fixes, } return pgSQL.insertVulnerability(v, true, true) } func (pgSQL *pgSQL) DeleteVulnerabilityFix(vulnerabilityNamespace, vulnerabilityName, featureName string) error { defer observeQueryTime("DeleteVulnerabilityFix", "all", time.Now()) v := database.Vulnerability{ Name: vulnerabilityName, Namespace: database.Namespace{ Name: vulnerabilityNamespace, }, FixedIn: []database.FeatureVersion{ { Feature: database.Feature{ Name: featureName, Namespace: database.Namespace{ Name: vulnerabilityNamespace, }, }, Version: types.MinVersion, }, }, } return pgSQL.insertVulnerability(v, true, true) } func (pgSQL *pgSQL) DeleteVulnerability(namespaceName, name string) error { defer observeQueryTime("DeleteVulnerability", "all", time.Now()) // Begin transaction. tx, err := pgSQL.Begin() if err != nil { tx.Rollback() return handleError("DeleteVulnerability.Begin()", err) } var vulnerabilityID int err = tx.QueryRow(removeVulnerability, namespaceName, name).Scan(&vulnerabilityID) if err != nil { tx.Rollback() return handleError("removeVulnerability", err) } // Create a notification. err = createNotification(tx, vulnerabilityID, 0) if err != nil { return err } // Commit transaction. err = tx.Commit() if err != nil { tx.Rollback() return handleError("DeleteVulnerability.Commit()", err) } return nil }