// 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) FindVulnerability(namespaceName, name string) (database.Vulnerability, error) { defer observeQueryTime("FindVulnerability", "all", time.Now()) vulnerability := database.Vulnerability{ Name: name, Namespace: database.Namespace{ Name: namespaceName, }, } // Find Vulnerability. rows, err := pgSQL.Query(getQuery("f_vulnerability"), namespaceName, name) if err != nil { return vulnerability, handleError("f_vulnerability", err) } defer rows.Close() // Iterate to scan the Vulnerability and its FixedIn FeatureVersions. for rows.Next() { var featureVersionID zero.Int var featureVersionVersion zero.String var featureVersionFeatureName zero.String err := rows.Scan(&vulnerability.ID, &vulnerability.Namespace.ID, &vulnerability.Description, &vulnerability.Link, &vulnerability.Severity, &vulnerability.Metadata, &featureVersionVersion, &featureVersionID, &featureVersionFeatureName) if err != nil { return vulnerability, handleError("f_vulnerability.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("s_featureversions_vulnerabilities.Rows()", err) } if vulnerability.ID == 0 { return vulnerability, cerrors.ErrNotFound } 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) error { for _, vulnerability := range vulnerabilities { err := pgSQL.insertVulnerability(vulnerability) if err != nil { fmt.Printf("%#v\n", vulnerability) return err } } return nil } func (pgSQL *pgSQL) insertVulnerability(vulnerability database.Vulnerability) error { tf := time.Now() // Verify parameters if vulnerability.Name == "" || len(vulnerability.FixedIn) == 0 || vulnerability.Namespace.Name == "" || !vulnerability.Severity.IsValid() { log.Warning("could not insert an invalid vulnerability") return cerrors.NewBadRequestError("could not insert an invalid vulnerability") } for _, fixedInFeatureVersion := range vulnerability.FixedIn { if fixedInFeatureVersion.Feature.Namespace.Name != "" && fixedInFeatureVersion.Feature.Namespace.Name != vulnerability.Namespace.Name { msg := "could not insert an invalid vulnerability: FixedIn FeatureVersion must be in the " + "same namespace as the Vulnerability" log.Warning(msg) return cerrors.NewBadRequestError(msg) } } // Find or insert Vulnerability's Namespace. namespaceID, err := pgSQL.insertNamespace(vulnerability.Namespace) if err != nil { return err } // Find vulnerability and its Vulnerability_FixedIn_Features. existingVulnerability, err := pgSQL.FindVulnerability(vulnerability.Namespace.Name, vulnerability.Name) if err != nil && err != cerrors.ErrNotFound { return err } // Compute new/updated FixedIn FeatureVersions. var newFixedInFeatureVersions []database.FeatureVersion var updatedFixedInFeatureVersions []database.FeatureVersion if existingVulnerability.ID == 0 { newFixedInFeatureVersions = vulnerability.FixedIn } else { newFixedInFeatureVersions, updatedFixedInFeatureVersions = diffFixedIn(vulnerability, existingVulnerability) if vulnerability.Description == existingVulnerability.Description && vulnerability.Link == existingVulnerability.Link && vulnerability.Severity == existingVulnerability.Severity && reflect.DeepEqual(castMetadata(vulnerability.Metadata), existingVulnerability.Metadata) && len(newFixedInFeatureVersions) == 0 && len(updatedFixedInFeatureVersions) == 0 { // Nothing to do. return nil } } // We do `defer observeQueryTime` here because we don't want to observe existing & up-to-date // vulnerabilities. defer observeQueryTime("insertVulnerability", "all", tf) // Insert or find the new Features. // We already have the Feature IDs in updatedFixedInFeatureVersions because diffFixedIn fills them // in using the existing vulnerability's FixedIn FeatureVersions. Note that even if FixedIn // is type FeatureVersion, the actual stored ID in these structs are the Feature IDs. // // Also, we enforce the namespace of the FeatureVersion in case it was empty. There is a test // above to ensure that the passed Namespace is either the same as the vulnerability or empty. // // TODO(Quentin-M): Batch me. for i := 0; i < len(newFixedInFeatureVersions); i++ { newFixedInFeatureVersions[i].Feature.Namespace.Name = vulnerability.Namespace.Name newFixedInFeatureVersions[i].Feature.ID, err = pgSQL.insertFeature(newFixedInFeatureVersions[i].Feature) if err != nil { return err } } // Begin transaction. tx, err := pgSQL.Begin() if err != nil { tx.Rollback() return handleError("insertVulnerability.Begin()", 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(getQuery("l_vulnerability_affects_featureversion")) observeQueryTime("insertVulnerability", "lock", t) if err != nil { tx.Rollback() return handleError("insertVulnerability.l_vulnerability_affects_featureversion", err) } if existingVulnerability.ID == 0 { // Insert new vulnerability. err = tx.QueryRow(getQuery("i_vulnerability"), namespaceID, vulnerability.Name, vulnerability.Description, vulnerability.Link, &vulnerability.Severity, &vulnerability.Metadata).Scan(&vulnerability.ID) if err != nil { tx.Rollback() return handleError("i_vulnerability", err) } } else { // Update vulnerability if vulnerability.Description != existingVulnerability.Description || vulnerability.Link != existingVulnerability.Link || vulnerability.Severity != existingVulnerability.Severity { _, err = tx.Exec(getQuery("u_vulnerability"), existingVulnerability.ID, vulnerability.Description, vulnerability.Link, &vulnerability.Severity, &vulnerability.Metadata) if err != nil { tx.Rollback() return handleError("u_vulnerability", err) } } vulnerability.ID = existingVulnerability.ID } // Update Vulnerability_FixedIn_Feature and Vulnerability_Affects_FeatureVersion now. t = time.Now() err = pgSQL.updateVulnerabilityFeatureVersions(tx, &vulnerability, &existingVulnerability, newFixedInFeatureVersions, updatedFixedInFeatureVersions) observeQueryTime("insertVulnerability", "updateVulnerabilityFeatureVersions", t) if err != nil { tx.Rollback() return err } // Create notification. notification := database.VulnerabilityNotification{ NewVulnerability: vulnerability, } if existingVulnerability.ID != 0 { notification.OldVulnerability = &existingVulnerability } if err := pgSQL.insertNotification(tx, notification); 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 } func diffFixedIn(vulnerability, existingVulnerability database.Vulnerability) (newFixedIn, updatedFixedIn []database.FeatureVersion) { // Build FeatureVersion.Feature.Namespace.Name:FeatureVersion.Feature.Name (NaN) structures. vulnerabilityFixedInNameMap, vulnerabilityFixedInNameSlice := createFeatureVersionNameMap(vulnerability.FixedIn) existingFixedInMapNameMap, existingFixedInNameSlice := createFeatureVersionNameMap(existingVulnerability.FixedIn) // Calculate the new FixedIn FeatureVersion NaN and updated ones. newFixedInName := utils.CompareStringLists(vulnerabilityFixedInNameSlice, existingFixedInNameSlice) updatedFixedInName := utils.CompareStringListsInBoth(vulnerabilityFixedInNameSlice, existingFixedInNameSlice) for _, nan := range newFixedInName { fv := vulnerabilityFixedInNameMap[nan] if fv.Version == types.MinVersion { // We don't want to mark a Feature as fixed in MinVersion. MinVersion only makes sense when a // Feature is already marked as fixed in some version, in which case we would be in the // "updatedFixedInFeatureVersions" loop and removes the fixed in mark. continue } newFixedIn = append(newFixedIn, fv) } for _, nan := range updatedFixedInName { fv := existingFixedInMapNameMap[nan] fv.Version = vulnerabilityFixedInNameMap[nan].Version if existingFixedInMapNameMap[nan].Version == fv.Version { // Versions are actually the same! // Even though they appear in both lists, it's not an update. continue } updatedFixedIn = append(updatedFixedIn, fv) } return } 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 } func (pgSQL *pgSQL) updateVulnerabilityFeatureVersions(tx *sql.Tx, vulnerability, existingVulnerability *database.Vulnerability, newFixedInFeatureVersions, updatedFixedInFeatureVersions []database.FeatureVersion) error { var fixedInID int for _, fv := range newFixedInFeatureVersions { // Insert Vulnerability_FixedIn_Feature. err := tx.QueryRow(getQuery("i_vulnerability_fixedin_feature"), vulnerability.ID, fv.Feature.ID, &fv.Version).Scan(&fixedInID) if err != nil { return handleError("i_vulnerability_fixedin_feature", err) } // Insert Vulnerability_Affects_FeatureVersion. err = linkVulnerabilityToFeatureVersions(tx, fixedInID, vulnerability.ID, fv.Feature.ID, fv.Version) if err != nil { return err } } for _, fv := range updatedFixedInFeatureVersions { if fv.Version != types.MinVersion { // Update Vulnerability_FixedIn_Feature. err := tx.QueryRow(getQuery("u_vulnerability_fixedin_feature"), vulnerability.ID, fv.Feature.ID, &fv.Version).Scan(&fixedInID) if err != nil { return handleError("u_vulnerability_fixedin_feature", err) } // Drop all old Vulnerability_Affects_FeatureVersion. _, err = tx.Exec(getQuery("r_vulnerability_affects_featureversion"), fixedInID) if err != nil { return handleError("r_vulnerability_affects_featureversion", err) } // Insert Vulnerability_Affects_FeatureVersion. err = linkVulnerabilityToFeatureVersions(tx, fixedInID, vulnerability.ID, fv.Feature.ID, fv.Version) if err != nil { return err } } else { // Updating FixedIn by saying that the fixed version is the lowest possible version, it // basically means that the vulnerability doesn't affect the feature (anymore). // Drop it from Vulnerability_FixedIn_Feature and let it cascade to // Vulnerability_Affects_FeatureVersion. err := tx.QueryRow(getQuery("r_vulnerability_fixedin_feature"), vulnerability.ID, fv.Feature.ID).Scan(&fixedInID) if err != nil && err != sql.ErrNoRows { return handleError("r_vulnerability_fixedin_feature", 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(getQuery("f_featureversion_by_feature"), featureID) if err != nil { return handleError("f_featureversion_by_feature", 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("f_featureversion_by_feature.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("f_featureversion_by_feature.Rows()", err) } rows.Close() // Insert into Vulnerability_Affects_FeatureVersion. for _, affected := range affecteds { // TODO(Quentin-M): Batch me. _, err := tx.Exec(getQuery("i_vulnerability_affects_featureversion"), vulnerabilityID, affected.ID, fixedInID) if err != nil { return handleError("i_vulnerability_affects_featureversion", err) } } return nil } func (pgSQL *pgSQL) DeleteVulnerability(namespaceName, name string) error { defer observeQueryTime("DeleteVulnerability", "all", time.Now()) result, err := pgSQL.Exec(getQuery("r_vulnerability"), namespaceName, name) if err != nil { return handleError("r_vulnerability", err) } affected, err := result.RowsAffected() if err != nil { return handleError("r_vulnerability.RowsAffected()", err) } if affected <= 0 { return cerrors.ErrNotFound } return nil }