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:
parent
57b146d0d8
commit
fb32dcfa58
@ -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: ¬ification.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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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:
|
||||
|
379
database/mock.go
379
database/mock.go
@ -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")
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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"),
|
||||
},
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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"),
|
||||
},
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|