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