You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
clair/database/pgsql/vulnerability.go

430 lines
14 KiB

// 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
}