Clair Logic, Extensions: updated mock tests, extensions, basic logic

Main Clair logic is changed in worker, updater, notifier for better adapting
ancestry schema. Extensions are updated with the new model and feature lister
 and namespace detector drivers are able to specify the specific listers and
detectors used to process layer's content. InRange and GetFixedIn interfaces
are added to Version format for adapting ranged affected features and next
available fixed in in the future. Tests for worker, updater and extensions
are fixed.
This commit is contained in:
Sida Chen 2017-07-26 16:22:29 -07:00
parent 57b146d0d8
commit fb32dcfa58
36 changed files with 2695 additions and 914 deletions

View File

@ -20,13 +20,17 @@ import (
"os"
"time"
"github.com/fernet/fernet-go"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
"github.com/coreos/clair"
"github.com/coreos/clair/api"
"github.com/coreos/clair/database"
"github.com/coreos/clair/ext/featurefmt"
"github.com/coreos/clair/ext/featurens"
"github.com/coreos/clair/ext/notification"
"github.com/fernet/fernet-go"
"github.com/coreos/clair/ext/vulnsrc"
)
// ErrDatasourceNotLoaded is returned when the datasource variable in the
@ -43,6 +47,7 @@ type File struct {
type Config struct {
Database database.RegistrableComponentConfig
Updater *clair.UpdaterConfig
Worker *clair.WorkerConfig
Notifier *notification.Config
API *api.Config
}
@ -54,12 +59,16 @@ func DefaultConfig() Config {
Type: "pgsql",
},
Updater: &clair.UpdaterConfig{
Interval: 1 * time.Hour,
EnabledUpdaters: vulnsrc.ListUpdaters(),
Interval: 1 * time.Hour,
},
Worker: &clair.WorkerConfig{
EnabledDetectors: featurens.ListDetectors(),
EnabledListers: featurefmt.ListListers(),
},
API: &api.Config{
Port: 6060,
HealthPort: 6061,
GrpcPort: 6070,
GrpcPort: 6060,
Timeout: 900 * time.Second,
},
Notifier: &notification.Config{
@ -97,14 +106,15 @@ func LoadConfig(path string) (config *Config, err error) {
config = &cfgFile.Clair
// Generate a pagination key if none is provided.
if config.API.PaginationKey == "" {
if v, ok := config.Database.Options["paginationkey"]; !ok || v == nil || v.(string) == "" {
log.Warn("pagination key is empty, generating...")
var key fernet.Key
if err = key.Generate(); err != nil {
return
}
config.API.PaginationKey = key.Encode()
config.Database.Options["paginationkey"] = key.Encode()
} else {
_, err = fernet.DecodeKey(config.API.PaginationKey)
_, err = fernet.DecodeKey(config.Database.Options["paginationkey"].(string))
if err != nil {
err = errors.New("Invalid Pagination key; must be 32-bit URL-safe base64")
return

View File

@ -30,9 +30,13 @@ import (
"github.com/coreos/clair"
"github.com/coreos/clair/api"
"github.com/coreos/clair/database"
"github.com/coreos/clair/ext/featurefmt"
"github.com/coreos/clair/ext/featurens"
"github.com/coreos/clair/ext/imagefmt"
"github.com/coreos/clair/ext/vulnsrc"
"github.com/coreos/clair/pkg/formatter"
"github.com/coreos/clair/pkg/stopper"
"github.com/coreos/clair/pkg/strutil"
// Register database driver.
_ "github.com/coreos/clair/database/pgsql"
@ -85,6 +89,43 @@ func stopCPUProfiling(f *os.File) {
log.Info("stopped CPU profiling")
}
func configClairVersion(config *Config) {
listers := featurefmt.ListListers()
detectors := featurens.ListDetectors()
updaters := vulnsrc.ListUpdaters()
log.WithFields(log.Fields{
"Listers": strings.Join(listers, ","),
"Detectors": strings.Join(detectors, ","),
"Updaters": strings.Join(updaters, ","),
}).Info("Clair registered components")
unregDetectors := strutil.CompareStringLists(config.Worker.EnabledDetectors, detectors)
unregListers := strutil.CompareStringLists(config.Worker.EnabledListers, listers)
unregUpdaters := strutil.CompareStringLists(config.Updater.EnabledUpdaters, updaters)
if len(unregDetectors) != 0 || len(unregListers) != 0 || len(unregUpdaters) != 0 {
log.WithFields(log.Fields{
"Unknown Detectors": strings.Join(unregDetectors, ","),
"Unknown Listers": strings.Join(unregListers, ","),
"Unknown Updaters": strings.Join(unregUpdaters, ","),
"Available Listers": strings.Join(featurefmt.ListListers(), ","),
"Available Detectors": strings.Join(featurens.ListDetectors(), ","),
"Available Updaters": strings.Join(vulnsrc.ListUpdaters(), ","),
}).Fatal("Unknown or unregistered components are configured")
}
// verify the user specified detectors/listers/updaters are implemented. If
// some are not registered, it logs warning and won't use the unregistered
// extensions.
clair.Processors = database.Processors{
Detectors: strutil.CompareStringListsInBoth(config.Worker.EnabledDetectors, detectors),
Listers: strutil.CompareStringListsInBoth(config.Worker.EnabledListers, listers),
}
clair.EnabledUpdaters = strutil.CompareStringListsInBoth(config.Updater.EnabledUpdaters, updaters)
}
// Boot starts Clair instance with the provided config.
func Boot(config *Config) {
rand.Seed(time.Now().UnixNano())
@ -102,9 +143,8 @@ func Boot(config *Config) {
go clair.RunNotifier(config.Notifier, db, st)
// Start API
st.Begin()
go api.Run(config.API, db, st)
go api.RunV2(config.API, db)
st.Begin()
go api.RunHealth(config.API, db, st)
@ -135,19 +175,17 @@ func main() {
}
}
// Load configuration
config, err := LoadConfig(*flagConfigPath)
if err != nil {
log.WithError(err).Fatal("failed to load configuration")
}
// Initialize logging system
logLevel, err := log.ParseLevel(strings.ToUpper(*flagLogLevel))
log.SetLevel(logLevel)
log.SetOutput(os.Stdout)
log.SetFormatter(&formatter.JSONExtendedFormatter{ShowLn: true})
config, err := LoadConfig(*flagConfigPath)
if err != nil {
log.WithError(err).Fatal("failed to load configuration")
}
// Enable CPU Profiling if specified
if *flagCPUProfilePath != "" {
defer stopCPUProfiling(startCPUProfiling(*flagCPUProfilePath))
@ -159,5 +197,8 @@ func main() {
imagefmt.SetInsecureTLS(*flagInsecureTLS)
}
// configure updater and worker
configClairVersion(config)
Boot(config)
}

View File

@ -25,11 +25,15 @@ clair:
# Number of elements kept in the cache
# Values unlikely to change (e.g. namespaces) are cached in order to save prevent needless roundtrips to the database.
cachesize: 16384
# 32-bit URL-safe base64 key used to encrypt pagination tokens
# If one is not provided, it will be generated.
# Multiple clair instances in the same cluster need the same value.
paginationkey:
api:
# API server port
port: 6060
grpcPort: 6070
# v2 grpc/RESTful API server port
grpcport : 6060
# Health server port
# This is an unencrypted endpoint useful for load balancers to check to healthiness of the clair server.
healthport: 6061
@ -37,11 +41,6 @@ clair:
# Deadline before an API request will respond with a 503
timeout: 900s
# 32-bit URL-safe base64 key used to encrypt pagination tokens
# If one is not provided, it will be generated.
# Multiple clair instances in the same cluster need the same value.
paginationkey:
# Optional PKI configuration
# If you want to easily generate client certificates and CAs, try the following projects:
# https://github.com/coreos/etcd-ca
@ -51,10 +50,29 @@ clair:
keyfile:
certfile:
worker:
namespace_detectors:
- os-release
- lsb-release
- apt-sources
- alpine-release
- redhat-release
feature_listers:
- apk
- dpkg
- rpm
updater:
# Frequency the database will be updated with vulnerabilities from the default data sources
# The value 0 disables the updater entirely.
interval: 2h
enabledupdaters:
- debian
- ubuntu
- rhel
- oracle
- alpine
notifier:
# Number of attempts before the notification is marked as failed to be sent
@ -72,9 +90,9 @@ clair:
# https://github.com/cloudflare/cfssl
# https://github.com/coreos/etcd-ca
servername:
cafile:
keyfile:
certfile:
cafile:
keyfile:
certfile:
# Optional HTTP Proxy: must be a valid URL (including the scheme).
proxy:

View File

@ -16,161 +16,240 @@ package database
import "time"
// MockSession implements Session and enables overriding each available method.
// The default behavior of each method is to simply panic.
type MockSession struct {
FctCommit func() error
FctRollback func() error
FctUpsertAncestry func(Ancestry, []NamespacedFeature, Processors) error
FctFindAncestry func(name string) (Ancestry, Processors, bool, error)
FctFindAncestryFeatures func(name string) (AncestryWithFeatures, bool, error)
FctFindAffectedNamespacedFeatures func(features []NamespacedFeature) ([]NullableAffectedNamespacedFeature, error)
FctPersistNamespaces func([]Namespace) error
FctPersistFeatures func([]Feature) error
FctPersistNamespacedFeatures func([]NamespacedFeature) error
FctCacheAffectedNamespacedFeatures func([]NamespacedFeature) error
FctPersistLayer func(Layer) error
FctPersistLayerContent func(hash string, namespaces []Namespace, features []Feature, processedBy Processors) error
FctFindLayer func(name string) (Layer, Processors, bool, error)
FctFindLayerWithContent func(name string) (LayerWithContent, bool, error)
FctInsertVulnerabilities func([]VulnerabilityWithAffected) error
FctFindVulnerabilities func([]VulnerabilityID) ([]NullableVulnerability, error)
FctDeleteVulnerabilities func([]VulnerabilityID) error
FctInsertVulnerabilityNotifications func([]VulnerabilityNotification) error
FctFindNewNotification func(lastNotified time.Time) (NotificationHook, bool, error)
FctFindVulnerabilityNotification func(name string, limit int, oldPage PageNumber, newPage PageNumber) (
vuln VulnerabilityNotificationWithVulnerable, ok bool, err error)
FctMarkNotificationNotified func(name string) error
FctDeleteNotification func(name string) error
FctUpdateKeyValue func(key, value string) error
FctFindKeyValue func(key string) (string, bool, error)
FctLock func(name string, owner string, duration time.Duration, renew bool) (bool, time.Time, error)
FctUnlock func(name, owner string) error
FctFindLock func(name string) (string, time.Time, bool, error)
}
func (ms *MockSession) Commit() error {
if ms.FctCommit != nil {
return ms.FctCommit()
}
panic("required mock function not implemented")
}
func (ms *MockSession) Rollback() error {
if ms.FctRollback != nil {
return ms.FctRollback()
}
panic("required mock function not implemented")
}
func (ms *MockSession) UpsertAncestry(ancestry Ancestry, features []NamespacedFeature, processedBy Processors) error {
if ms.FctUpsertAncestry != nil {
return ms.FctUpsertAncestry(ancestry, features, processedBy)
}
panic("required mock function not implemented")
}
func (ms *MockSession) FindAncestry(name string) (Ancestry, Processors, bool, error) {
if ms.FctFindAncestry != nil {
return ms.FctFindAncestry(name)
}
panic("required mock function not implemented")
}
func (ms *MockSession) FindAncestryFeatures(name string) (AncestryWithFeatures, bool, error) {
if ms.FctFindAncestryFeatures != nil {
return ms.FctFindAncestryFeatures(name)
}
panic("required mock function not implemented")
}
func (ms *MockSession) FindAffectedNamespacedFeatures(features []NamespacedFeature) ([]NullableAffectedNamespacedFeature, error) {
if ms.FctFindAffectedNamespacedFeatures != nil {
return ms.FctFindAffectedNamespacedFeatures(features)
}
panic("required mock function not implemented")
}
func (ms *MockSession) PersistNamespaces(namespaces []Namespace) error {
if ms.FctPersistNamespaces != nil {
return ms.FctPersistNamespaces(namespaces)
}
panic("required mock function not implemented")
}
func (ms *MockSession) PersistFeatures(features []Feature) error {
if ms.FctPersistFeatures != nil {
return ms.FctPersistFeatures(features)
}
panic("required mock function not implemented")
}
func (ms *MockSession) PersistNamespacedFeatures(namespacedFeatures []NamespacedFeature) error {
if ms.FctPersistNamespacedFeatures != nil {
return ms.FctPersistNamespacedFeatures(namespacedFeatures)
}
panic("required mock function not implemented")
}
func (ms *MockSession) CacheAffectedNamespacedFeatures(namespacedFeatures []NamespacedFeature) error {
if ms.FctCacheAffectedNamespacedFeatures != nil {
return ms.FctCacheAffectedNamespacedFeatures(namespacedFeatures)
}
panic("required mock function not implemented")
}
func (ms *MockSession) PersistLayer(layer Layer) error {
if ms.FctPersistLayer != nil {
return ms.FctPersistLayer(layer)
}
panic("required mock function not implemented")
}
func (ms *MockSession) PersistLayerContent(hash string, namespaces []Namespace, features []Feature, processedBy Processors) error {
if ms.FctPersistLayerContent != nil {
return ms.FctPersistLayerContent(hash, namespaces, features, processedBy)
}
panic("required mock function not implemented")
}
func (ms *MockSession) FindLayer(name string) (Layer, Processors, bool, error) {
if ms.FctFindLayer != nil {
return ms.FctFindLayer(name)
}
panic("required mock function not implemented")
}
func (ms *MockSession) FindLayerWithContent(name string) (LayerWithContent, bool, error) {
if ms.FctFindLayerWithContent != nil {
return ms.FctFindLayerWithContent(name)
}
panic("required mock function not implemented")
}
func (ms *MockSession) InsertVulnerabilities(vulnerabilities []VulnerabilityWithAffected) error {
if ms.FctInsertVulnerabilities != nil {
return ms.FctInsertVulnerabilities(vulnerabilities)
}
panic("required mock function not implemented")
}
func (ms *MockSession) FindVulnerabilities(vulnerabilityIDs []VulnerabilityID) ([]NullableVulnerability, error) {
if ms.FctFindVulnerabilities != nil {
return ms.FctFindVulnerabilities(vulnerabilityIDs)
}
panic("required mock function not implemented")
}
func (ms *MockSession) DeleteVulnerabilities(VulnerabilityIDs []VulnerabilityID) error {
if ms.FctDeleteVulnerabilities != nil {
return ms.FctDeleteVulnerabilities(VulnerabilityIDs)
}
panic("required mock function not implemented")
}
func (ms *MockSession) InsertVulnerabilityNotifications(vulnerabilityNotifications []VulnerabilityNotification) error {
if ms.FctInsertVulnerabilityNotifications != nil {
return ms.FctInsertVulnerabilityNotifications(vulnerabilityNotifications)
}
panic("required mock function not implemented")
}
func (ms *MockSession) FindNewNotification(lastNotified time.Time) (NotificationHook, bool, error) {
if ms.FctFindNewNotification != nil {
return ms.FctFindNewNotification(lastNotified)
}
panic("required mock function not implemented")
}
func (ms *MockSession) FindVulnerabilityNotification(name string, limit int, oldPage PageNumber, newPage PageNumber) (
VulnerabilityNotificationWithVulnerable, bool, error) {
if ms.FctFindVulnerabilityNotification != nil {
return ms.FctFindVulnerabilityNotification(name, limit, oldPage, newPage)
}
panic("required mock function not implemented")
}
func (ms *MockSession) MarkNotificationNotified(name string) error {
if ms.FctMarkNotificationNotified != nil {
return ms.FctMarkNotificationNotified(name)
}
panic("required mock function not implemented")
}
func (ms *MockSession) DeleteNotification(name string) error {
if ms.FctDeleteNotification != nil {
return ms.FctDeleteNotification(name)
}
panic("required mock function not implemented")
}
func (ms *MockSession) UpdateKeyValue(key, value string) error {
if ms.FctUpdateKeyValue != nil {
return ms.FctUpdateKeyValue(key, value)
}
panic("required mock function not implemented")
}
func (ms *MockSession) FindKeyValue(key string) (string, bool, error) {
if ms.FctFindKeyValue != nil {
return ms.FctFindKeyValue(key)
}
panic("required mock function not implemented")
}
func (ms *MockSession) Lock(name string, owner string, duration time.Duration, renew bool) (bool, time.Time, error) {
if ms.FctLock != nil {
return ms.FctLock(name, owner, duration, renew)
}
panic("required mock function not implemented")
}
func (ms *MockSession) Unlock(name, owner string) error {
if ms.FctUnlock != nil {
return ms.FctUnlock(name, owner)
}
panic("required mock function not implemented")
}
func (ms *MockSession) FindLock(name string) (string, time.Time, bool, error) {
if ms.FctFindLock != nil {
return ms.FctFindLock(name)
}
panic("required mock function not implemented")
}
// MockDatastore implements Datastore and enables overriding each available method.
// The default behavior of each method is to simply panic.
type MockDatastore struct {
FctListNamespaces func() ([]Namespace, error)
FctInsertLayer func(Layer) error
FctFindLayer func(name string, withFeatures, withVulnerabilities bool) (Layer, error)
FctDeleteLayer func(name string) error
FctListVulnerabilities func(namespaceName string, limit int, page int) ([]Vulnerability, int, error)
FctInsertVulnerabilities func(vulnerabilities []Vulnerability, createNotification bool) error
FctFindVulnerability func(namespaceName, name string) (Vulnerability, error)
FctDeleteVulnerability func(namespaceName, name string) error
FctInsertVulnerabilityFixes func(vulnerabilityNamespace, vulnerabilityName string, fixes []FeatureVersion) error
FctDeleteVulnerabilityFix func(vulnerabilityNamespace, vulnerabilityName, featureName string) error
FctGetAvailableNotification func(renotifyInterval time.Duration) (VulnerabilityNotification, error)
FctGetNotification func(name string, limit int, page VulnerabilityNotificationPageNumber) (VulnerabilityNotification, VulnerabilityNotificationPageNumber, error)
FctSetNotificationNotified func(name string) error
FctDeleteNotification func(name string) error
FctInsertKeyValue func(key, value string) error
FctGetKeyValue func(key string) (string, error)
FctLock func(name string, owner string, duration time.Duration, renew bool) (bool, time.Time)
FctUnlock func(name, owner string)
FctFindLock func(name string) (string, time.Time, error)
FctPing func() bool
FctClose func()
FctBegin func() (Session, error)
FctPing func() bool
FctClose func()
}
func (mds *MockDatastore) ListNamespaces() ([]Namespace, error) {
if mds.FctListNamespaces != nil {
return mds.FctListNamespaces()
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) InsertLayer(layer Layer) error {
if mds.FctInsertLayer != nil {
return mds.FctInsertLayer(layer)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) FindLayer(name string, withFeatures, withVulnerabilities bool) (Layer, error) {
if mds.FctFindLayer != nil {
return mds.FctFindLayer(name, withFeatures, withVulnerabilities)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) DeleteLayer(name string) error {
if mds.FctDeleteLayer != nil {
return mds.FctDeleteLayer(name)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) ListVulnerabilities(namespaceName string, limit int, page int) ([]Vulnerability, int, error) {
if mds.FctListVulnerabilities != nil {
return mds.FctListVulnerabilities(namespaceName, limit, page)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) InsertVulnerabilities(vulnerabilities []Vulnerability, createNotification bool) error {
if mds.FctInsertVulnerabilities != nil {
return mds.FctInsertVulnerabilities(vulnerabilities, createNotification)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) FindVulnerability(namespaceName, name string) (Vulnerability, error) {
if mds.FctFindVulnerability != nil {
return mds.FctFindVulnerability(namespaceName, name)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) DeleteVulnerability(namespaceName, name string) error {
if mds.FctDeleteVulnerability != nil {
return mds.FctDeleteVulnerability(namespaceName, name)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) InsertVulnerabilityFixes(vulnerabilityNamespace, vulnerabilityName string, fixes []FeatureVersion) error {
if mds.FctInsertVulnerabilityFixes != nil {
return mds.FctInsertVulnerabilityFixes(vulnerabilityNamespace, vulnerabilityName, fixes)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) DeleteVulnerabilityFix(vulnerabilityNamespace, vulnerabilityName, featureName string) error {
if mds.FctDeleteVulnerabilityFix != nil {
return mds.FctDeleteVulnerabilityFix(vulnerabilityNamespace, vulnerabilityName, featureName)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) GetAvailableNotification(renotifyInterval time.Duration) (VulnerabilityNotification, error) {
if mds.FctGetAvailableNotification != nil {
return mds.FctGetAvailableNotification(renotifyInterval)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) GetNotification(name string, limit int, page VulnerabilityNotificationPageNumber) (VulnerabilityNotification, VulnerabilityNotificationPageNumber, error) {
if mds.FctGetNotification != nil {
return mds.FctGetNotification(name, limit, page)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) SetNotificationNotified(name string) error {
if mds.FctSetNotificationNotified != nil {
return mds.FctSetNotificationNotified(name)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) DeleteNotification(name string) error {
if mds.FctDeleteNotification != nil {
return mds.FctDeleteNotification(name)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) InsertKeyValue(key, value string) error {
if mds.FctInsertKeyValue != nil {
return mds.FctInsertKeyValue(key, value)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) GetKeyValue(key string) (string, error) {
if mds.FctGetKeyValue != nil {
return mds.FctGetKeyValue(key)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) Lock(name string, owner string, duration time.Duration, renew bool) (bool, time.Time) {
if mds.FctLock != nil {
return mds.FctLock(name, owner, duration, renew)
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) Unlock(name, owner string) {
if mds.FctUnlock != nil {
mds.FctUnlock(name, owner)
return
}
panic("required mock function not implemented")
}
func (mds *MockDatastore) FindLock(name string) (string, time.Time, error) {
if mds.FctFindLock != nil {
return mds.FctFindLock(name)
func (mds *MockDatastore) Begin() (Session, error) {
if mds.FctBegin != nil {
return mds.FctBegin()
}
panic("required mock function not implemented")
}

View File

@ -34,17 +34,17 @@ func init() {
type lister struct{}
func (l lister) ListFeatures(files tarutil.FilesMap) ([]database.FeatureVersion, error) {
func (l lister) ListFeatures(files tarutil.FilesMap) ([]database.Feature, error) {
file, exists := files["lib/apk/db/installed"]
if !exists {
return []database.FeatureVersion{}, nil
return []database.Feature{}, nil
}
// Iterate over each line in the "installed" file attempting to parse each
// package into a feature that will be stored in a set to guarantee
// uniqueness.
pkgSet := make(map[string]database.FeatureVersion)
ipkg := database.FeatureVersion{}
pkgSet := make(map[string]database.Feature)
ipkg := database.Feature{}
scanner := bufio.NewScanner(bytes.NewBuffer(file))
for scanner.Scan() {
line := scanner.Text()
@ -55,7 +55,7 @@ func (l lister) ListFeatures(files tarutil.FilesMap) ([]database.FeatureVersion,
// Parse the package name or version.
switch {
case line[:2] == "P:":
ipkg.Feature.Name = line[2:]
ipkg.Name = line[2:]
case line[:2] == "V:":
version := string(line[2:])
err := versionfmt.Valid(dpkg.ParserName, version)
@ -67,20 +67,21 @@ func (l lister) ListFeatures(files tarutil.FilesMap) ([]database.FeatureVersion,
case line == "":
// Restart if the parser reaches another package definition before
// creating a valid package.
ipkg = database.FeatureVersion{}
ipkg = database.Feature{}
}
// If we have a whole feature, store it in the set and try to parse a new
// one.
if ipkg.Feature.Name != "" && ipkg.Version != "" {
pkgSet[ipkg.Feature.Name+"#"+ipkg.Version] = ipkg
ipkg = database.FeatureVersion{}
if ipkg.Name != "" && ipkg.Version != "" {
pkgSet[ipkg.Name+"#"+ipkg.Version] = ipkg
ipkg = database.Feature{}
}
}
// Convert the map into a slice.
pkgs := make([]database.FeatureVersion, 0, len(pkgSet))
// Convert the map into a slice and attach the version format
pkgs := make([]database.Feature, 0, len(pkgSet))
for _, pkg := range pkgSet {
pkg.VersionFormat = dpkg.ParserName
pkgs = append(pkgs, pkg)
}

View File

@ -19,58 +19,32 @@ import (
"github.com/coreos/clair/database"
"github.com/coreos/clair/ext/featurefmt"
"github.com/coreos/clair/ext/versionfmt/dpkg"
"github.com/coreos/clair/pkg/tarutil"
)
func TestAPKFeatureDetection(t *testing.T) {
testFeatures := []database.Feature{
{Name: "musl", Version: "1.1.14-r10"},
{Name: "busybox", Version: "1.24.2-r9"},
{Name: "alpine-baselayout", Version: "3.0.3-r0"},
{Name: "alpine-keys", Version: "1.1-r0"},
{Name: "zlib", Version: "1.2.8-r2"},
{Name: "libcrypto1.0", Version: "1.0.2h-r1"},
{Name: "libssl1.0", Version: "1.0.2h-r1"},
{Name: "apk-tools", Version: "2.6.7-r0"},
{Name: "scanelf", Version: "1.1.6-r0"},
{Name: "musl-utils", Version: "1.1.14-r10"},
{Name: "libc-utils", Version: "0.7-r0"},
}
for i := range testFeatures {
testFeatures[i].VersionFormat = dpkg.ParserName
}
testData := []featurefmt.TestData{
{
FeatureVersions: []database.FeatureVersion{
{
Feature: database.Feature{Name: "musl"},
Version: "1.1.14-r10",
},
{
Feature: database.Feature{Name: "busybox"},
Version: "1.24.2-r9",
},
{
Feature: database.Feature{Name: "alpine-baselayout"},
Version: "3.0.3-r0",
},
{
Feature: database.Feature{Name: "alpine-keys"},
Version: "1.1-r0",
},
{
Feature: database.Feature{Name: "zlib"},
Version: "1.2.8-r2",
},
{
Feature: database.Feature{Name: "libcrypto1.0"},
Version: "1.0.2h-r1",
},
{
Feature: database.Feature{Name: "libssl1.0"},
Version: "1.0.2h-r1",
},
{
Feature: database.Feature{Name: "apk-tools"},
Version: "2.6.7-r0",
},
{
Feature: database.Feature{Name: "scanelf"},
Version: "1.1.6-r0",
},
{
Feature: database.Feature{Name: "musl-utils"},
Version: "1.1.14-r10",
},
{
Feature: database.Feature{Name: "libc-utils"},
Version: "0.7-r0",
},
},
Features: testFeatures,
Files: tarutil.FilesMap{
"lib/apk/db/installed": featurefmt.LoadFileForTest("apk/testdata/installed"),
},

View File

@ -40,16 +40,16 @@ func init() {
featurefmt.RegisterLister("dpkg", dpkg.ParserName, &lister{})
}
func (l lister) ListFeatures(files tarutil.FilesMap) ([]database.FeatureVersion, error) {
func (l lister) ListFeatures(files tarutil.FilesMap) ([]database.Feature, error) {
f, hasFile := files["var/lib/dpkg/status"]
if !hasFile {
return []database.FeatureVersion{}, nil
return []database.Feature{}, nil
}
// Create a map to store packages and ensure their uniqueness
packagesMap := make(map[string]database.FeatureVersion)
packagesMap := make(map[string]database.Feature)
var pkg database.FeatureVersion
var pkg database.Feature
var err error
scanner := bufio.NewScanner(strings.NewReader(string(f)))
for scanner.Scan() {
@ -59,7 +59,7 @@ func (l lister) ListFeatures(files tarutil.FilesMap) ([]database.FeatureVersion,
// Package line
// Defines the name of the package
pkg.Feature.Name = strings.TrimSpace(strings.TrimPrefix(line, "Package: "))
pkg.Name = strings.TrimSpace(strings.TrimPrefix(line, "Package: "))
pkg.Version = ""
} else if strings.HasPrefix(line, "Source: ") {
// Source line (Optionnal)
@ -72,7 +72,7 @@ func (l lister) ListFeatures(files tarutil.FilesMap) ([]database.FeatureVersion,
md[dpkgSrcCaptureRegexpNames[i]] = strings.TrimSpace(n)
}
pkg.Feature.Name = md["name"]
pkg.Name = md["name"]
if md["version"] != "" {
version := md["version"]
err = versionfmt.Valid(dpkg.ParserName, version)
@ -96,21 +96,22 @@ func (l lister) ListFeatures(files tarutil.FilesMap) ([]database.FeatureVersion,
pkg.Version = version
}
} else if line == "" {
pkg.Feature.Name = ""
pkg.Name = ""
pkg.Version = ""
}
// Add the package to the result array if we have all the informations
if pkg.Feature.Name != "" && pkg.Version != "" {
packagesMap[pkg.Feature.Name+"#"+pkg.Version] = pkg
pkg.Feature.Name = ""
if pkg.Name != "" && pkg.Version != "" {
packagesMap[pkg.Name+"#"+pkg.Version] = pkg
pkg.Name = ""
pkg.Version = ""
}
}
// Convert the map to a slice
packages := make([]database.FeatureVersion, 0, len(packagesMap))
// Convert the map to a slice and add version format.
packages := make([]database.Feature, 0, len(packagesMap))
for _, pkg := range packagesMap {
pkg.VersionFormat = dpkg.ParserName
packages = append(packages, pkg)
}

View File

@ -19,28 +19,35 @@ import (
"github.com/coreos/clair/database"
"github.com/coreos/clair/ext/featurefmt"
"github.com/coreos/clair/ext/versionfmt/dpkg"
"github.com/coreos/clair/pkg/tarutil"
)
func TestDpkgFeatureDetection(t *testing.T) {
testFeatures := []database.Feature{
// Two packages from this source are installed, it should only appear one time
{
Name: "pam",
Version: "1.1.8-3.1ubuntu3",
},
{
Name: "makedev", // The source name and the package name are equals
Version: "2.3.1-93ubuntu1", // The version comes from the "Version:" line
},
{
Name: "gcc-5",
Version: "5.1.1-12ubuntu1", // The version comes from the "Source:" line
},
}
for i := range testFeatures {
testFeatures[i].VersionFormat = dpkg.ParserName
}
testData := []featurefmt.TestData{
// Test an Ubuntu dpkg status file
{
FeatureVersions: []database.FeatureVersion{
// Two packages from this source are installed, it should only appear one time
{
Feature: database.Feature{Name: "pam"},
Version: "1.1.8-3.1ubuntu3",
},
{
Feature: database.Feature{Name: "makedev"}, // The source name and the package name are equals
Version: "2.3.1-93ubuntu1", // The version comes from the "Version:" line
},
{
Feature: database.Feature{Name: "gcc-5"},
Version: "5.1.1-12ubuntu1", // The version comes from the "Source:" line
},
},
Features: testFeatures,
Files: tarutil.FilesMap{
"var/lib/dpkg/status": featurefmt.LoadFileForTest("dpkg/testdata/status"),
},

View File

@ -38,8 +38,8 @@ var (
// Lister represents an ability to list the features present in an image layer.
type Lister interface {
// ListFeatures produces a list of FeatureVersions present in an image layer.
ListFeatures(tarutil.FilesMap) ([]database.FeatureVersion, error)
// ListFeatures produces a list of Features present in an image layer.
ListFeatures(tarutil.FilesMap) ([]database.Feature, error)
// RequiredFilenames returns the list of files required to be in the FilesMap
// provided to the ListFeatures method.
@ -71,34 +71,24 @@ func RegisterLister(name string, versionfmt string, l Lister) {
versionfmtListerName[versionfmt] = append(versionfmtListerName[versionfmt], name)
}
// ListFeatures produces the list of FeatureVersions in an image layer using
// ListFeatures produces the list of Features in an image layer using
// every registered Lister.
func ListFeatures(files tarutil.FilesMap, namespace *database.Namespace) ([]database.FeatureVersion, error) {
func ListFeatures(files tarutil.FilesMap, listerNames []string) ([]database.Feature, error) {
listersM.RLock()
defer listersM.RUnlock()
var (
totalFeatures []database.FeatureVersion
listersName []string
found bool
)
var totalFeatures []database.Feature
if namespace == nil {
log.Debug("Can't detect features without namespace")
return totalFeatures, nil
}
if listersName, found = versionfmtListerName[namespace.VersionFormat]; !found {
log.WithFields(log.Fields{"namespace": namespace.Name, "version format": namespace.VersionFormat}).Debug("Unsupported Namespace")
return totalFeatures, nil
}
for _, listerName := range listersName {
features, err := listers[listerName].ListFeatures(files)
if err != nil {
return totalFeatures, err
for _, name := range listerNames {
if lister, ok := listers[name]; ok {
features, err := lister.ListFeatures(files)
if err != nil {
return []database.Feature{}, err
}
totalFeatures = append(totalFeatures, features...)
} else {
log.WithField("Name", name).Warn("Unknown Lister")
}
totalFeatures = append(totalFeatures, features...)
}
return totalFeatures, nil
@ -106,7 +96,7 @@ func ListFeatures(files tarutil.FilesMap, namespace *database.Namespace) ([]data
// RequiredFilenames returns the total list of files required for all
// registered Listers.
func RequiredFilenames() (files []string) {
func RequiredFilenames(listerNames []string) (files []string) {
listersM.RLock()
defer listersM.RUnlock()
@ -117,10 +107,19 @@ func RequiredFilenames() (files []string) {
return
}
// ListListers returns the names of all the registered feature listers.
func ListListers() []string {
r := []string{}
for name := range listers {
r = append(r, name)
}
return r
}
// TestData represents the data used to test an implementation of Lister.
type TestData struct {
Files tarutil.FilesMap
FeatureVersions []database.FeatureVersion
Files tarutil.FilesMap
Features []database.Feature
}
// LoadFileForTest can be used in order to obtain the []byte contents of a file
@ -136,9 +135,9 @@ func LoadFileForTest(name string) []byte {
func TestLister(t *testing.T, l Lister, testData []TestData) {
for _, td := range testData {
featureVersions, err := l.ListFeatures(td.Files)
if assert.Nil(t, err) && assert.Len(t, featureVersions, len(td.FeatureVersions)) {
for _, expectedFeatureVersion := range td.FeatureVersions {
assert.Contains(t, featureVersions, expectedFeatureVersion)
if assert.Nil(t, err) && assert.Len(t, featureVersions, len(td.Features)) {
for _, expectedFeature := range td.Features {
assert.Contains(t, featureVersions, expectedFeature)
}
}
}

View File

@ -38,27 +38,27 @@ func init() {
featurefmt.RegisterLister("rpm", rpm.ParserName, &lister{})
}
func (l lister) ListFeatures(files tarutil.FilesMap) ([]database.FeatureVersion, error) {
func (l lister) ListFeatures(files tarutil.FilesMap) ([]database.Feature, error) {
f, hasFile := files["var/lib/rpm/Packages"]
if !hasFile {
return []database.FeatureVersion{}, nil
return []database.Feature{}, nil
}
// Create a map to store packages and ensure their uniqueness
packagesMap := make(map[string]database.FeatureVersion)
packagesMap := make(map[string]database.Feature)
// Write the required "Packages" file to disk
tmpDir, err := ioutil.TempDir(os.TempDir(), "rpm")
defer os.RemoveAll(tmpDir)
if err != nil {
log.WithError(err).Error("could not create temporary folder for RPM detection")
return []database.FeatureVersion{}, commonerr.ErrFilesystem
return []database.Feature{}, commonerr.ErrFilesystem
}
err = ioutil.WriteFile(tmpDir+"/Packages", f, 0700)
if err != nil {
log.WithError(err).Error("could not create temporary file for RPM detection")
return []database.FeatureVersion{}, commonerr.ErrFilesystem
return []database.Feature{}, commonerr.ErrFilesystem
}
// Extract binary package names because RHSA refers to binary package names.
@ -67,7 +67,7 @@ func (l lister) ListFeatures(files tarutil.FilesMap) ([]database.FeatureVersion,
log.WithError(err).WithField("output", string(out)).Error("could not query RPM")
// Do not bubble up because we probably won't be able to fix it,
// the database must be corrupted
return []database.FeatureVersion{}, nil
return []database.Feature{}, nil
}
scanner := bufio.NewScanner(strings.NewReader(string(out)))
@ -93,18 +93,17 @@ func (l lister) ListFeatures(files tarutil.FilesMap) ([]database.FeatureVersion,
}
// Add package
pkg := database.FeatureVersion{
Feature: database.Feature{
Name: line[0],
},
pkg := database.Feature{
Name: line[0],
Version: version,
}
packagesMap[pkg.Feature.Name+"#"+pkg.Version] = pkg
packagesMap[pkg.Name+"#"+pkg.Version] = pkg
}
// Convert the map to a slice
packages := make([]database.FeatureVersion, 0, len(packagesMap))
packages := make([]database.Feature, 0, len(packagesMap))
for _, pkg := range packagesMap {
pkg.VersionFormat = rpm.ParserName
packages = append(packages, pkg)
}

View File

@ -19,6 +19,7 @@ import (
"github.com/coreos/clair/database"
"github.com/coreos/clair/ext/featurefmt"
"github.com/coreos/clair/ext/versionfmt/rpm"
"github.com/coreos/clair/pkg/tarutil"
)
@ -27,16 +28,18 @@ func TestRpmFeatureDetection(t *testing.T) {
// Test a CentOS 7 RPM database
// Memo: Use the following command on a RPM-based system to shrink a database: rpm -qa --qf "%{NAME}\n" |tail -n +3| xargs rpm -e --justdb
{
FeatureVersions: []database.FeatureVersion{
Features: []database.Feature{
// Two packages from this source are installed, it should only appear once
{
Feature: database.Feature{Name: "centos-release"},
Version: "7-1.1503.el7.centos.2.8",
Name: "centos-release",
Version: "7-1.1503.el7.centos.2.8",
VersionFormat: rpm.ParserName,
},
// Two packages from this source are installed, it should only appear once
{
Feature: database.Feature{Name: "filesystem"},
Version: "3.2-18.el7",
Name: "filesystem",
Version: "3.2-18.el7",
VersionFormat: rpm.ParserName,
},
},
Files: tarutil.FilesMap{

View File

@ -69,20 +69,24 @@ func RegisterDetector(name string, d Detector) {
}
// Detect iterators through all registered Detectors and returns all non-nil detected namespaces
func Detect(files tarutil.FilesMap) ([]database.Namespace, error) {
func Detect(files tarutil.FilesMap, detectorNames []string) ([]database.Namespace, error) {
detectorsM.RLock()
defer detectorsM.RUnlock()
namespaces := map[string]*database.Namespace{}
for name, detector := range detectors {
namespace, err := detector.Detect(files)
if err != nil {
log.WithError(err).WithField("name", name).Warning("failed while attempting to detect namespace")
return []database.Namespace{}, err
}
for _, name := range detectorNames {
if detector, ok := detectors[name]; ok {
namespace, err := detector.Detect(files)
if err != nil {
log.WithError(err).WithField("name", name).Warning("failed while attempting to detect namespace")
return nil, err
}
if namespace != nil {
log.WithFields(log.Fields{"name": name, "namespace": namespace.Name}).Debug("detected namespace")
namespaces[namespace.Name] = namespace
if namespace != nil {
log.WithFields(log.Fields{"name": name, "namespace": namespace.Name}).Debug("detected namespace")
namespaces[namespace.Name] = namespace
}
} else {
log.WithField("Name", name).Warn("Unknown namespace detector")
}
}
@ -95,7 +99,7 @@ func Detect(files tarutil.FilesMap) ([]database.Namespace, error) {
// RequiredFilenames returns the total list of files required for all
// registered Detectors.
func RequiredFilenames() (files []string) {
func RequiredFilenames(detectorNames []string) (files []string) {
detectorsM.RLock()
defer detectorsM.RUnlock()
@ -106,6 +110,15 @@ func RequiredFilenames() (files []string) {
return
}
// ListDetectors returns the names of all registered namespace detectors.
func ListDetectors() []string {
r := []string{}
for name := range detectors {
r = append(r, name)
}
return r
}
// TestData represents the data used to test an implementation of Detector.
type TestData struct {
Files tarutil.FilesMap

View File

@ -8,7 +8,7 @@ import (
"github.com/coreos/clair/database"
"github.com/coreos/clair/ext/featurens"
"github.com/coreos/clair/pkg/tarutil"
_ "github.com/coreos/clair/ext/featurens/alpinerelease"
_ "github.com/coreos/clair/ext/featurens/aptsources"
_ "github.com/coreos/clair/ext/featurens/lsbrelease"
@ -35,7 +35,7 @@ func assertnsNameEqual(t *testing.T, nslist_expected, nslist []database.Namespac
func testMultipleNamespace(t *testing.T, testData []MultipleNamespaceTestData) {
for _, td := range testData {
nslist, err := featurens.Detect(td.Files)
nslist, err := featurens.Detect(td.Files, featurens.ListDetectors())
assert.Nil(t, err)
assertnsNameEqual(t, td.ExpectedNamespaces, nslist)
}

View File

@ -38,7 +38,7 @@ import (
var (
// ErrCouldNotFindLayer is returned when we could not download or open the layer file.
ErrCouldNotFindLayer = commonerr.NewBadRequestError("could not find layer")
ErrCouldNotFindLayer = commonerr.NewBadRequestError("could not find layer from given path")
// insecureTLS controls whether TLS server's certificate chain and hostname are verified
// when pulling layers, verified in default.

View File

@ -23,8 +23,6 @@ package notification
import (
"sync"
"time"
"github.com/coreos/clair/database"
)
var (
@ -47,7 +45,7 @@ type Sender interface {
Configure(*Config) (bool, error)
// Send informs the existence of the specified notification.
Send(notification database.VulnerabilityNotification) error
Send(notificationName string) error
}
// RegisterSender makes a Sender available by the provided name.

View File

@ -29,7 +29,6 @@ import (
"gopkg.in/yaml.v2"
"github.com/coreos/clair/database"
"github.com/coreos/clair/ext/notification"
)
@ -112,9 +111,9 @@ type notificationEnvelope struct {
}
}
func (s *sender) Send(notification database.VulnerabilityNotification) error {
func (s *sender) Send(notificationName string) error {
// Marshal notification.
jsonNotification, err := json.Marshal(notificationEnvelope{struct{ Name string }{notification.Name}})
jsonNotification, err := json.Marshal(notificationEnvelope{struct{ Name string }{notificationName}})
if err != nil {
return fmt.Errorf("could not marshal: %s", err)
}

View File

@ -120,6 +120,18 @@ func (p parser) Valid(str string) bool {
return err == nil
}
func (p parser) InRange(versionA, rangeB string) (bool, error) {
cmp, err := p.Compare(versionA, rangeB)
if err != nil {
return false, err
}
return cmp < 0, nil
}
func (p parser) GetFixedIn(fixedIn string) (string, error) {
return fixedIn, nil
}
// Compare function compares two Debian-like package version
//
// The implementation is based on http://man.he.net/man5/deb-version

View File

@ -19,6 +19,8 @@ package versionfmt
import (
"errors"
"sync"
log "github.com/sirupsen/logrus"
)
const (
@ -50,6 +52,18 @@ type Parser interface {
// Compare parses two different version strings.
// Returns 0 when equal, -1 when a < b, 1 when b < a.
Compare(a, b string) (int, error)
// InRange computes if a is in range of b
//
// NOTE(Sida): For legacy version formats, rangeB is a version and
// always use if versionA < rangeB as threshold.
InRange(versionA, rangeB string) (bool, error)
// GetFixedIn computes a fixed in version for a certain version range.
//
// NOTE(Sida): For legacy version formats, rangeA is a version and
// be returned directly becuase it was considered fixed in version.
GetFixedIn(rangeA string) (string, error)
}
// RegisterParser provides a way to dynamically register an implementation of a
@ -110,3 +124,28 @@ func Compare(format, versionA, versionB string) (int, error) {
return versionParser.Compare(versionA, versionB)
}
// InRange is a helper function that checks if `versionA` is in `rangeB`
func InRange(format, version, versionRange string) (bool, error) {
versionParser, exists := GetParser(format)
if !exists {
return false, ErrUnknownVersionFormat
}
in, err := versionParser.InRange(version, versionRange)
if err != nil {
log.WithFields(log.Fields{"Format": format, "Version": version, "Range": versionRange}).Error(err)
}
return in, err
}
// GetFixedIn is a helper function that computes the next fixed in version given
// a affected version range `rangeA`.
func GetFixedIn(format, rangeA string) (string, error) {
versionParser, exists := GetParser(format)
if !exists {
return "", ErrUnknownVersionFormat
}
return versionParser.GetFixedIn(rangeA)
}