2019-02-20 22:14:51 +00:00
|
|
|
// Copyright 2019 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 clair
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/sha256"
|
|
|
|
"encoding/hex"
|
2019-02-27 22:02:36 +00:00
|
|
|
"encoding/json"
|
2019-02-20 22:14:51 +00:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
|
|
|
|
"github.com/coreos/clair/database"
|
|
|
|
)
|
|
|
|
|
|
|
|
type layerIndexedFeature struct {
|
2019-02-27 22:02:36 +00:00
|
|
|
Feature *database.LayerFeature
|
|
|
|
Namespace *layerIndexedNamespace
|
|
|
|
IntroducedIn int
|
2019-02-20 22:14:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type layerIndexedNamespace struct {
|
2019-02-27 22:02:36 +00:00
|
|
|
Namespace database.LayerNamespace `json:"namespace"`
|
|
|
|
IntroducedIn int `json:"introducedIn"`
|
2019-02-20 22:14:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// AncestryBuilder builds an Ancestry, which contains an ordered list of layers
|
|
|
|
// and their features.
|
|
|
|
type AncestryBuilder struct {
|
|
|
|
layerIndex int
|
|
|
|
layerNames []string
|
|
|
|
detectors []database.Detector
|
2019-02-27 22:02:36 +00:00
|
|
|
namespaces []layerIndexedNamespace // unique namespaces
|
2019-02-20 22:14:51 +00:00
|
|
|
features map[database.Detector][]layerIndexedFeature
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewAncestryBuilder creates a new ancestry builder.
|
|
|
|
//
|
|
|
|
// ancestry builder takes in the extracted layer information and produce a set of
|
|
|
|
// namespaces, features, and the relation between features for the whole image.
|
|
|
|
func NewAncestryBuilder(detectors []database.Detector) *AncestryBuilder {
|
|
|
|
return &AncestryBuilder{
|
|
|
|
layerIndex: 0,
|
|
|
|
detectors: detectors,
|
2019-02-27 22:02:36 +00:00
|
|
|
namespaces: make([]layerIndexedNamespace, 0),
|
2019-02-20 22:14:51 +00:00
|
|
|
features: make(map[database.Detector][]layerIndexedFeature),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// AddLeafLayer adds a leaf layer to the ancestry builder, and computes the
|
|
|
|
// namespaced features.
|
|
|
|
func (b *AncestryBuilder) AddLeafLayer(layer *database.Layer) {
|
|
|
|
b.layerNames = append(b.layerNames, layer.Hash)
|
|
|
|
for i := range layer.Namespaces {
|
|
|
|
b.updateNamespace(&layer.Namespaces[i])
|
|
|
|
}
|
|
|
|
|
|
|
|
allFeatureMap := map[database.Detector][]database.LayerFeature{}
|
|
|
|
for i := range layer.Features {
|
|
|
|
layerFeature := layer.Features[i]
|
|
|
|
allFeatureMap[layerFeature.By] = append(allFeatureMap[layerFeature.By], layerFeature)
|
|
|
|
}
|
|
|
|
|
|
|
|
// we only care about the ones specified by builder's detectors
|
|
|
|
featureMap := map[database.Detector][]database.LayerFeature{}
|
|
|
|
for i := range b.detectors {
|
|
|
|
detector := b.detectors[i]
|
|
|
|
featureMap[detector] = allFeatureMap[detector]
|
|
|
|
}
|
|
|
|
|
|
|
|
for detector := range featureMap {
|
|
|
|
b.addLayerFeatures(detector, featureMap[detector])
|
|
|
|
}
|
|
|
|
|
|
|
|
b.layerIndex++
|
|
|
|
}
|
|
|
|
|
|
|
|
// Every detector inspects a set of files for the features
|
|
|
|
// therefore, if that set of files gives a different set of features, it
|
|
|
|
// should replace the existing features.
|
|
|
|
func (b *AncestryBuilder) addLayerFeatures(detector database.Detector, features []database.LayerFeature) {
|
|
|
|
if len(features) == 0 {
|
|
|
|
// TODO(sidac): we need to differentiate if the detector finds that all
|
|
|
|
// features are removed ( a file change ), or the package installer is
|
|
|
|
// removed ( a file deletion ), or there's no change in the file ( file
|
|
|
|
// does not exist in the blob ) Right now, we're just assuming that no
|
|
|
|
// change in the file because that's the most common case.
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
existingFeatures := b.features[detector]
|
|
|
|
currentFeatures := make([]layerIndexedFeature, 0, len(features))
|
|
|
|
// Features that are not in the current layer should be removed.
|
|
|
|
for i := range existingFeatures {
|
|
|
|
feature := existingFeatures[i]
|
|
|
|
for j := range features {
|
2019-02-27 22:02:36 +00:00
|
|
|
if features[j] == *feature.Feature {
|
2019-02-20 22:14:51 +00:00
|
|
|
currentFeatures = append(currentFeatures, feature)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Features that newly introduced in the current layer should be added.
|
|
|
|
for i := range features {
|
|
|
|
found := false
|
|
|
|
for j := range existingFeatures {
|
2019-02-27 22:02:36 +00:00
|
|
|
if *existingFeatures[j].Feature == features[i] {
|
2019-02-20 22:14:51 +00:00
|
|
|
found = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !found {
|
|
|
|
namespace, found := b.lookupNamespace(&features[i])
|
|
|
|
if !found {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
currentFeatures = append(currentFeatures, b.createLayerIndexedFeature(namespace, &features[i]))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
b.features[detector] = currentFeatures
|
|
|
|
}
|
|
|
|
|
|
|
|
// updateNamespace update the namespaces for the ancestry. It does the following things:
|
|
|
|
// 1. when a detector detects a new namespace, it's added to the ancestry.
|
|
|
|
// 2. when a detector detects a difference in the detected namespace, it
|
|
|
|
// replaces the namespace, and also move all features under that namespace to
|
|
|
|
// the new namespace.
|
|
|
|
func (b *AncestryBuilder) updateNamespace(layerNamespace *database.LayerNamespace) {
|
|
|
|
var (
|
2019-02-27 22:02:36 +00:00
|
|
|
previous *layerIndexedNamespace
|
|
|
|
foundUpgrade bool
|
2019-02-20 22:14:51 +00:00
|
|
|
)
|
|
|
|
|
2019-02-27 22:02:36 +00:00
|
|
|
newNSNames := strings.Split(layerNamespace.Name, ":")
|
|
|
|
if len(newNSNames) != 2 {
|
|
|
|
log.Error("invalid namespace name")
|
|
|
|
}
|
|
|
|
|
|
|
|
newNSName := newNSNames[0]
|
|
|
|
newNSVersion := newNSNames[1]
|
|
|
|
for i, ns := range b.namespaces {
|
|
|
|
nsNames := strings.Split(ns.Namespace.Name, ":")
|
|
|
|
if len(nsNames) != 2 {
|
|
|
|
log.Error("invalid namespace name")
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
nsName := nsNames[0]
|
|
|
|
nsVersion := nsNames[1]
|
|
|
|
if ns.Namespace.VersionFormat == layerNamespace.VersionFormat && nsName == newNSName {
|
|
|
|
if nsVersion != newNSVersion {
|
|
|
|
previous = &b.namespaces[i]
|
|
|
|
foundUpgrade = true
|
|
|
|
break
|
|
|
|
} else {
|
|
|
|
// not changed
|
|
|
|
return
|
|
|
|
}
|
2019-02-20 22:14:51 +00:00
|
|
|
}
|
2019-02-27 22:02:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// we didn't found the namespace is a upgrade from another namespace, so we
|
|
|
|
// simply add it.
|
|
|
|
if !foundUpgrade {
|
|
|
|
b.namespaces = append(b.namespaces, layerIndexedNamespace{
|
|
|
|
Namespace: *layerNamespace,
|
|
|
|
IntroducedIn: b.layerIndex,
|
|
|
|
})
|
2019-02-20 22:14:51 +00:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// All features referencing to this namespace are now pointing to the new namespace.
|
|
|
|
// Also those features are now treated as introduced in the same layer as
|
|
|
|
// when this new namespace is introduced.
|
2019-02-27 22:02:36 +00:00
|
|
|
previous.Namespace = *layerNamespace
|
|
|
|
previous.IntroducedIn = b.layerIndex
|
2019-02-20 22:14:51 +00:00
|
|
|
|
|
|
|
for _, features := range b.features {
|
|
|
|
for i, feature := range features {
|
2019-02-27 22:02:36 +00:00
|
|
|
if feature.Namespace == previous {
|
|
|
|
features[i].IntroducedIn = previous.IntroducedIn
|
2019-02-20 22:14:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *AncestryBuilder) createLayerIndexedFeature(namespace *layerIndexedNamespace, feature *database.LayerFeature) layerIndexedFeature {
|
|
|
|
return layerIndexedFeature{
|
2019-02-27 22:02:36 +00:00
|
|
|
Feature: feature,
|
|
|
|
Namespace: namespace,
|
|
|
|
IntroducedIn: b.layerIndex,
|
2019-02-20 22:14:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *AncestryBuilder) lookupNamespace(feature *database.LayerFeature) (*layerIndexedNamespace, bool) {
|
2019-02-27 22:02:36 +00:00
|
|
|
matchedNamespaces := []*layerIndexedNamespace{}
|
2019-03-14 16:27:30 +00:00
|
|
|
if feature.PotentialNamespace.Name != "" {
|
|
|
|
a := &layerIndexedNamespace{
|
|
|
|
Namespace: database.LayerNamespace{
|
|
|
|
Namespace: feature.PotentialNamespace,
|
|
|
|
},
|
|
|
|
IntroducedIn: b.layerIndex,
|
|
|
|
}
|
|
|
|
matchedNamespaces = append(matchedNamespaces, a)
|
|
|
|
} else {
|
|
|
|
|
|
|
|
for i, namespace := range b.namespaces {
|
|
|
|
if namespace.Namespace.VersionFormat == feature.VersionFormat {
|
|
|
|
matchedNamespaces = append(matchedNamespaces, &b.namespaces[i])
|
|
|
|
}
|
2019-02-20 22:14:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-27 22:02:36 +00:00
|
|
|
if len(matchedNamespaces) == 1 {
|
|
|
|
return matchedNamespaces[0], true
|
|
|
|
}
|
|
|
|
|
|
|
|
serialized, _ := json.Marshal(matchedNamespaces)
|
|
|
|
fields := log.Fields{
|
|
|
|
"feature.Name": feature.Name,
|
|
|
|
"feature.VersionFormat": feature.VersionFormat,
|
|
|
|
"ancestryBuilder.namespaces": string(serialized),
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(matchedNamespaces) > 1 {
|
|
|
|
log.WithFields(fields).Warn("skip features with ambiguous namespaces")
|
|
|
|
} else {
|
|
|
|
log.WithFields(fields).Warn("skip features with no matching namespace")
|
|
|
|
}
|
|
|
|
|
2019-02-20 22:14:51 +00:00
|
|
|
return nil, false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *AncestryBuilder) ancestryFeatures(index int) []database.AncestryFeature {
|
|
|
|
ancestryFeatures := []database.AncestryFeature{}
|
|
|
|
for detector, features := range b.features {
|
|
|
|
for _, feature := range features {
|
2019-02-27 22:02:36 +00:00
|
|
|
if feature.IntroducedIn == index {
|
2019-02-20 22:14:51 +00:00
|
|
|
ancestryFeatures = append(ancestryFeatures, database.AncestryFeature{
|
|
|
|
NamespacedFeature: database.NamespacedFeature{
|
2019-02-27 22:02:36 +00:00
|
|
|
Feature: feature.Feature.Feature,
|
|
|
|
Namespace: feature.Namespace.Namespace.Namespace,
|
2019-02-20 22:14:51 +00:00
|
|
|
},
|
|
|
|
FeatureBy: detector,
|
2019-02-27 22:02:36 +00:00
|
|
|
NamespaceBy: feature.Namespace.Namespace.By,
|
2019-02-20 22:14:51 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ancestryFeatures
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *AncestryBuilder) ancestryLayers() []database.AncestryLayer {
|
|
|
|
layers := make([]database.AncestryLayer, 0, b.layerIndex)
|
|
|
|
for i := 0; i < b.layerIndex; i++ {
|
|
|
|
layers = append(layers, database.AncestryLayer{
|
|
|
|
Hash: b.layerNames[i],
|
|
|
|
Features: b.ancestryFeatures(i),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return layers
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ancestry produces an Ancestry from the builder.
|
|
|
|
func (b *AncestryBuilder) Ancestry(name string) *database.Ancestry {
|
|
|
|
if name == "" {
|
|
|
|
// TODO(sidac): we'll use the computed ancestry name in the future.
|
|
|
|
// During the transition, it still requires the user to use the correct
|
|
|
|
// ancestry name.
|
|
|
|
name = ancestryName(b.layerNames)
|
|
|
|
log.WithField("ancestry.Name", name).Warn("generated ancestry name since it's not specified")
|
|
|
|
}
|
|
|
|
|
|
|
|
return &database.Ancestry{
|
|
|
|
Name: name,
|
|
|
|
By: b.detectors,
|
|
|
|
Layers: b.ancestryLayers(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// SaveAncestry saves an ancestry to the datastore.
|
|
|
|
func SaveAncestry(store database.Datastore, ancestry *database.Ancestry) error {
|
|
|
|
log.WithField("ancestry.Name", ancestry.Name).Debug("saving ancestry")
|
|
|
|
features := []database.NamespacedFeature{}
|
|
|
|
for _, layer := range ancestry.Layers {
|
|
|
|
features = append(features, layer.GetFeatures()...)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := database.PersistNamespacedFeaturesAndCommit(store, features); err != nil {
|
|
|
|
return StorageError
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := database.UpsertAncestryAndCommit(store, ancestry); err != nil {
|
|
|
|
return StorageError
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := database.CacheRelatedVulnerabilityAndCommit(store, features); err != nil {
|
|
|
|
return StorageError
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// IsAncestryCached checks if the ancestry is already cached in the database with the current set of detectors.
|
|
|
|
func IsAncestryCached(store database.Datastore, name string, layerHashes []string) (bool, error) {
|
|
|
|
if name == "" {
|
|
|
|
// TODO(sidac): we'll use the computed ancestry name in the future.
|
|
|
|
// During the transition, it still requires the user to use the correct
|
|
|
|
// ancestry name.
|
|
|
|
name = ancestryName(layerHashes)
|
|
|
|
log.WithField("ancestry.Name", name).Warn("generated ancestry name since it's not specified")
|
|
|
|
}
|
|
|
|
|
|
|
|
ancestry, found, err := database.FindAncestryAndRollback(store, name)
|
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).WithField("ancestry.Name", name).Error("failed to query ancestry in database")
|
|
|
|
return false, StorageError
|
|
|
|
}
|
|
|
|
|
|
|
|
if found {
|
2019-02-22 16:37:51 +00:00
|
|
|
if len(database.DiffDetectors(EnabledDetectors(), ancestry.By)) == 0 {
|
|
|
|
log.WithField("ancestry.Name", name).Debug("found cached ancestry")
|
|
|
|
} else {
|
|
|
|
log.WithField("ancestry.Name", name).Debug("found outdated ancestry cache")
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
log.WithField("ancestry.Name", name).Debug("ancestry not cached")
|
2019-02-20 22:14:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return found && len(database.DiffDetectors(EnabledDetectors(), ancestry.By)) == 0, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func ancestryName(layerHashes []string) string {
|
|
|
|
tag := sha256.Sum256([]byte(strings.Join(layerHashes, ",")))
|
|
|
|
return hex.EncodeToString(tag[:])
|
|
|
|
}
|