// 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 ( FieldVulnerabilityID = "id" FieldVulnerabilityLink = "link" FieldVulnerabilityPriority = "priority" FieldVulnerabilityDescription = "description" FieldVulnerabilityFixedIn = "fixedIn" // FieldVulnerabilityCausedByPackage only makes sense with FindAllVulnerabilitiesByFixedIn. FieldVulnerabilityCausedByPackage = "causedByPackage" // This field is not selectable and is for internal use only. fieldVulnerabilityIsValue = "vulnerability" ) 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:"-"` CausedByPackage string `json:",omitempty"` } // 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 // 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) var notifications []Notification 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 { // Check if the vulnerability already exists 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 } } // 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() 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 notification := &NewVulnerabilityNotification{VulnerabilityID: vulnerability.ID} notifications = append(notifications, notification) newVulnerabilityNotifications[vulnerability.ID] = notification cachedVulnerabilities[vulnerability.ID] = vulnerability } 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 existingPriorityNotification.NewPriority = vulnerability.Priority } else { // No previous notification, just add a new one notification := &VulnerabilityPriorityIncreasedNotification{OldPriority: existingVulnerability.Priority, NewPriority: vulnerability.Priority, VulnerabilityID: existingVulnerability.ID} notifications = append(notifications, notification) vulnerabilityPriorityIncreasedNotifications[vulnerability.ID] = notification } } } 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 } newFixedInNodes := utils.CompareStringLists(vulnerability.FixedInNodes, existingVulnerability.FixedInNodes) if len(newFixedInNodes) > 0 { var removedNodes []string var addedNodes []string existingVulnerabilityFixedInPackages, err := FindAllPackagesByNodes(existingVulnerability.FixedInNodes, []string{FieldPackageOS, FieldPackageName, FieldPackageVersion}) if err != nil { return []Notification{}, err } newFixedInPackages, err := FindAllPackagesByNodes(newFixedInNodes, []string{FieldPackageOS, FieldPackageName, FieldPackageVersion}) if err != nil { return []Notification{}, err } for _, p := range newFixedInPackages { for _, ep := range existingVulnerabilityFixedInPackages { 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) } } 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 _, 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 notification := &VulnerabilityPackageChangedNotification{VulnerabilityID: vulnerability.ID, AddedFixedInNodes: addedNodes, RemovedFixedInNodes: removedNodes} notifications = append(notifications, notification) vulnerabilityPackageChangedNotifications[vulnerability.ID] = notification } } } } } // Apply transaction if err = store.ApplyTransaction(t); err != nil { log.Errorf("failed transaction (InsertVulnerabilities): %s", err) return []Notification{}, ErrTransaction } return notifications, 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 } // Construct path, potentially saving FieldVulnerabilityCausedByPackage path := cayley.StartPath(store, nodes...) if utils.Contains(FieldVulnerabilityCausedByPackage, selectedFields) { path = path.Save(FieldPackageName, FieldVulnerabilityCausedByPackage) } path = path.In(FieldVulnerabilityFixedIn) return toVulnerabilities(path, 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, FieldVulnerabilityCausedByPackage}) 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 } case FieldVulnerabilityCausedByPackage: vulnerability.CausedByPackage = store.NameOf(tags[FieldVulnerabilityCausedByPackage]) 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 }