388 lines
18 KiB
Go
388 lines
18 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 database
|
||
|
|
||
|
import (
|
||
|
"github.com/coreos/clair/utils"
|
||
|
cerrors "github.com/coreos/clair/utils/errors"
|
||
|
"github.com/coreos/clair/utils/types"
|
||
|
"github.com/google/cayley"
|
||
|
"github.com/google/cayley/graph"
|
||
|
"github.com/google/cayley/graph/path"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
FieldVulnerabilityIsValue = "vulnerability"
|
||
|
FieldVulnerabilityID = "id"
|
||
|
FieldVulnerabilityLink = "link"
|
||
|
FieldVulnerabilityPriority = "priority"
|
||
|
FieldVulnerabilityDescription = "description"
|
||
|
FieldVulnerabilityFixedIn = "fixedIn"
|
||
|
)
|
||
|
|
||
|
var FieldVulnerabilityAll = []string{FieldVulnerabilityID, FieldVulnerabilityLink, FieldVulnerabilityPriority, FieldVulnerabilityDescription, FieldVulnerabilityFixedIn}
|
||
|
|
||
|
// Vulnerability represents a vulnerability that is fixed in some Packages
|
||
|
type Vulnerability struct {
|
||
|
Node string `json:"-"`
|
||
|
ID string
|
||
|
Link string
|
||
|
Priority types.Priority
|
||
|
Description string `json:",omitempty"`
|
||
|
FixedInNodes []string `json:"-"`
|
||
|
}
|
||
|
|
||
|
// GetNode returns an unique identifier for the graph node
|
||
|
// Requires the key field: ID
|
||
|
func (v *Vulnerability) GetNode() string {
|
||
|
return FieldVulnerabilityIsValue + ":" + utils.Hash(v.ID)
|
||
|
}
|
||
|
|
||
|
// ToAbstractVulnerability converts a Vulnerability into an
|
||
|
// AbstractVulnerability.
|
||
|
func (v *Vulnerability) ToAbstractVulnerability() (*AbstractVulnerability, error) {
|
||
|
// Find FixedIn packages.
|
||
|
fixedInPackages, err := FindAllPackagesByNodes(v.FixedInNodes, []string{FieldPackageOS, FieldPackageName, FieldPackageVersion})
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return &AbstractVulnerability{
|
||
|
ID: v.ID,
|
||
|
Link: v.Link,
|
||
|
Priority: v.Priority,
|
||
|
Description: v.Description,
|
||
|
AffectedPackages: PackagesToAbstractPackages(fixedInPackages),
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
// AbstractVulnerability represents a Vulnerability as it is defined in the database
|
||
|
// package but exposes directly a list of AbstractPackage instead of
|
||
|
// nodes to packages.
|
||
|
type AbstractVulnerability struct {
|
||
|
ID string
|
||
|
Link string
|
||
|
Priority types.Priority
|
||
|
Description string
|
||
|
AffectedPackages []*AbstractPackage
|
||
|
}
|
||
|
|
||
|
// ToVulnerability converts an abstractVulnerability into
|
||
|
// a Vulnerability
|
||
|
func (av *AbstractVulnerability) ToVulnerability(fixedInNodes []string) *Vulnerability {
|
||
|
return &Vulnerability{
|
||
|
ID: av.ID,
|
||
|
Link: av.Link,
|
||
|
Priority: av.Priority,
|
||
|
Description: av.Description,
|
||
|
FixedInNodes: fixedInNodes,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// InsertVulnerabilities inserts or updates several vulnerabilities in the database in one transaction
|
||
|
// It ensures that a vulnerability can't be fixed by two packages belonging the same Branch.
|
||
|
// During an update, if the vulnerability was previously fixed by a version in a branch and a new package of that branch is specified, the previous one is deleted
|
||
|
// Otherwise, it simply adds the defined packages, there is currently no way to delete affected packages.
|
||
|
//
|
||
|
// ID, Link, Priority and FixedInNodes fields have to be specified. Description is optionnal.
|
||
|
func InsertVulnerabilities(vulnerabilities []*Vulnerability) ([]Notification, error) {
|
||
|
if len(vulnerabilities) == 0 {
|
||
|
return []Notification{}, nil
|
||
|
}
|
||
|
|
||
|
// Create required data structure
|
||
|
var err error
|
||
|
t := cayley.NewTransaction()
|
||
|
cachedVulnerabilities := make(map[string]*Vulnerability)
|
||
|
newVulnerabilityNotifications := make(map[string]*NewVulnerabilityNotification)
|
||
|
vulnerabilityPriorityIncreasedNotifications := make(map[string]*VulnerabilityPriorityIncreasedNotification)
|
||
|
vulnerabilityPackageChangedNotifications := make(map[string]*VulnerabilityPackageChangedNotification)
|
||
|
|
||
|
// Iterate over all the vulnerabilities we need to insert/update
|
||
|
for _, vulnerability := range vulnerabilities {
|
||
|
// Is the vulnerability already existing ?
|
||
|
existingVulnerability, _ := cachedVulnerabilities[vulnerability.ID]
|
||
|
if existingVulnerability == nil {
|
||
|
existingVulnerability, err = FindOneVulnerability(vulnerability.ID, FieldVulnerabilityAll)
|
||
|
if err != nil && err != cerrors.ErrNotFound {
|
||
|
return []Notification{}, err
|
||
|
}
|
||
|
if existingVulnerability != nil {
|
||
|
cachedVulnerabilities[vulnerability.ID] = existingVulnerability
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Don't allow inserting/updating a vulnerability which is fixed in two packages of the same branch
|
||
|
if len(vulnerability.FixedInNodes) > 0 {
|
||
|
fixedInPackages, err := FindAllPackagesByNodes(vulnerability.FixedInNodes, []string{FieldPackageOS, FieldPackageName})
|
||
|
if err != nil {
|
||
|
return []Notification{}, err
|
||
|
}
|
||
|
fixedInBranches := make(map[string]struct{})
|
||
|
for _, fixedInPackage := range fixedInPackages {
|
||
|
branch := fixedInPackage.Branch()
|
||
|
if _, branchExists := fixedInBranches[branch]; branchExists {
|
||
|
log.Warningf("could not insert vulnerability %s because it is fixed in two packages of the same branch", vulnerability.ID)
|
||
|
return []Notification{}, cerrors.NewBadRequestError("could not insert a vulnerability which is fixed in two packages of the same branch")
|
||
|
}
|
||
|
fixedInBranches[branch] = struct{}{}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Insert/Update vulnerability
|
||
|
if existingVulnerability == nil {
|
||
|
// The vulnerability does not exist, create it
|
||
|
|
||
|
// Verify parameters
|
||
|
if vulnerability.ID == "" || vulnerability.Link == "" || vulnerability.Priority == "" {
|
||
|
log.Warningf("could not insert an incomplete vulnerability [ID: %s, Link: %s, Priority: %s]", vulnerability.ID, vulnerability.Link, vulnerability.Priority)
|
||
|
return []Notification{}, cerrors.NewBadRequestError("Could not insert an incomplete vulnerability")
|
||
|
}
|
||
|
if !vulnerability.Priority.IsValid() {
|
||
|
log.Warningf("could not insert a vulnerability which has an invalid priority [ID: %s, Link: %s, Priority: %s]. Valid priorities are: %v.", vulnerability.ID, vulnerability.Link, vulnerability.Priority, types.Priorities)
|
||
|
return []Notification{}, cerrors.NewBadRequestError("Could not insert a vulnerability which has an invalid priority")
|
||
|
}
|
||
|
if len(vulnerability.FixedInNodes) == 0 {
|
||
|
log.Warningf("could not insert a vulnerability which doesn't affect any package [ID: %s].", vulnerability.ID)
|
||
|
return []Notification{}, cerrors.NewBadRequestError("could not insert a vulnerability which doesn't affect any package")
|
||
|
}
|
||
|
|
||
|
// Insert it
|
||
|
vulnerability.Node = vulnerability.GetNode()
|
||
|
cachedVulnerabilities[vulnerability.ID] = vulnerability
|
||
|
|
||
|
t.AddQuad(cayley.Quad(vulnerability.Node, FieldIs, FieldVulnerabilityIsValue, ""))
|
||
|
t.AddQuad(cayley.Quad(vulnerability.Node, FieldVulnerabilityID, vulnerability.ID, ""))
|
||
|
t.AddQuad(cayley.Quad(vulnerability.Node, FieldVulnerabilityLink, vulnerability.Link, ""))
|
||
|
t.AddQuad(cayley.Quad(vulnerability.Node, FieldVulnerabilityPriority, string(vulnerability.Priority), ""))
|
||
|
t.AddQuad(cayley.Quad(vulnerability.Node, FieldVulnerabilityDescription, vulnerability.Description, ""))
|
||
|
for _, p := range vulnerability.FixedInNodes {
|
||
|
t.AddQuad(cayley.Quad(vulnerability.Node, FieldVulnerabilityFixedIn, p, ""))
|
||
|
}
|
||
|
|
||
|
// Add a notification
|
||
|
newVulnerabilityNotifications[vulnerability.ID] = &NewVulnerabilityNotification{VulnerabilityID: vulnerability.ID}
|
||
|
} else {
|
||
|
// The vulnerability already exists, update it
|
||
|
if vulnerability.Link != "" && existingVulnerability.Link != vulnerability.Link {
|
||
|
t.RemoveQuad(cayley.Quad(existingVulnerability.Node, FieldVulnerabilityLink, existingVulnerability.Link, ""))
|
||
|
t.AddQuad(cayley.Quad(existingVulnerability.Node, FieldVulnerabilityLink, vulnerability.Link, ""))
|
||
|
existingVulnerability.Link = vulnerability.Link
|
||
|
}
|
||
|
if vulnerability.Priority != "" && vulnerability.Priority != types.Unknown && existingVulnerability.Priority != vulnerability.Priority {
|
||
|
if !vulnerability.Priority.IsValid() {
|
||
|
log.Warningf("could not update a vulnerability which has an invalid priority [ID: %s, Link: %s, Priority: %s]. Valid priorities are: %v.", vulnerability.ID, vulnerability.Link, vulnerability.Priority, types.Priorities)
|
||
|
return []Notification{}, cerrors.NewBadRequestError("Could not update a vulnerability which has an invalid priority")
|
||
|
}
|
||
|
|
||
|
// Add a notification about the priority change if the new priority is higher and the vulnerability is not new
|
||
|
if vulnerability.Priority.Compare(existingVulnerability.Priority) > 0 {
|
||
|
if _, newVulnerabilityNotificationExists := newVulnerabilityNotifications[vulnerability.ID]; !newVulnerabilityNotificationExists {
|
||
|
// Any priorityChangeNotification already ?
|
||
|
if existingPriorityNotification, _ := vulnerabilityPriorityIncreasedNotifications[vulnerability.ID]; existingPriorityNotification != nil {
|
||
|
// There is a priority change notification, replace it but keep the old priority field
|
||
|
vulnerabilityPriorityIncreasedNotifications[vulnerability.ID] = &VulnerabilityPriorityIncreasedNotification{OldPriority: existingPriorityNotification.OldPriority, NewPriority: vulnerability.Priority, VulnerabilityID: existingVulnerability.ID}
|
||
|
} else {
|
||
|
// No previous notification, just add a new one
|
||
|
vulnerabilityPriorityIncreasedNotifications[vulnerability.ID] = &VulnerabilityPriorityIncreasedNotification{OldPriority: existingVulnerability.Priority, NewPriority: vulnerability.Priority, VulnerabilityID: existingVulnerability.ID}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
t.RemoveQuad(cayley.Quad(existingVulnerability.Node, FieldVulnerabilityPriority, string(existingVulnerability.Priority), ""))
|
||
|
t.AddQuad(cayley.Quad(existingVulnerability.Node, FieldVulnerabilityPriority, string(vulnerability.Priority), ""))
|
||
|
existingVulnerability.Priority = vulnerability.Priority
|
||
|
}
|
||
|
if vulnerability.Description != "" && existingVulnerability.Description != vulnerability.Description {
|
||
|
t.RemoveQuad(cayley.Quad(existingVulnerability.Node, FieldVulnerabilityDescription, existingVulnerability.Description, ""))
|
||
|
t.AddQuad(cayley.Quad(existingVulnerability.Node, FieldVulnerabilityDescription, vulnerability.Description, ""))
|
||
|
existingVulnerability.Description = vulnerability.Description
|
||
|
}
|
||
|
if len(vulnerability.FixedInNodes) > 0 && len(utils.CompareStringLists(vulnerability.FixedInNodes, existingVulnerability.FixedInNodes)) != 0 {
|
||
|
var removedNodes []string
|
||
|
var addedNodes []string
|
||
|
|
||
|
existingVulnerabilityFixedInPackages, err := FindAllPackagesByNodes(existingVulnerability.FixedInNodes, []string{FieldPackageOS, FieldPackageName, FieldPackageVersion})
|
||
|
if err != nil {
|
||
|
return []Notification{}, err
|
||
|
}
|
||
|
vulnerabilityFixedInPackages, err := FindAllPackagesByNodes(vulnerability.FixedInNodes, []string{FieldPackageOS, FieldPackageName, FieldPackageVersion})
|
||
|
if err != nil {
|
||
|
return []Notification{}, err
|
||
|
}
|
||
|
|
||
|
for _, p := range vulnerabilityFixedInPackages {
|
||
|
// Any already existing link ?
|
||
|
fixedInLinkAlreadyExists := false
|
||
|
for _, ep := range existingVulnerabilityFixedInPackages {
|
||
|
if *p == *ep {
|
||
|
// This exact link already exists, we won't insert it again
|
||
|
fixedInLinkAlreadyExists = true
|
||
|
} else if p.Branch() == ep.Branch() {
|
||
|
// A link to this package branch already exist and is not the same version, we will delete it
|
||
|
t.RemoveQuad(cayley.Quad(existingVulnerability.Node, FieldVulnerabilityFixedIn, ep.Node, ""))
|
||
|
|
||
|
var index int
|
||
|
for i, n := range existingVulnerability.FixedInNodes {
|
||
|
if n == ep.Node {
|
||
|
index = i
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
existingVulnerability.FixedInNodes = append(existingVulnerability.FixedInNodes[index:], existingVulnerability.FixedInNodes[index+1:]...)
|
||
|
removedNodes = append(removedNodes, ep.Node)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if fixedInLinkAlreadyExists == false {
|
||
|
t.AddQuad(cayley.Quad(existingVulnerability.Node, FieldVulnerabilityFixedIn, p.Node, ""))
|
||
|
existingVulnerability.FixedInNodes = append(existingVulnerability.FixedInNodes, p.Node)
|
||
|
addedNodes = append(addedNodes, p.Node)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Add notification about the FixedIn modification if the vulnerability is not new
|
||
|
if len(removedNodes) > 0 || len(addedNodes) > 0 {
|
||
|
if _, newVulnerabilityNotificationExists := newVulnerabilityNotifications[vulnerability.ID]; !newVulnerabilityNotificationExists {
|
||
|
// Any VulnerabilityPackageChangedNotification already ?
|
||
|
if existingPackageNotification, _ := vulnerabilityPackageChangedNotifications[vulnerability.ID]; existingPackageNotification != nil {
|
||
|
// There is a priority change notification, add the packages modifications to it
|
||
|
existingPackageNotification.AddedFixedInNodes = append(existingPackageNotification.AddedFixedInNodes, addedNodes...)
|
||
|
existingPackageNotification.RemovedFixedInNodes = append(existingPackageNotification.RemovedFixedInNodes, removedNodes...)
|
||
|
} else {
|
||
|
// No previous notification, just add a new one
|
||
|
vulnerabilityPackageChangedNotifications[vulnerability.ID] = &VulnerabilityPackageChangedNotification{VulnerabilityID: vulnerability.ID, AddedFixedInNodes: addedNodes, RemovedFixedInNodes: removedNodes}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Apply transaction
|
||
|
if err = store.ApplyTransaction(t); err != nil {
|
||
|
log.Errorf("failed transaction (InsertVulnerabilities): %s", err)
|
||
|
return []Notification{}, ErrTransaction
|
||
|
}
|
||
|
|
||
|
// Group all notifications
|
||
|
var allNotifications []Notification
|
||
|
for _, notification := range newVulnerabilityNotifications {
|
||
|
allNotifications = append(allNotifications, notification)
|
||
|
}
|
||
|
for _, notification := range vulnerabilityPriorityIncreasedNotifications {
|
||
|
allNotifications = append(allNotifications, notification)
|
||
|
}
|
||
|
for _, notification := range vulnerabilityPackageChangedNotifications {
|
||
|
allNotifications = append(allNotifications, notification)
|
||
|
}
|
||
|
|
||
|
return allNotifications, nil
|
||
|
}
|
||
|
|
||
|
// DeleteVulnerability deletes the vulnerability having the given ID
|
||
|
func DeleteVulnerability(id string) error {
|
||
|
vulnerability, err := FindOneVulnerability(id, FieldVulnerabilityAll)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
t := cayley.NewTransaction()
|
||
|
t.RemoveQuad(cayley.Quad(vulnerability.Node, FieldVulnerabilityID, vulnerability.ID, ""))
|
||
|
t.RemoveQuad(cayley.Quad(vulnerability.Node, FieldVulnerabilityLink, vulnerability.Link, ""))
|
||
|
t.RemoveQuad(cayley.Quad(vulnerability.Node, FieldVulnerabilityPriority, string(vulnerability.Priority), ""))
|
||
|
t.RemoveQuad(cayley.Quad(vulnerability.Node, FieldVulnerabilityDescription, vulnerability.Description, ""))
|
||
|
for _, p := range vulnerability.FixedInNodes {
|
||
|
t.RemoveQuad(cayley.Quad(vulnerability.Node, FieldVulnerabilityFixedIn, p, ""))
|
||
|
}
|
||
|
|
||
|
if err := store.ApplyTransaction(t); err != nil {
|
||
|
log.Errorf("failed transaction (DeleteVulnerability): %s", err)
|
||
|
return ErrTransaction
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// FindOneVulnerability finds and returns a single vulnerability having the given ID selecting the specified fields
|
||
|
func FindOneVulnerability(id string, selectedFields []string) (*Vulnerability, error) {
|
||
|
t := &Vulnerability{ID: id}
|
||
|
v, err := toVulnerabilities(cayley.StartPath(store, t.GetNode()).Has(FieldIs, FieldVulnerabilityIsValue), selectedFields)
|
||
|
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
if len(v) == 1 {
|
||
|
return v[0], nil
|
||
|
}
|
||
|
if len(v) > 1 {
|
||
|
log.Errorf("found multiple vulnerabilities with identical ID [ID: %s]", id)
|
||
|
return nil, ErrInconsistent
|
||
|
}
|
||
|
return nil, cerrors.ErrNotFound
|
||
|
}
|
||
|
|
||
|
// FindAllVulnerabilitiesByFixedIn finds and returns all vulnerabilities that are fixed in the given packages (speficied by their nodes), selecting the specified fields
|
||
|
func FindAllVulnerabilitiesByFixedIn(nodes []string, selectedFields []string) ([]*Vulnerability, error) {
|
||
|
if len(nodes) == 0 {
|
||
|
log.Warning("Could not FindAllVulnerabilitiesByFixedIn with an empty nodes array.")
|
||
|
return []*Vulnerability{}, nil
|
||
|
}
|
||
|
return toVulnerabilities(cayley.StartPath(store, nodes...).In(FieldVulnerabilityFixedIn), selectedFields)
|
||
|
}
|
||
|
|
||
|
// toVulnerabilities converts a path leading to one or multiple vulnerabilities to Vulnerability structs, selecting the specified fields
|
||
|
func toVulnerabilities(path *path.Path, selectedFields []string) ([]*Vulnerability, error) {
|
||
|
var vulnerabilities []*Vulnerability
|
||
|
|
||
|
saveFields(path, selectedFields, []string{FieldVulnerabilityFixedIn})
|
||
|
it, _ := path.BuildIterator().Optimize()
|
||
|
defer it.Close()
|
||
|
for cayley.RawNext(it) {
|
||
|
tags := make(map[string]graph.Value)
|
||
|
it.TagResults(tags)
|
||
|
|
||
|
vulnerability := Vulnerability{Node: store.NameOf(it.Result())}
|
||
|
for _, selectedField := range selectedFields {
|
||
|
switch selectedField {
|
||
|
case FieldVulnerabilityID:
|
||
|
vulnerability.ID = store.NameOf(tags[FieldVulnerabilityID])
|
||
|
case FieldVulnerabilityLink:
|
||
|
vulnerability.Link = store.NameOf(tags[FieldVulnerabilityLink])
|
||
|
case FieldVulnerabilityPriority:
|
||
|
vulnerability.Priority = types.Priority(store.NameOf(tags[FieldVulnerabilityPriority]))
|
||
|
case FieldVulnerabilityDescription:
|
||
|
vulnerability.Description = store.NameOf(tags[FieldVulnerabilityDescription])
|
||
|
case FieldVulnerabilityFixedIn:
|
||
|
var err error
|
||
|
vulnerability.FixedInNodes, err = toValues(cayley.StartPath(store, vulnerability.Node).Out(FieldVulnerabilityFixedIn))
|
||
|
if err != nil {
|
||
|
log.Errorf("could not get fixedIn on vulnerability %s: %s.", vulnerability.Node, err.Error())
|
||
|
return []*Vulnerability{}, err
|
||
|
}
|
||
|
default:
|
||
|
panic("unknown selectedField")
|
||
|
}
|
||
|
}
|
||
|
vulnerabilities = append(vulnerabilities, &vulnerability)
|
||
|
}
|
||
|
if it.Err() != nil {
|
||
|
log.Errorf("failed query in toVulnerabilities: %s", it.Err())
|
||
|
return []*Vulnerability{}, ErrBackendException
|
||
|
}
|
||
|
|
||
|
return vulnerabilities, nil
|
||
|
}
|