408 lines
14 KiB
Go
408 lines
14 KiB
Go
// 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"
|
|
"fmt"
|
|
"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, &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 &&
|
|
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).Scan(&vulnerability.ID)
|
|
if err != nil {
|
|
tx.Rollback()
|
|
return handleError("i_vulnerability", err)
|
|
}
|
|
|
|
// Create NewVulnerabilityNotification.
|
|
notification := database.VulnerabilityNotification{NewVulnerability: vulnerability}
|
|
if err := pgSQL.insertNotification(tx, notification); err != nil {
|
|
return 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)
|
|
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
|
|
}
|
|
|
|
// Commit transaction.
|
|
err = tx.Commit()
|
|
if err != nil {
|
|
tx.Rollback()
|
|
return handleError("insertVulnerability.Commit()", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|