add registerable version formats

Since we only ever used dpkg, this change shims everything into using
dpkg.
pull/298/head
Jimmy Zelinskie 8 years ago
parent 3897fb6706
commit 033709eaea

@ -21,16 +21,18 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/coreos/clair/database"
"github.com/coreos/clair/utils/types"
"github.com/coreos/pkg/capnslog" "github.com/coreos/pkg/capnslog"
"github.com/fernet/fernet-go" "github.com/fernet/fernet-go"
"github.com/coreos/clair/database"
"github.com/coreos/clair/ext/versionfmt"
"github.com/coreos/clair/utils/types"
) )
var log = capnslog.NewPackageLogger("github.com/coreos/clair", "v1") var log = capnslog.NewPackageLogger("github.com/coreos/clair", "v1")
type Error struct { type Error struct {
Message string `json:"Layer` Message string `json:"Layer"`
} }
type Layer struct { type Layer struct {
@ -63,7 +65,8 @@ func LayerFromDatabaseModel(dbLayer database.Layer, withFeatures, withVulnerabil
feature := Feature{ feature := Feature{
Name: dbFeatureVersion.Feature.Name, Name: dbFeatureVersion.Feature.Name,
NamespaceName: dbFeatureVersion.Feature.Namespace.Name, NamespaceName: dbFeatureVersion.Feature.Namespace.Name,
Version: dbFeatureVersion.Version.String(), VersionFormat: dbFeatureVersion.Feature.Namespace.VersionFormat,
Version: dbFeatureVersion.Version,
AddedBy: dbFeatureVersion.AddedBy.Name, AddedBy: dbFeatureVersion.AddedBy.Name,
} }
@ -77,8 +80,8 @@ func LayerFromDatabaseModel(dbLayer database.Layer, withFeatures, withVulnerabil
Metadata: dbVuln.Metadata, Metadata: dbVuln.Metadata,
} }
if dbVuln.FixedBy != types.MaxVersion { if dbVuln.FixedBy != versionfmt.MaxVersion {
vuln.FixedBy = dbVuln.FixedBy.String() vuln.FixedBy = dbVuln.FixedBy
} }
feature.Vulnerabilities = append(feature.Vulnerabilities, vuln) feature.Vulnerabilities = append(feature.Vulnerabilities, vuln)
} }
@ -90,7 +93,8 @@ func LayerFromDatabaseModel(dbLayer database.Layer, withFeatures, withVulnerabil
} }
type Namespace struct { type Namespace struct {
Name string `json:"Name,omitempty"` Name string `json:"Name,omitempty"`
VersionFormat string `json:"VersionFormat,omitempty"`
} }
type Vulnerability struct { type Vulnerability struct {
@ -153,44 +157,51 @@ func VulnerabilityFromDatabaseModel(dbVuln database.Vulnerability, withFixedIn b
type Feature struct { type Feature struct {
Name string `json:"Name,omitempty"` Name string `json:"Name,omitempty"`
NamespaceName string `json:"NamespaceName,omitempty"` NamespaceName string `json:"NamespaceName,omitempty"`
VersionFormat string `json:"VersionFormat,omitempty"`
Version string `json:"Version,omitempty"` Version string `json:"Version,omitempty"`
Vulnerabilities []Vulnerability `json:"Vulnerabilities,omitempty"` Vulnerabilities []Vulnerability `json:"Vulnerabilities,omitempty"`
AddedBy string `json:"AddedBy,omitempty"` AddedBy string `json:"AddedBy,omitempty"`
} }
func FeatureFromDatabaseModel(dbFeatureVersion database.FeatureVersion) Feature { func FeatureFromDatabaseModel(dbFeatureVersion database.FeatureVersion) Feature {
versionStr := dbFeatureVersion.Version.String() version := dbFeatureVersion.Version
if versionStr == types.MaxVersion.String() { if version == versionfmt.MaxVersion {
versionStr = "None" version = "None"
} }
return Feature{ return Feature{
Name: dbFeatureVersion.Feature.Name, Name: dbFeatureVersion.Feature.Name,
NamespaceName: dbFeatureVersion.Feature.Namespace.Name, NamespaceName: dbFeatureVersion.Feature.Namespace.Name,
Version: versionStr, VersionFormat: dbFeatureVersion.Feature.Namespace.VersionFormat,
Version: version,
AddedBy: dbFeatureVersion.AddedBy.Name, AddedBy: dbFeatureVersion.AddedBy.Name,
} }
} }
func (f Feature) DatabaseModel() (database.FeatureVersion, error) { func (f Feature) DatabaseModel() (fv database.FeatureVersion, err error) {
var version types.Version var version string
if f.Version == "None" { if f.Version == "None" {
version = types.MaxVersion version = versionfmt.MaxVersion
} else { } else {
var err error err = versionfmt.Valid(f.VersionFormat, f.Version)
version, err = types.NewVersion(f.Version)
if err != nil { if err != nil {
return database.FeatureVersion{}, err return
} }
version = f.Version
} }
return database.FeatureVersion{ fv = database.FeatureVersion{
Feature: database.Feature{ Feature: database.Feature{
Name: f.Name, Name: f.Name,
Namespace: database.Namespace{Name: f.NamespaceName}, Namespace: database.Namespace{
Name: f.NamespaceName,
VersionFormat: f.VersionFormat,
},
}, },
Version: version, Version: version,
}, nil }
return
} }
type Notification struct { type Notification struct {

@ -179,7 +179,10 @@ func getNamespaces(w http.ResponseWriter, r *http.Request, p httprouter.Params,
} }
var namespaces []Namespace var namespaces []Namespace
for _, dbNamespace := range dbNamespaces { for _, dbNamespace := range dbNamespaces {
namespaces = append(namespaces, Namespace{Name: dbNamespace.Name}) namespaces = append(namespaces, Namespace{
Name: dbNamespace.Name,
VersionFormat: dbNamespace.VersionFormat,
})
} }
writeResponse(w, r, http.StatusOK, NamespaceEnvelope{Namespaces: &namespaces}) writeResponse(w, r, http.StatusOK, NamespaceEnvelope{Namespaces: &namespaces})

@ -40,7 +40,8 @@ type Layer struct {
type Namespace struct { type Namespace struct {
Model Model
Name string Name string
VersionFormat string
} }
type Feature struct { type Feature struct {
@ -54,7 +55,7 @@ type FeatureVersion struct {
Model Model
Feature Feature Feature Feature
Version types.Version Version string
AffectedBy []Vulnerability AffectedBy []Vulnerability
// For output purposes. Only make sense when the feature version is in the context of an image. // For output purposes. Only make sense when the feature version is in the context of an image.
@ -78,7 +79,7 @@ type Vulnerability struct {
// For output purposes. Only make sense when the vulnerability // For output purposes. Only make sense when the vulnerability
// is already about a specific Feature/FeatureVersion. // is already about a specific Feature/FeatureVersion.
FixedBy types.Version `json:",omitempty"` FixedBy string `json:",omitempty"`
} }
type MetadataMap map[string]interface{} type MetadataMap map[string]interface{}

@ -1,4 +1,4 @@
// Copyright 2015 clair authors // Copyright 2016 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -29,6 +29,9 @@ import (
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/utils" "github.com/coreos/clair/utils"
"github.com/coreos/clair/utils/types" "github.com/coreos/clair/utils/types"
// dpkg versioning is used to parse test packages.
_ "github.com/coreos/clair/ext/versionfmt/dpkg"
) )
const ( const (
@ -46,8 +49,11 @@ func TestRaceAffects(t *testing.T) {
// Insert the Feature on which we'll work. // Insert the Feature on which we'll work.
feature := database.Feature{ feature := database.Feature{
Namespace: database.Namespace{Name: "TestRaceAffectsFeatureNamespace1"}, Namespace: database.Namespace{
Name: "TestRaceAffecturesFeature1", Name: "TestRaceAffectsFeatureNamespace1",
VersionFormat: "dpkg",
},
Name: "TestRaceAffecturesFeature1",
} }
_, err = datastore.insertFeature(feature) _, err = datastore.insertFeature(feature)
if err != nil { if err != nil {
@ -66,7 +72,7 @@ func TestRaceAffects(t *testing.T) {
featureVersions[i] = database.FeatureVersion{ featureVersions[i] = database.FeatureVersion{
Feature: feature, Feature: feature,
Version: types.NewVersionUnsafe(strconv.Itoa(version)), Version: strconv.Itoa(version),
} }
} }
@ -86,7 +92,7 @@ func TestRaceAffects(t *testing.T) {
FixedIn: []database.FeatureVersion{ FixedIn: []database.FeatureVersion{
{ {
Feature: feature, Feature: feature,
Version: types.NewVersionUnsafe(strconv.Itoa(version)), Version: strconv.Itoa(version),
}, },
}, },
Severity: types.Unknown, Severity: types.Unknown,
@ -126,7 +132,7 @@ func TestRaceAffects(t *testing.T) {
var expectedAffectedNames []string var expectedAffectedNames []string
for _, featureVersion := range featureVersions { for _, featureVersion := range featureVersions {
featureVersionVersion, _ := strconv.Atoi(featureVersion.Version.String()) featureVersionVersion, _ := strconv.Atoi(featureVersion.Version)
// Get actual affects. // Get actual affects.
rows, err := datastore.Query(searchComplexTestFeatureVersionAffects, rows, err := datastore.Query(searchComplexTestFeatureVersionAffects,

@ -1,4 +1,4 @@
// Copyright 2015 clair authors // Copyright 2016 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -16,11 +16,13 @@ package pgsql
import ( import (
"database/sql" "database/sql"
"fmt"
"strings"
"time" "time"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/ext/versionfmt"
cerrors "github.com/coreos/clair/utils/errors" cerrors "github.com/coreos/clair/utils/errors"
"github.com/coreos/clair/utils/types"
) )
func (pgSQL *pgSQL) insertFeature(feature database.Feature) (int, error) { func (pgSQL *pgSQL) insertFeature(feature database.Feature) (int, error) {
@ -61,13 +63,15 @@ func (pgSQL *pgSQL) insertFeature(feature database.Feature) (int, error) {
return id, nil return id, nil
} }
func (pgSQL *pgSQL) insertFeatureVersion(featureVersion database.FeatureVersion) (id int, err error) { func (pgSQL *pgSQL) insertFeatureVersion(fv database.FeatureVersion) (id int, err error) {
if featureVersion.Version.String() == "" { err = versionfmt.Valid(fv.Feature.Namespace.VersionFormat, fv.Version)
if err != nil {
fmt.Println(err)
return 0, cerrors.NewBadRequestError("could not find/insert invalid FeatureVersion") return 0, cerrors.NewBadRequestError("could not find/insert invalid FeatureVersion")
} }
// Do cache lookup. // Do cache lookup.
cacheIndex := "featureversion:" + featureVersion.Feature.Namespace.Name + ":" + featureVersion.Feature.Name + ":" + featureVersion.Version.String() cacheIndex := strings.Join([]string{"featureversion", fv.Feature.Namespace.Name, fv.Feature.Name, fv.Version}, ":")
if pgSQL.cache != nil { if pgSQL.cache != nil {
promCacheQueriesTotal.WithLabelValues("featureversion").Inc() promCacheQueriesTotal.WithLabelValues("featureversion").Inc()
id, found := pgSQL.cache.Get(cacheIndex) id, found := pgSQL.cache.Get(cacheIndex)
@ -82,30 +86,29 @@ func (pgSQL *pgSQL) insertFeatureVersion(featureVersion database.FeatureVersion)
// Find or create Feature first. // Find or create Feature first.
t := time.Now() t := time.Now()
featureID, err := pgSQL.insertFeature(featureVersion.Feature) featureID, err := pgSQL.insertFeature(fv.Feature)
observeQueryTime("insertFeatureVersion", "insertFeature", t) observeQueryTime("insertFeatureVersion", "insertFeature", t)
if err != nil { if err != nil {
return 0, err return 0, err
} }
featureVersion.Feature.ID = featureID fv.Feature.ID = featureID
// Try to find the FeatureVersion. // Try to find the FeatureVersion.
// //
// In a populated database, the likelihood of the FeatureVersion already being there is high. // In a populated database, the likelihood of the FeatureVersion already being there is high.
// If we can find it here, we then avoid using a transaction and locking the database. // If we can find it here, we then avoid using a transaction and locking the database.
err = pgSQL.QueryRow(searchFeatureVersion, featureID, &featureVersion.Version). err = pgSQL.QueryRow(searchFeatureVersion, featureID, fv.Version).Scan(&fv.ID)
Scan(&featureVersion.ID)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
return 0, handleError("searchFeatureVersion", err) return 0, handleError("searchFeatureVersion", err)
} }
if err == nil { if err == nil {
if pgSQL.cache != nil { if pgSQL.cache != nil {
pgSQL.cache.Add(cacheIndex, featureVersion.ID) pgSQL.cache.Add(cacheIndex, fv.ID)
} }
return featureVersion.ID, nil return fv.ID, nil
} }
// Begin transaction. // Begin transaction.
@ -132,8 +135,7 @@ func (pgSQL *pgSQL) insertFeatureVersion(featureVersion database.FeatureVersion)
var created bool var created bool
t = time.Now() t = time.Now()
err = tx.QueryRow(soiFeatureVersion, featureID, &featureVersion.Version). err = tx.QueryRow(soiFeatureVersion, featureID, fv.Version).Scan(&created, &fv.ID)
Scan(&created, &featureVersion.ID)
observeQueryTime("insertFeatureVersion", "soiFeatureVersion", t) observeQueryTime("insertFeatureVersion", "soiFeatureVersion", t)
if err != nil { if err != nil {
@ -147,16 +149,16 @@ func (pgSQL *pgSQL) insertFeatureVersion(featureVersion database.FeatureVersion)
tx.Commit() tx.Commit()
if pgSQL.cache != nil { if pgSQL.cache != nil {
pgSQL.cache.Add(cacheIndex, featureVersion.ID) pgSQL.cache.Add(cacheIndex, fv.ID)
} }
return featureVersion.ID, nil return fv.ID, nil
} }
// Link the new FeatureVersion with every vulnerabilities that affect it, by inserting in // Link the new FeatureVersion with every vulnerabilities that affect it, by inserting in
// Vulnerability_Affects_FeatureVersion. // Vulnerability_Affects_FeatureVersion.
t = time.Now() t = time.Now()
err = linkFeatureVersionToVulnerabilities(tx, featureVersion) err = linkFeatureVersionToVulnerabilities(tx, fv)
observeQueryTime("insertFeatureVersion", "linkFeatureVersionToVulnerabilities", t) observeQueryTime("insertFeatureVersion", "linkFeatureVersionToVulnerabilities", t)
if err != nil { if err != nil {
@ -171,10 +173,10 @@ func (pgSQL *pgSQL) insertFeatureVersion(featureVersion database.FeatureVersion)
} }
if pgSQL.cache != nil { if pgSQL.cache != nil {
pgSQL.cache.Add(cacheIndex, featureVersion.ID) pgSQL.cache.Add(cacheIndex, fv.ID)
} }
return featureVersion.ID, nil return fv.ID, nil
} }
// TODO(Quentin-M): Batch me // TODO(Quentin-M): Batch me
@ -195,7 +197,7 @@ func (pgSQL *pgSQL) insertFeatureVersions(featureVersions []database.FeatureVers
type vulnerabilityAffectsFeatureVersion struct { type vulnerabilityAffectsFeatureVersion struct {
vulnerabilityID int vulnerabilityID int
fixedInID int fixedInID int
fixedInVersion types.Version fixedInVersion string
} }
func linkFeatureVersionToVulnerabilities(tx *sql.Tx, featureVersion database.FeatureVersion) error { func linkFeatureVersionToVulnerabilities(tx *sql.Tx, featureVersion database.FeatureVersion) error {
@ -216,7 +218,11 @@ func linkFeatureVersionToVulnerabilities(tx *sql.Tx, featureVersion database.Fea
return handleError("searchVulnerabilityFixedInFeature.Scan()", err) return handleError("searchVulnerabilityFixedInFeature.Scan()", err)
} }
if featureVersion.Version.Compare(affect.fixedInVersion) < 0 { cmp, err := versionfmt.Compare(featureVersion.Feature.Namespace.VersionFormat, featureVersion.Version, affect.fixedInVersion)
if err != nil {
return handleError("searchVulnerabilityVersionComparison", err)
}
if cmp < 0 {
// The version of the FeatureVersion we are inserting is lower than the fixed version on this // The version of the FeatureVersion we are inserting is lower than the fixed version on this
// Vulnerability, thus, this FeatureVersion is affected by it. // Vulnerability, thus, this FeatureVersion is affected by it.
affects = append(affects, affect) affects = append(affects, affect)

@ -1,4 +1,4 @@
// Copyright 2015 clair authors // Copyright 2016 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -20,7 +20,9 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/utils/types"
// dpkg versioning is used to parse test packages.
_ "github.com/coreos/clair/ext/versionfmt/dpkg"
) )
func TestInsertFeature(t *testing.T) { func TestInsertFeature(t *testing.T) {
@ -45,8 +47,11 @@ func TestInsertFeature(t *testing.T) {
// Insert Feature and ensure we can find it. // Insert Feature and ensure we can find it.
feature := database.Feature{ feature := database.Feature{
Namespace: database.Namespace{Name: "TestInsertFeatureNamespace1"}, Namespace: database.Namespace{
Name: "TestInsertFeature1", Name: "TestInsertFeatureNamespace1",
VersionFormat: "dpkg",
},
Name: "TestInsertFeature1",
} }
id1, err := datastore.insertFeature(feature) id1, err := datastore.insertFeature(feature)
assert.Nil(t, err) assert.Nil(t, err)
@ -58,28 +63,34 @@ func TestInsertFeature(t *testing.T) {
for _, invalidFeatureVersion := range []database.FeatureVersion{ for _, invalidFeatureVersion := range []database.FeatureVersion{
{ {
Feature: database.Feature{}, Feature: database.Feature{},
Version: types.NewVersionUnsafe("1.0"), Version: "1.0",
}, },
{ {
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{}, Namespace: database.Namespace{},
Name: "TestInsertFeature2", Name: "TestInsertFeature2",
}, },
Version: types.NewVersionUnsafe("1.0"), Version: "1.0",
}, },
{ {
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "TestInsertFeatureNamespace2"}, Namespace: database.Namespace{
Name: "TestInsertFeature2", Name: "TestInsertFeatureNamespace2",
VersionFormat: "dpkg",
},
Name: "TestInsertFeature2",
}, },
Version: types.NewVersionUnsafe(""), Version: "",
}, },
{ {
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "TestInsertFeatureNamespace2"}, Namespace: database.Namespace{
Name: "TestInsertFeature2", Name: "TestInsertFeatureNamespace2",
VersionFormat: "dpkg",
},
Name: "TestInsertFeature2",
}, },
Version: types.NewVersionUnsafe("bad version"), Version: "bad version",
}, },
} { } {
id3, err := datastore.insertFeatureVersion(invalidFeatureVersion) id3, err := datastore.insertFeatureVersion(invalidFeatureVersion)
@ -90,10 +101,13 @@ func TestInsertFeature(t *testing.T) {
// Insert FeatureVersion and ensure we can find it. // Insert FeatureVersion and ensure we can find it.
featureVersion := database.FeatureVersion{ featureVersion := database.FeatureVersion{
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "TestInsertFeatureNamespace1"}, Namespace: database.Namespace{
Name: "TestInsertFeature1", Name: "TestInsertFeatureNamespace1",
VersionFormat: "dpkg",
},
Name: "TestInsertFeature1",
}, },
Version: types.NewVersionUnsafe("2:3.0-imba"), Version: "2:3.0-imba",
} }
id4, err := datastore.insertFeatureVersion(featureVersion) id4, err := datastore.insertFeatureVersion(featureVersion)
assert.Nil(t, err) assert.Nil(t, err)

@ -1,4 +1,4 @@
// Copyright 2015 clair authors // Copyright 2016 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.

@ -16,12 +16,14 @@ package pgsql
import ( import (
"database/sql" "database/sql"
"strings"
"time" "time"
"github.com/guregu/null/zero"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/utils" "github.com/coreos/clair/utils"
cerrors "github.com/coreos/clair/utils/errors" cerrors "github.com/coreos/clair/utils/errors"
"github.com/guregu/null/zero"
) )
func (pgSQL *pgSQL) FindLayer(name string, withFeatures, withVulnerabilities bool) (database.Layer, error) { func (pgSQL *pgSQL) FindLayer(name string, withFeatures, withVulnerabilities bool) (database.Layer, error) {
@ -34,14 +36,26 @@ func (pgSQL *pgSQL) FindLayer(name string, withFeatures, withVulnerabilities boo
defer observeQueryTime("FindLayer", subquery, time.Now()) defer observeQueryTime("FindLayer", subquery, time.Now())
// Find the layer // Find the layer
var layer database.Layer var (
var parentID zero.Int layer database.Layer
var parentName zero.String parentID zero.Int
var namespaceID zero.Int parentName zero.String
var namespaceName sql.NullString nsID zero.Int
nsName sql.NullString
nsVersionFormat sql.NullString
)
t := time.Now() t := time.Now()
err := pgSQL.QueryRow(searchLayer, name).Scan(&layer.ID, &layer.Name, &layer.EngineVersion, &parentID, &parentName, &namespaceID, &namespaceName) err := pgSQL.QueryRow(searchLayer, name).Scan(
&layer.ID,
&layer.Name,
&layer.EngineVersion,
&parentID,
&parentName,
&nsID,
&nsName,
&nsVersionFormat,
)
observeQueryTime("FindLayer", "searchLayer", t) observeQueryTime("FindLayer", "searchLayer", t)
if err != nil { if err != nil {
@ -54,10 +68,11 @@ func (pgSQL *pgSQL) FindLayer(name string, withFeatures, withVulnerabilities boo
Name: parentName.String, Name: parentName.String,
} }
} }
if !namespaceID.IsZero() { if !nsID.IsZero() {
layer.Namespace = &database.Namespace{ layer.Namespace = &database.Namespace{
Model: database.Model{ID: int(namespaceID.Int64)}, Model: database.Model{ID: int(nsID.Int64)},
Name: namespaceName.String, Name: nsName.String,
VersionFormat: nsVersionFormat.String,
} }
} }
@ -125,12 +140,20 @@ func getLayerFeatureVersions(tx *sql.Tx, layerID int) ([]database.FeatureVersion
var modification string var modification string
mapFeatureVersions := make(map[int]database.FeatureVersion) mapFeatureVersions := make(map[int]database.FeatureVersion)
for rows.Next() { for rows.Next() {
var featureVersion database.FeatureVersion var fv database.FeatureVersion
err = rows.Scan(
err = rows.Scan(&featureVersion.ID, &modification, &featureVersion.Feature.Namespace.ID, &fv.ID,
&featureVersion.Feature.Namespace.Name, &featureVersion.Feature.ID, &modification,
&featureVersion.Feature.Name, &featureVersion.ID, &featureVersion.Version, &fv.Feature.Namespace.ID,
&featureVersion.AddedBy.ID, &featureVersion.AddedBy.Name) &fv.Feature.Namespace.Name,
&fv.Feature.Namespace.VersionFormat,
&fv.Feature.ID,
&fv.Feature.Name,
&fv.ID,
&fv.Version,
&fv.AddedBy.ID,
&fv.AddedBy.Name,
)
if err != nil { if err != nil {
return featureVersions, handleError("searchLayerFeatureVersion.Scan()", err) return featureVersions, handleError("searchLayerFeatureVersion.Scan()", err)
} }
@ -138,9 +161,9 @@ func getLayerFeatureVersions(tx *sql.Tx, layerID int) ([]database.FeatureVersion
// Do transitive closure. // Do transitive closure.
switch modification { switch modification {
case "add": case "add":
mapFeatureVersions[featureVersion.ID] = featureVersion mapFeatureVersions[fv.ID] = fv
case "del": case "del":
delete(mapFeatureVersions, featureVersion.ID) delete(mapFeatureVersions, fv.ID)
default: default:
log.Warningf("unknown Layer_diff_FeatureVersion's modification: %s", modification) log.Warningf("unknown Layer_diff_FeatureVersion's modification: %s", modification)
return featureVersions, database.ErrInconsistent return featureVersions, database.ErrInconsistent
@ -182,9 +205,18 @@ func loadAffectedBy(tx *sql.Tx, featureVersions []database.FeatureVersion) error
var featureversionID int var featureversionID int
for rows.Next() { for rows.Next() {
var vulnerability database.Vulnerability var vulnerability database.Vulnerability
err := rows.Scan(&featureversionID, &vulnerability.ID, &vulnerability.Name, err := rows.Scan(
&vulnerability.Description, &vulnerability.Link, &vulnerability.Severity, &featureversionID,
&vulnerability.Metadata, &vulnerability.Namespace.Name, &vulnerability.FixedBy) &vulnerability.ID,
&vulnerability.Name,
&vulnerability.Description,
&vulnerability.Link,
&vulnerability.Severity,
&vulnerability.Metadata,
&vulnerability.Namespace.Name,
&vulnerability.Namespace.VersionFormat,
&vulnerability.FixedBy,
)
if err != nil { if err != nil {
return handleError("searchFeatureVersionVulnerability.Scan()", err) return handleError("searchFeatureVersionVulnerability.Scan()", err)
} }
@ -374,9 +406,9 @@ func createNV(features []database.FeatureVersion) (map[string]*database.FeatureV
sliceNV := make([]string, 0, len(features)) sliceNV := make([]string, 0, len(features))
for i := 0; i < len(features); i++ { for i := 0; i < len(features); i++ {
featureVersion := &features[i] fv := &features[i]
nv := featureVersion.Feature.Namespace.Name + ":" + featureVersion.Feature.Name + ":" + featureVersion.Version.String() nv := strings.Join([]string{fv.Feature.Namespace.Name, fv.Feature.Name, fv.Version}, ":")
mapNV[nv] = featureVersion mapNV[nv] = fv
sliceNV = append(sliceNV, nv) sliceNV = append(sliceNV, nv)
} }

@ -1,4 +1,4 @@
// Copyright 2015 clair authors // Copyright 2016 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -23,6 +23,9 @@ import (
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
cerrors "github.com/coreos/clair/utils/errors" cerrors "github.com/coreos/clair/utils/errors"
"github.com/coreos/clair/utils/types" "github.com/coreos/clair/utils/types"
// dpkg versioning is used to parse test packages.
_ "github.com/coreos/clair/ext/versionfmt/dpkg"
) )
func TestFindLayer(t *testing.T) { func TestFindLayer(t *testing.T) {
@ -67,9 +70,9 @@ func TestFindLayer(t *testing.T) {
switch featureVersion.Feature.Name { switch featureVersion.Feature.Name {
case "wechat": case "wechat":
assert.Equal(t, types.NewVersionUnsafe("0.5"), featureVersion.Version) assert.Equal(t, "0.5", featureVersion.Version)
case "openssl": case "openssl":
assert.Equal(t, types.NewVersionUnsafe("1.0"), featureVersion.Version) assert.Equal(t, "1.0", featureVersion.Version)
default: default:
t.Errorf("unexpected package %s for layer-1", featureVersion.Feature.Name) t.Errorf("unexpected package %s for layer-1", featureVersion.Feature.Name)
} }
@ -83,9 +86,9 @@ func TestFindLayer(t *testing.T) {
switch featureVersion.Feature.Name { switch featureVersion.Feature.Name {
case "wechat": case "wechat":
assert.Equal(t, types.NewVersionUnsafe("0.5"), featureVersion.Version) assert.Equal(t, "0.5", featureVersion.Version)
case "openssl": case "openssl":
assert.Equal(t, types.NewVersionUnsafe("1.0"), featureVersion.Version) assert.Equal(t, "1.0", featureVersion.Version)
if assert.Len(t, featureVersion.AffectedBy, 1) { if assert.Len(t, featureVersion.AffectedBy, 1) {
assert.Equal(t, "debian:7", featureVersion.AffectedBy[0].Namespace.Name) assert.Equal(t, "debian:7", featureVersion.AffectedBy[0].Namespace.Name)
@ -93,7 +96,7 @@ func TestFindLayer(t *testing.T) {
assert.Equal(t, types.High, featureVersion.AffectedBy[0].Severity) assert.Equal(t, types.High, featureVersion.AffectedBy[0].Severity)
assert.Equal(t, "A vulnerability affecting OpenSSL < 2.0 on Debian 7.0", featureVersion.AffectedBy[0].Description) assert.Equal(t, "A vulnerability affecting OpenSSL < 2.0 on Debian 7.0", featureVersion.AffectedBy[0].Description)
assert.Equal(t, "http://google.com/#q=CVE-OPENSSL-1-DEB7", featureVersion.AffectedBy[0].Link) assert.Equal(t, "http://google.com/#q=CVE-OPENSSL-1-DEB7", featureVersion.AffectedBy[0].Link)
assert.Equal(t, types.NewVersionUnsafe("2.0"), featureVersion.AffectedBy[0].FixedBy) assert.Equal(t, "2.0", featureVersion.AffectedBy[0].FixedBy)
} }
default: default:
t.Errorf("unexpected package %s for layer-1", featureVersion.Feature.Name) t.Errorf("unexpected package %s for layer-1", featureVersion.Feature.Name)
@ -139,45 +142,63 @@ func testInsertLayerInvalid(t *testing.T, datastore database.Datastore) {
func testInsertLayerTree(t *testing.T, datastore database.Datastore) { func testInsertLayerTree(t *testing.T, datastore database.Datastore) {
f1 := database.FeatureVersion{ f1 := database.FeatureVersion{
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "TestInsertLayerNamespace2"}, Namespace: database.Namespace{
Name: "TestInsertLayerFeature1", Name: "TestInsertLayerNamespace2",
VersionFormat: "dpkg",
},
Name: "TestInsertLayerFeature1",
}, },
Version: types.NewVersionUnsafe("1.0"), Version: "1.0",
} }
f2 := database.FeatureVersion{ f2 := database.FeatureVersion{
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "TestInsertLayerNamespace2"}, Namespace: database.Namespace{
Name: "TestInsertLayerFeature2", Name: "TestInsertLayerNamespace2",
VersionFormat: "dpkg",
},
Name: "TestInsertLayerFeature2",
}, },
Version: types.NewVersionUnsafe("0.34"), Version: "0.34",
} }
f3 := database.FeatureVersion{ f3 := database.FeatureVersion{
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "TestInsertLayerNamespace2"}, Namespace: database.Namespace{
Name: "TestInsertLayerFeature3", Name: "TestInsertLayerNamespace2",
VersionFormat: "dpkg",
},
Name: "TestInsertLayerFeature3",
}, },
Version: types.NewVersionUnsafe("0.56"), Version: "0.56",
} }
f4 := database.FeatureVersion{ f4 := database.FeatureVersion{
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "TestInsertLayerNamespace3"}, Namespace: database.Namespace{
Name: "TestInsertLayerFeature2", Name: "TestInsertLayerNamespace3",
VersionFormat: "dpkg",
},
Name: "TestInsertLayerFeature2",
}, },
Version: types.NewVersionUnsafe("0.34"), Version: "0.34",
} }
f5 := database.FeatureVersion{ f5 := database.FeatureVersion{
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "TestInsertLayerNamespace3"}, Namespace: database.Namespace{
Name: "TestInsertLayerFeature3", Name: "TestInsertLayerNamespace3",
VersionFormat: "dpkg",
},
Name: "TestInsertLayerFeature3",
}, },
Version: types.NewVersionUnsafe("0.56"), Version: "0.56",
} }
f6 := database.FeatureVersion{ f6 := database.FeatureVersion{
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "TestInsertLayerNamespace3"}, Namespace: database.Namespace{
Name: "TestInsertLayerFeature4", Name: "TestInsertLayerNamespace3",
VersionFormat: "dpkg",
},
Name: "TestInsertLayerFeature4",
}, },
Version: types.NewVersionUnsafe("0.666"), Version: "0.666",
} }
layers := []database.Layer{ layers := []database.Layer{
@ -185,16 +206,22 @@ func testInsertLayerTree(t *testing.T, datastore database.Datastore) {
Name: "TestInsertLayer1", Name: "TestInsertLayer1",
}, },
{ {
Name: "TestInsertLayer2", Name: "TestInsertLayer2",
Parent: &database.Layer{Name: "TestInsertLayer1"}, Parent: &database.Layer{Name: "TestInsertLayer1"},
Namespace: &database.Namespace{Name: "TestInsertLayerNamespace1"}, Namespace: &database.Namespace{
Name: "TestInsertLayerNamespace1",
VersionFormat: "dpkg",
},
}, },
// This layer changes the namespace and adds Features. // This layer changes the namespace and adds Features.
{ {
Name: "TestInsertLayer3", Name: "TestInsertLayer3",
Parent: &database.Layer{Name: "TestInsertLayer2"}, Parent: &database.Layer{Name: "TestInsertLayer2"},
Namespace: &database.Namespace{Name: "TestInsertLayerNamespace2"}, Namespace: &database.Namespace{
Features: []database.FeatureVersion{f1, f2, f3}, Name: "TestInsertLayerNamespace2",
VersionFormat: "dpkg",
},
Features: []database.FeatureVersion{f1, f2, f3},
}, },
// This layer covers the case where the last layer doesn't provide any new Feature. // This layer covers the case where the last layer doesn't provide any new Feature.
{ {
@ -206,9 +233,12 @@ func testInsertLayerTree(t *testing.T, datastore database.Datastore) {
// It also modifies the Namespace ("upgrade") but keeps some Features not upgraded, their // It also modifies the Namespace ("upgrade") but keeps some Features not upgraded, their
// Namespaces should then remain unchanged. // Namespaces should then remain unchanged.
{ {
Name: "TestInsertLayer4b", Name: "TestInsertLayer4b",
Parent: &database.Layer{Name: "TestInsertLayer3"}, Parent: &database.Layer{Name: "TestInsertLayer3"},
Namespace: &database.Namespace{Name: "TestInsertLayerNamespace3"}, Namespace: &database.Namespace{
Name: "TestInsertLayerNamespace3",
VersionFormat: "dpkg",
},
Features: []database.FeatureVersion{ Features: []database.FeatureVersion{
// Deletes TestInsertLayerFeature1. // Deletes TestInsertLayerFeature1.
// Keep TestInsertLayerFeature2 (old Namespace should be kept): // Keep TestInsertLayerFeature2 (old Namespace should be kept):
@ -263,18 +293,24 @@ func testInsertLayerTree(t *testing.T, datastore database.Datastore) {
func testInsertLayerUpdate(t *testing.T, datastore database.Datastore) { func testInsertLayerUpdate(t *testing.T, datastore database.Datastore) {
f7 := database.FeatureVersion{ f7 := database.FeatureVersion{
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "TestInsertLayerNamespace3"}, Namespace: database.Namespace{
Name: "TestInsertLayerFeature7", Name: "TestInsertLayerNamespace3",
VersionFormat: "dpkg",
},
Name: "TestInsertLayerFeature7",
}, },
Version: types.NewVersionUnsafe("0.01"), Version: "0.01",
} }
l3, _ := datastore.FindLayer("TestInsertLayer3", true, false) l3, _ := datastore.FindLayer("TestInsertLayer3", true, false)
l3u := database.Layer{ l3u := database.Layer{
Name: l3.Name, Name: l3.Name,
Parent: l3.Parent, Parent: l3.Parent,
Namespace: &database.Namespace{Name: "TestInsertLayerNamespaceUpdated1"}, Namespace: &database.Namespace{
Features: []database.FeatureVersion{f7}, Name: "TestInsertLayerNamespaceUpdated1",
VersionFormat: "dpkg",
},
Features: []database.FeatureVersion{f7},
} }
l4u := database.Layer{ l4u := database.Layer{
@ -347,5 +383,5 @@ func testInsertLayerDelete(t *testing.T, datastore database.Datastore) {
func cmpFV(a, b database.FeatureVersion) bool { func cmpFV(a, b database.FeatureVersion) bool {
return a.Feature.Name == b.Feature.Name && return a.Feature.Name == b.Feature.Name &&
a.Feature.Namespace.Name == b.Feature.Namespace.Name && a.Feature.Namespace.Name == b.Feature.Namespace.Name &&
a.Version.String() == b.Version.String() a.Version == b.Version
} }

@ -1,4 +1,4 @@
// Copyright 2015 clair authors // Copyright 2016 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.

@ -0,0 +1,29 @@
// Copyright 2016 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package migrations
import "github.com/remind101/migrate"
func init() {
RegisterMigration(migrate.Migration{
ID: 6,
Up: migrate.Queries([]string{
`ALTER TABLE Namespace ADD COLUMN version_format varchar(128);`,
}),
Down: migrate.Queries([]string{
`ALTER TABLE Namespace DROP COLUMN version_format;`,
}),
})
}

@ -38,7 +38,7 @@ func (pgSQL *pgSQL) insertNamespace(namespace database.Namespace) (int, error) {
defer observeQueryTime("insertNamespace", "all", time.Now()) defer observeQueryTime("insertNamespace", "all", time.Now())
var id int var id int
err := pgSQL.QueryRow(soiNamespace, namespace.Name).Scan(&id) err := pgSQL.QueryRow(soiNamespace, namespace.Name, namespace.VersionFormat).Scan(&id)
if err != nil { if err != nil {
return 0, handleError("soiNamespace", err) return 0, handleError("soiNamespace", err)
} }
@ -58,14 +58,14 @@ func (pgSQL *pgSQL) ListNamespaces() (namespaces []database.Namespace, err error
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
var namespace database.Namespace var ns database.Namespace
err = rows.Scan(&namespace.ID, &namespace.Name) err = rows.Scan(&ns.ID, &ns.Name, &ns.VersionFormat)
if err != nil { if err != nil {
return namespaces, handleError("listNamespace.Scan()", err) return namespaces, handleError("listNamespace.Scan()", err)
} }
namespaces = append(namespaces, namespace) namespaces = append(namespaces, ns)
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
return namespaces, handleError("listNamespace.Rows()", err) return namespaces, handleError("listNamespace.Rows()", err)

@ -1,4 +1,4 @@
// Copyright 2015 clair authors // Copyright 2016 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -21,6 +21,9 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
// dpkg versioning is used to parse test packages.
_ "github.com/coreos/clair/ext/versionfmt/dpkg"
) )
func TestInsertNamespace(t *testing.T) { func TestInsertNamespace(t *testing.T) {
@ -37,9 +40,15 @@ func TestInsertNamespace(t *testing.T) {
assert.Zero(t, id0) assert.Zero(t, id0)
// Insert Namespace and ensure we can find it. // Insert Namespace and ensure we can find it.
id1, err := datastore.insertNamespace(database.Namespace{Name: "TestInsertNamespace1"}) id1, err := datastore.insertNamespace(database.Namespace{
Name: "TestInsertNamespace1",
VersionFormat: "dpkg",
})
assert.Nil(t, err) assert.Nil(t, err)
id2, err := datastore.insertNamespace(database.Namespace{Name: "TestInsertNamespace1"}) id2, err := datastore.insertNamespace(database.Namespace{
Name: "TestInsertNamespace1",
VersionFormat: "dpkg",
})
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, id1, id2) assert.Equal(t, id1, id2)
} }

@ -1,4 +1,4 @@
// Copyright 2015 clair authors // Copyright 2016 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -21,8 +21,12 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/ext/versionfmt"
cerrors "github.com/coreos/clair/utils/errors" cerrors "github.com/coreos/clair/utils/errors"
"github.com/coreos/clair/utils/types" "github.com/coreos/clair/utils/types"
// dpkg versioning is used to parse test packages.
_ "github.com/coreos/clair/ext/versionfmt/dpkg"
) )
func TestNotification(t *testing.T) { func TestNotification(t *testing.T) {
@ -39,13 +43,19 @@ func TestNotification(t *testing.T) {
// Create some data. // Create some data.
f1 := database.Feature{ f1 := database.Feature{
Name: "TestNotificationFeature1", Name: "TestNotificationFeature1",
Namespace: database.Namespace{Name: "TestNotificationNamespace1"}, Namespace: database.Namespace{
Name: "TestNotificationNamespace1",
VersionFormat: "dpkg",
},
} }
f2 := database.Feature{ f2 := database.Feature{
Name: "TestNotificationFeature2", Name: "TestNotificationFeature2",
Namespace: database.Namespace{Name: "TestNotificationNamespace1"}, Namespace: database.Namespace{
Name: "TestNotificationNamespace1",
VersionFormat: "dpkg",
},
} }
l1 := database.Layer{ l1 := database.Layer{
@ -53,7 +63,7 @@ func TestNotification(t *testing.T) {
Features: []database.FeatureVersion{ Features: []database.FeatureVersion{
{ {
Feature: f1, Feature: f1,
Version: types.NewVersionUnsafe("0.1"), Version: "0.1",
}, },
}, },
} }
@ -63,7 +73,7 @@ func TestNotification(t *testing.T) {
Features: []database.FeatureVersion{ Features: []database.FeatureVersion{
{ {
Feature: f1, Feature: f1,
Version: types.NewVersionUnsafe("0.2"), Version: "0.2",
}, },
}, },
} }
@ -73,7 +83,7 @@ func TestNotification(t *testing.T) {
Features: []database.FeatureVersion{ Features: []database.FeatureVersion{
{ {
Feature: f1, Feature: f1,
Version: types.NewVersionUnsafe("0.3"), Version: "0.3",
}, },
}, },
} }
@ -83,7 +93,7 @@ func TestNotification(t *testing.T) {
Features: []database.FeatureVersion{ Features: []database.FeatureVersion{
{ {
Feature: f2, Feature: f2,
Version: types.NewVersionUnsafe("0.1"), Version: "0.1",
}, },
}, },
} }
@ -105,7 +115,7 @@ func TestNotification(t *testing.T) {
FixedIn: []database.FeatureVersion{ FixedIn: []database.FeatureVersion{
{ {
Feature: f1, Feature: f1,
Version: types.NewVersionUnsafe("1.0"), Version: "1.0",
}, },
}, },
} }
@ -165,11 +175,11 @@ func TestNotification(t *testing.T) {
v1b.FixedIn = []database.FeatureVersion{ v1b.FixedIn = []database.FeatureVersion{
{ {
Feature: f1, Feature: f1,
Version: types.MinVersion, Version: versionfmt.MinVersion,
}, },
{ {
Feature: f2, Feature: f2,
Version: types.MaxVersion, Version: versionfmt.MaxVersion,
}, },
} }

@ -1,4 +1,4 @@
// Copyright 2015 clair authors // Copyright 2016 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.

@ -29,8 +29,8 @@ const (
// namespace.go // namespace.go
soiNamespace = ` soiNamespace = `
WITH new_namespace AS ( WITH new_namespace AS (
INSERT INTO Namespace(name) INSERT INTO Namespace(name, version_format)
SELECT CAST($1 AS VARCHAR) SELECT CAST($1 AS VARCHAR), CAST($2 AS VARCHAR)
WHERE NOT EXISTS (SELECT name FROM Namespace WHERE name = $1) WHERE NOT EXISTS (SELECT name FROM Namespace WHERE name = $1)
RETURNING id RETURNING id
) )
@ -39,7 +39,7 @@ const (
SELECT id FROM new_namespace` SELECT id FROM new_namespace`
searchNamespace = `SELECT id FROM Namespace WHERE name = $1` searchNamespace = `SELECT id FROM Namespace WHERE name = $1`
listNamespace = `SELECT id, name FROM Namespace` listNamespace = `SELECT id, name, version_format FROM Namespace`
// feature.go // feature.go
soiFeature = ` soiFeature = `
@ -72,12 +72,12 @@ const (
WHERE feature_id = $1` WHERE feature_id = $1`
insertVulnerabilityAffectsFeatureVersion = ` insertVulnerabilityAffectsFeatureVersion = `
INSERT INTO Vulnerability_Affects_FeatureVersion(vulnerability_id, INSERT INTO Vulnerability_Affects_FeatureVersion(vulnerability_id, featureversion_id, fixedin_id)
featureversion_id, fixedin_id) VALUES($1, $2, $3)` VALUES($1, $2, $3)`
// layer.go // layer.go
searchLayer = ` searchLayer = `
SELECT l.id, l.name, l.engineversion, p.id, p.name, n.id, n.name SELECT l.id, l.name, l.engineversion, p.id, p.name, n.id, n.name, n.version_format
FROM Layer l FROM Layer l
LEFT JOIN Layer p ON l.parent_id = p.id LEFT JOIN Layer p ON l.parent_id = p.id
LEFT JOIN Namespace n ON l.namespace_id = n.id LEFT JOIN Namespace n ON l.namespace_id = n.id
@ -93,7 +93,7 @@ const (
FROM Layer l, layer_tree lt FROM Layer l, layer_tree lt
WHERE l.id = lt.parent_id WHERE l.id = lt.parent_id
) )
SELECT ldf.featureversion_id, ldf.modification, fn.id, fn.name, f.id, f.name, fv.id, fv.version, ltree.id, ltree.name SELECT ldf.featureversion_id, ldf.modification, fn.id, fn.name, fn.version_format, f.id, f.name, fv.id, fv.version, ltree.id, ltree.name
FROM Layer_diff_FeatureVersion ldf FROM Layer_diff_FeatureVersion ldf
JOIN ( JOIN (
SELECT row_number() over (ORDER BY depth DESC), id, name FROM layer_tree SELECT row_number() over (ORDER BY depth DESC), id, name FROM layer_tree
@ -103,7 +103,7 @@ const (
searchFeatureVersionVulnerability = ` searchFeatureVersionVulnerability = `
SELECT vafv.featureversion_id, v.id, v.name, v.description, v.link, v.severity, v.metadata, SELECT vafv.featureversion_id, v.id, v.name, v.description, v.link, v.severity, v.metadata,
vn.name, vfif.version vn.name, vn.version_format, vfif.version
FROM Vulnerability_Affects_FeatureVersion vafv, Vulnerability v, FROM Vulnerability_Affects_FeatureVersion vafv, Vulnerability v,
Namespace vn, Vulnerability_FixedIn_Feature vfif Namespace vn, Vulnerability_FixedIn_Feature vfif
WHERE vafv.featureversion_id = ANY($1::integer[]) WHERE vafv.featureversion_id = ANY($1::integer[])
@ -140,7 +140,7 @@ const (
// vulnerability.go // vulnerability.go
searchVulnerabilityBase = ` searchVulnerabilityBase = `
SELECT v.id, v.name, n.id, n.name, v.description, v.link, v.severity, v.metadata SELECT v.id, v.name, n.id, n.name, n.version_format, v.description, v.link, v.severity, v.metadata
FROM Vulnerability v JOIN Namespace n ON v.namespace_id = n.id` FROM Vulnerability v JOIN Namespace n ON v.namespace_id = n.id`
searchVulnerabilityForUpdate = ` FOR UPDATE OF v` searchVulnerabilityForUpdate = ` FOR UPDATE OF v`
searchVulnerabilityByNamespaceAndName = ` WHERE n.name = $1 AND v.name = $2 AND v.deleted_at IS NULL` searchVulnerabilityByNamespaceAndName = ` WHERE n.name = $1 AND v.name = $2 AND v.deleted_at IS NULL`

@ -12,9 +12,9 @@
-- See the License for the specific language governing permissions and -- See the License for the specific language governing permissions and
-- limitations under the License. -- limitations under the License.
INSERT INTO namespace (id, name) VALUES INSERT INTO namespace (id, name, version_format) VALUES
(1, 'debian:7'), (1, 'debian:7', 'dpkg'),
(2, 'debian:8'); (2, 'debian:8', 'dpkg');
INSERT INTO feature (id, namespace_id, name) VALUES INSERT INTO feature (id, namespace_id, name) VALUES
(1, 1, 'wechat'), (1, 1, 'wechat'),

@ -1,4 +1,4 @@
// Copyright 2015 clair authors // Copyright 2016 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -22,9 +22,9 @@ import (
"time" "time"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/ext/versionfmt"
"github.com/coreos/clair/utils" "github.com/coreos/clair/utils"
cerrors "github.com/coreos/clair/utils/errors" cerrors "github.com/coreos/clair/utils/errors"
"github.com/coreos/clair/utils/types"
"github.com/guregu/null/zero" "github.com/guregu/null/zero"
) )
@ -60,6 +60,7 @@ func (pgSQL *pgSQL) ListVulnerabilities(namespaceName string, limit int, startID
&vulnerability.Name, &vulnerability.Name,
&vulnerability.Namespace.ID, &vulnerability.Namespace.ID,
&vulnerability.Namespace.Name, &vulnerability.Namespace.Name,
&vulnerability.Namespace.VersionFormat,
&vulnerability.Description, &vulnerability.Description,
&vulnerability.Link, &vulnerability.Link,
&vulnerability.Severity, &vulnerability.Severity,
@ -117,6 +118,7 @@ func scanVulnerability(queryer Queryer, queryName string, vulnerabilityRow *sql.
&vulnerability.Name, &vulnerability.Name,
&vulnerability.Namespace.ID, &vulnerability.Namespace.ID,
&vulnerability.Namespace.Name, &vulnerability.Namespace.Name,
&vulnerability.Namespace.VersionFormat,
&vulnerability.Description, &vulnerability.Description,
&vulnerability.Link, &vulnerability.Link,
&vulnerability.Severity, &vulnerability.Severity,
@ -163,7 +165,7 @@ func scanVulnerability(queryer Queryer, queryName string, vulnerabilityRow *sql.
Namespace: vulnerability.Namespace, Namespace: vulnerability.Namespace,
Name: featureVersionFeatureName.String, Name: featureVersionFeatureName.String,
}, },
Version: types.NewVersionUnsafe(featureVersionVersion.String), Version: featureVersionVersion.String,
} }
vulnerability.FixedIn = append(vulnerability.FixedIn, featureVersion) vulnerability.FixedIn = append(vulnerability.FixedIn, featureVersion)
} }
@ -182,7 +184,6 @@ func (pgSQL *pgSQL) InsertVulnerabilities(vulnerabilities []database.Vulnerabili
for _, vulnerability := range vulnerabilities { for _, vulnerability := range vulnerabilities {
err := pgSQL.insertVulnerability(vulnerability, false, generateNotifications) err := pgSQL.insertVulnerability(vulnerability, false, generateNotifications)
if err != nil { if err != nil {
fmt.Printf("%#v\n", vulnerability)
return err return err
} }
} }
@ -274,7 +275,7 @@ func (pgSQL *pgSQL) insertVulnerability(vulnerability database.Vulnerability, on
// for diffing existing vulnerabilities. // for diffing existing vulnerabilities.
var fixedIn []database.FeatureVersion var fixedIn []database.FeatureVersion
for _, fv := range vulnerability.FixedIn { for _, fv := range vulnerability.FixedIn {
if fv.Version != types.MinVersion { if fv.Version != versionfmt.MinVersion {
fixedIn = append(fixedIn, fv) fixedIn = append(fixedIn, fv)
} }
} }
@ -350,7 +351,7 @@ func applyFixedInDiff(currentList, diff []database.FeatureVersion) ([]database.F
different := false different := false
for _, name := range addedNames { for _, name := range addedNames {
if diffMap[name].Version == types.MinVersion { if diffMap[name].Version == versionfmt.MinVersion {
// MinVersion only makes sense when a Feature is already fixed in some version, // MinVersion only makes sense when a Feature is already fixed in some version,
// in which case we would be in the "inBothNames". // in which case we would be in the "inBothNames".
continue continue
@ -363,7 +364,7 @@ func applyFixedInDiff(currentList, diff []database.FeatureVersion) ([]database.F
for _, name := range inBothNames { for _, name := range inBothNames {
fv := diffMap[name] fv := diffMap[name]
if fv.Version == types.MinVersion { if fv.Version == versionfmt.MinVersion {
// MinVersion means that the Feature doesn't affect the Vulnerability anymore. // MinVersion means that the Feature doesn't affect the Vulnerability anymore.
delete(currentMap, name) delete(currentMap, name)
different = true different = true
@ -453,7 +454,7 @@ func (pgSQL *pgSQL) insertVulnerabilityFixedInFeatureVersions(tx *sql.Tx, vulner
} }
// Insert Vulnerability_Affects_FeatureVersion. // Insert Vulnerability_Affects_FeatureVersion.
err = linkVulnerabilityToFeatureVersions(tx, fixedInID, vulnerabilityID, fv.Feature.ID, fv.Version) err = linkVulnerabilityToFeatureVersions(tx, fixedInID, vulnerabilityID, fv.Feature.ID, fv.Feature.Namespace.VersionFormat, fv.Version)
if err != nil { if err != nil {
return err return err
} }
@ -462,7 +463,7 @@ func (pgSQL *pgSQL) insertVulnerabilityFixedInFeatureVersions(tx *sql.Tx, vulner
return nil return nil
} }
func linkVulnerabilityToFeatureVersions(tx *sql.Tx, fixedInID, vulnerabilityID, featureID int, fixedInVersion types.Version) error { func linkVulnerabilityToFeatureVersions(tx *sql.Tx, fixedInID, vulnerabilityID, featureID int, versionFormat, fixedInVersion string) error {
// Find every FeatureVersions of the Feature that the vulnerability affects. // Find every FeatureVersions of the Feature that the vulnerability affects.
// TODO(Quentin-M): LIMIT // TODO(Quentin-M): LIMIT
rows, err := tx.Query(searchFeatureVersionByFeature, featureID) rows, err := tx.Query(searchFeatureVersionByFeature, featureID)
@ -480,7 +481,11 @@ func linkVulnerabilityToFeatureVersions(tx *sql.Tx, fixedInID, vulnerabilityID,
return handleError("searchFeatureVersionByFeature.Scan()", err) return handleError("searchFeatureVersionByFeature.Scan()", err)
} }
if affected.Version.Compare(fixedInVersion) < 0 { cmp, err := versionfmt.Compare(versionFormat, affected.Version, fixedInVersion)
if err != nil {
return err
}
if cmp < 0 {
// The version of the FeatureVersion is lower than the fixed version of this vulnerability, // The version of the FeatureVersion is lower than the fixed version of this vulnerability,
// thus, this FeatureVersion is affected by it. // thus, this FeatureVersion is affected by it.
affecteds = append(affecteds, affected) affecteds = append(affecteds, affected)
@ -494,8 +499,7 @@ func linkVulnerabilityToFeatureVersions(tx *sql.Tx, fixedInID, vulnerabilityID,
// Insert into Vulnerability_Affects_FeatureVersion. // Insert into Vulnerability_Affects_FeatureVersion.
for _, affected := range affecteds { for _, affected := range affecteds {
// TODO(Quentin-M): Batch me. // TODO(Quentin-M): Batch me.
_, err := tx.Exec(insertVulnerabilityAffectsFeatureVersion, vulnerabilityID, _, err := tx.Exec(insertVulnerabilityAffectsFeatureVersion, vulnerabilityID, affected.ID, fixedInID)
affected.ID, fixedInID)
if err != nil { if err != nil {
return handleError("insertVulnerabilityAffectsFeatureVersion", err) return handleError("insertVulnerabilityAffectsFeatureVersion", err)
} }
@ -534,7 +538,7 @@ func (pgSQL *pgSQL) DeleteVulnerabilityFix(vulnerabilityNamespace, vulnerability
Name: vulnerabilityNamespace, Name: vulnerabilityNamespace,
}, },
}, },
Version: types.MinVersion, Version: versionfmt.MinVersion,
}, },
}, },
} }

@ -1,4 +1,4 @@
// Copyright 2015 clair authors // Copyright 2016 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -21,8 +21,12 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/ext/versionfmt"
cerrors "github.com/coreos/clair/utils/errors" cerrors "github.com/coreos/clair/utils/errors"
"github.com/coreos/clair/utils/types" "github.com/coreos/clair/utils/types"
// dpkg versioning is used to parse test packages.
_ "github.com/coreos/clair/ext/versionfmt/dpkg"
) )
func TestFindVulnerability(t *testing.T) { func TestFindVulnerability(t *testing.T) {
@ -43,15 +47,18 @@ func TestFindVulnerability(t *testing.T) {
Description: "A vulnerability affecting OpenSSL < 2.0 on Debian 7.0", Description: "A vulnerability affecting OpenSSL < 2.0 on Debian 7.0",
Link: "http://google.com/#q=CVE-OPENSSL-1-DEB7", Link: "http://google.com/#q=CVE-OPENSSL-1-DEB7",
Severity: types.High, Severity: types.High,
Namespace: database.Namespace{Name: "debian:7"}, Namespace: database.Namespace{
Name: "debian:7",
VersionFormat: "dpkg",
},
FixedIn: []database.FeatureVersion{ FixedIn: []database.FeatureVersion{
{ {
Feature: database.Feature{Name: "openssl"}, Feature: database.Feature{Name: "openssl"},
Version: types.NewVersionUnsafe("2.0"), Version: "2.0",
}, },
{ {
Feature: database.Feature{Name: "libssl"}, Feature: database.Feature{Name: "libssl"},
Version: types.NewVersionUnsafe("1.9-abc"), Version: "1.9-abc",
}, },
}, },
} }
@ -65,8 +72,11 @@ func TestFindVulnerability(t *testing.T) {
v2 := database.Vulnerability{ v2 := database.Vulnerability{
Name: "CVE-NOPE", Name: "CVE-NOPE",
Description: "A vulnerability affecting nothing", Description: "A vulnerability affecting nothing",
Namespace: database.Namespace{Name: "debian:7"}, Namespace: database.Namespace{
Severity: types.Unknown, Name: "debian:7",
VersionFormat: "dpkg",
},
Severity: types.Unknown,
} }
v2f, err := datastore.FindVulnerability("debian:7", "CVE-NOPE") v2f, err := datastore.FindVulnerability("debian:7", "CVE-NOPE")
@ -106,58 +116,64 @@ func TestInsertVulnerability(t *testing.T) {
defer datastore.Close() defer datastore.Close()
// Create some data. // Create some data.
n1 := database.Namespace{Name: "TestInsertVulnerabilityNamespace1"} n1 := database.Namespace{
n2 := database.Namespace{Name: "TestInsertVulnerabilityNamespace2"} Name: "TestInsertVulnerabilityNamespace1",
VersionFormat: "dpkg",
}
n2 := database.Namespace{
Name: "TestInsertVulnerabilityNamespace2",
VersionFormat: "dpkg",
}
f1 := database.FeatureVersion{ f1 := database.FeatureVersion{
Feature: database.Feature{ Feature: database.Feature{
Name: "TestInsertVulnerabilityFeatureVersion1", Name: "TestInsertVulnerabilityFeatureVersion1",
Namespace: n1, Namespace: n1,
}, },
Version: types.NewVersionUnsafe("1.0"), Version: "1.0",
} }
f2 := database.FeatureVersion{ f2 := database.FeatureVersion{
Feature: database.Feature{ Feature: database.Feature{
Name: "TestInsertVulnerabilityFeatureVersion1", Name: "TestInsertVulnerabilityFeatureVersion1",
Namespace: n2, Namespace: n2,
}, },
Version: types.NewVersionUnsafe("1.0"), Version: "1.0",
} }
f3 := database.FeatureVersion{ f3 := database.FeatureVersion{
Feature: database.Feature{ Feature: database.Feature{
Name: "TestInsertVulnerabilityFeatureVersion2", Name: "TestInsertVulnerabilityFeatureVersion2",
}, },
Version: types.MaxVersion, Version: versionfmt.MaxVersion,
} }
f4 := database.FeatureVersion{ f4 := database.FeatureVersion{
Feature: database.Feature{ Feature: database.Feature{
Name: "TestInsertVulnerabilityFeatureVersion2", Name: "TestInsertVulnerabilityFeatureVersion2",
}, },
Version: types.NewVersionUnsafe("1.4"), Version: "1.4",
} }
f5 := database.FeatureVersion{ f5 := database.FeatureVersion{
Feature: database.Feature{ Feature: database.Feature{
Name: "TestInsertVulnerabilityFeatureVersion3", Name: "TestInsertVulnerabilityFeatureVersion3",
}, },
Version: types.NewVersionUnsafe("1.5"), Version: "1.5",
} }
f6 := database.FeatureVersion{ f6 := database.FeatureVersion{
Feature: database.Feature{ Feature: database.Feature{
Name: "TestInsertVulnerabilityFeatureVersion4", Name: "TestInsertVulnerabilityFeatureVersion4",
}, },
Version: types.NewVersionUnsafe("0.1"), Version: "0.1",
} }
f7 := database.FeatureVersion{ f7 := database.FeatureVersion{
Feature: database.Feature{ Feature: database.Feature{
Name: "TestInsertVulnerabilityFeatureVersion5", Name: "TestInsertVulnerabilityFeatureVersion5",
}, },
Version: types.MaxVersion, Version: versionfmt.MaxVersion,
} }
f8 := database.FeatureVersion{ f8 := database.FeatureVersion{
Feature: database.Feature{ Feature: database.Feature{
Name: "TestInsertVulnerabilityFeatureVersion5", Name: "TestInsertVulnerabilityFeatureVersion5",
}, },
Version: types.MinVersion, Version: versionfmt.MinVersion,
} }
// Insert invalid vulnerabilities. // Insert invalid vulnerabilities.

@ -0,0 +1,282 @@
// Copyright 2016 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dpkg
import (
"errors"
"strconv"
"strings"
"unicode"
"github.com/coreos/clair/ext/versionfmt"
)
type version struct {
epoch int
version string
revision string
}
var (
minVersion = version{version: versionfmt.MinVersion}
maxVersion = version{version: versionfmt.MaxVersion}
versionAllowedSymbols = []rune{'.', '-', '+', '~', ':', '_'}
revisionAllowedSymbols = []rune{'.', '+', '~', '_'}
)
// newVersion function parses a string into a Version struct which can be compared
//
// The implementation is based on http://man.he.net/man5/deb-version
// on https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version
//
// It uses the dpkg-1.17.25's algorithm (lib/parsehelp.c)
func newVersion(str string) (version, error) {
var v version
// Trim leading and trailing space
str = strings.TrimSpace(str)
if len(str) == 0 {
return version{}, errors.New("Version string is empty")
}
// Max/Min versions
if str == maxVersion.String() {
return maxVersion, nil
}
if str == minVersion.String() {
return minVersion, nil
}
// Find epoch
sepepoch := strings.Index(str, ":")
if sepepoch > -1 {
intepoch, err := strconv.Atoi(str[:sepepoch])
if err == nil {
v.epoch = intepoch
} else {
return version{}, errors.New("epoch in version is not a number")
}
if intepoch < 0 {
return version{}, errors.New("epoch in version is negative")
}
} else {
v.epoch = 0
}
// Find version / revision
seprevision := strings.LastIndex(str, "-")
if seprevision > -1 {
v.version = str[sepepoch+1 : seprevision]
v.revision = str[seprevision+1:]
} else {
v.version = str[sepepoch+1:]
v.revision = ""
}
// Verify format
if len(v.version) == 0 {
return version{}, errors.New("No version")
}
if !unicode.IsDigit(rune(v.version[0])) {
return version{}, errors.New("version does not start with digit")
}
for i := 0; i < len(v.version); i = i + 1 {
r := rune(v.version[i])
if !unicode.IsDigit(r) && !unicode.IsLetter(r) && !containsRune(versionAllowedSymbols, r) {
return version{}, errors.New("invalid character in version")
}
}
for i := 0; i < len(v.revision); i = i + 1 {
r := rune(v.revision[i])
if !unicode.IsDigit(r) && !unicode.IsLetter(r) && !containsRune(revisionAllowedSymbols, r) {
return version{}, errors.New("invalid character in revision")
}
}
return v, nil
}
type parser struct{}
func (p parser) Valid(str string) bool {
_, err := newVersion(str)
return err == nil
}
// Compare function compares two Debian-like package version
//
// The implementation is based on http://man.he.net/man5/deb-version
// on https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version
//
// It uses the dpkg-1.17.25's algorithm (lib/version.c)
func (p parser) Compare(a, b string) (int, error) {
v1, err := newVersion(a)
if err != nil {
return 0, err
}
v2, err := newVersion(b)
if err != nil {
return 0, err
}
// Quick check
if v1 == v2 {
return 0, nil
}
// Max/Min comparison
if v1 == minVersion || v2 == maxVersion {
return -1, nil
}
if v2 == minVersion || v1 == maxVersion {
return 1, nil
}
// Compare epochs
if v1.epoch > v2.epoch {
return 1, nil
}
if v1.epoch < v2.epoch {
return -1, nil
}
// Compare version
rc := verrevcmp(v1.version, v2.version)
if rc != 0 {
return signum(rc), nil
}
// Compare revision
return signum(verrevcmp(v1.revision, v2.revision)), nil
}
// String returns the string representation of a Version.
func (v version) String() (s string) {
if v.epoch != 0 {
s = strconv.Itoa(v.epoch) + ":"
}
s += v.version
if v.revision != "" {
s += "-" + v.revision
}
return
}
func verrevcmp(t1, t2 string) int {
t1, rt1 := nextRune(t1)
t2, rt2 := nextRune(t2)
for rt1 != nil || rt2 != nil {
firstDiff := 0
for (rt1 != nil && !unicode.IsDigit(*rt1)) || (rt2 != nil && !unicode.IsDigit(*rt2)) {
ac := 0
bc := 0
if rt1 != nil {
ac = order(*rt1)
}
if rt2 != nil {
bc = order(*rt2)
}
if ac != bc {
return ac - bc
}
t1, rt1 = nextRune(t1)
t2, rt2 = nextRune(t2)
}
for rt1 != nil && *rt1 == '0' {
t1, rt1 = nextRune(t1)
}
for rt2 != nil && *rt2 == '0' {
t2, rt2 = nextRune(t2)
}
for rt1 != nil && unicode.IsDigit(*rt1) && rt2 != nil && unicode.IsDigit(*rt2) {
if firstDiff == 0 {
firstDiff = int(*rt1) - int(*rt2)
}
t1, rt1 = nextRune(t1)
t2, rt2 = nextRune(t2)
}
if rt1 != nil && unicode.IsDigit(*rt1) {
return 1
}
if rt2 != nil && unicode.IsDigit(*rt2) {
return -1
}
if firstDiff != 0 {
return firstDiff
}
}
return 0
}
// order compares runes using a modified ASCII table
// so that letters are sorted earlier than non-letters
// and so that tildes sorts before anything
func order(r rune) int {
if unicode.IsDigit(r) {
return 0
}
if unicode.IsLetter(r) {
return int(r)
}
if r == '~' {
return -1
}
return int(r) + 256
}
func nextRune(str string) (string, *rune) {
if len(str) >= 1 {
r := rune(str[0])
return str[1:], &r
}
return str, nil
}
func containsRune(s []rune, e rune) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
func signum(a int) int {
switch {
case a < 0:
return -1
case a > 0:
return +1
}
return 0
}
func init() {
versionfmt.RegisterParser("dpkg", parser{})
}

@ -0,0 +1,197 @@
// Copyright 2016 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dpkg
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
const (
LESS = -1
EQUAL = 0
GREATER = 1
)
func TestParse(t *testing.T) {
cases := []struct {
str string
ver version
err bool
}{
// Test 0
{"0", version{epoch: 0, version: "0", revision: ""}, false},
{"0:0", version{epoch: 0, version: "0", revision: ""}, false},
{"0:0-", version{epoch: 0, version: "0", revision: ""}, false},
{"0:0-0", version{epoch: 0, version: "0", revision: "0"}, false},
{"0:0.0-0.0", version{epoch: 0, version: "0.0", revision: "0.0"}, false},
// Test epoched
{"1:0", version{epoch: 1, version: "0", revision: ""}, false},
{"5:1", version{epoch: 5, version: "1", revision: ""}, false},
// Test multiple hypens
{"0:0-0-0", version{epoch: 0, version: "0-0", revision: "0"}, false},
{"0:0-0-0-0", version{epoch: 0, version: "0-0-0", revision: "0"}, false},
// Test multiple colons
{"0:0:0-0", version{epoch: 0, version: "0:0", revision: "0"}, false},
{"0:0:0:0-0", version{epoch: 0, version: "0:0:0", revision: "0"}, false},
// Test multiple hyphens and colons
{"0:0:0-0-0", version{epoch: 0, version: "0:0-0", revision: "0"}, false},
{"0:0-0:0-0", version{epoch: 0, version: "0-0:0", revision: "0"}, false},
// Test valid characters in version
{"0:09azAZ.-+~:_-0", version{epoch: 0, version: "09azAZ.-+~:_", revision: "0"}, false},
// Test valid characters in debian revision
{"0:0-azAZ09.+~_", version{epoch: 0, version: "0", revision: "azAZ09.+~_"}, false},
// Test version with leading and trailing spaces
{" 0:0-1", version{epoch: 0, version: "0", revision: "1"}, false},
{"0:0-1 ", version{epoch: 0, version: "0", revision: "1"}, false},
{" 0:0-1 ", version{epoch: 0, version: "0", revision: "1"}, false},
// Test empty version
{"", version{}, true},
{" ", version{}, true},
{"0:", version{}, true},
// Test version with embedded spaces
{"0:0 0-1", version{}, true},
// Test version with negative epoch
{"-1:0-1", version{}, true},
// Test invalid characters in epoch
{"a:0-0", version{}, true},
{"A:0-0", version{}, true},
// Test version not starting with a digit
{"0:abc3-0", version{}, true},
}
for _, c := range cases {
v, err := newVersion(c.str)
if c.err {
assert.Error(t, err, "When parsing '%s'", c.str)
} else {
assert.Nil(t, err, "When parsing '%s'", c.str)
}
assert.Equal(t, c.ver, v, "When parsing '%s'", c.str)
}
// Test invalid characters in version
versym := []rune{'!', '#', '@', '$', '%', '&', '/', '|', '\\', '<', '>', '(', ')', '[', ']', '{', '}', ';', ',', '=', '*', '^', '\''}
for _, r := range versym {
_, err := newVersion(strings.Join([]string{"0:0", string(r), "-0"}, ""))
assert.Error(t, err, "Parsing with invalid character '%s' in version should have failed", string(r))
}
// Test invalid characters in revision
versym = []rune{'!', '#', '@', '$', '%', '&', '/', '|', '\\', '<', '>', '(', ')', '[', ']', '{', '}', ':', ';', ',', '=', '*', '^', '\''}
for _, r := range versym {
_, err := newVersion(strings.Join([]string{"0:0-", string(r)}, ""))
assert.Error(t, err, "Parsing with invalid character '%s' in revision should have failed", string(r))
}
}
func TestParseAndCompare(t *testing.T) {
cases := []struct {
v1 string
expected int
v2 string
}{
{"7.6p2-4", GREATER, "7.6-0"},
{"1.0.3-3", GREATER, "1.0-1"},
{"1.3", GREATER, "1.2.2-2"},
{"1.3", GREATER, "1.2.2"},
// Some properties of text strings
{"0-pre", EQUAL, "0-pre"},
{"0-pre", LESS, "0-pree"},
{"1.1.6r2-2", GREATER, "1.1.6r-1"},
{"2.6b2-1", GREATER, "2.6b-2"},
{"98.1p5-1", LESS, "98.1-pre2-b6-2"},
{"0.4a6-2", GREATER, "0.4-1"},
{"1:3.0.5-2", LESS, "1:3.0.5.1"},
// epochs
{"1:0.4", GREATER, "10.3"},
{"1:1.25-4", LESS, "1:1.25-8"},
{"0:1.18.36", EQUAL, "1.18.36"},
{"1.18.36", GREATER, "1.18.35"},
{"0:1.18.36", GREATER, "1.18.35"},
// Funky, but allowed, characters in upstream version
{"9:1.18.36:5.4-20", LESS, "10:0.5.1-22"},
{"9:1.18.36:5.4-20", LESS, "9:1.18.36:5.5-1"},
{"9:1.18.36:5.4-20", LESS, " 9:1.18.37:4.3-22"},
{"1.18.36-0.17.35-18", GREATER, "1.18.36-19"},
// Junk
{"1:1.2.13-3", LESS, "1:1.2.13-3.1"},
{"2.0.7pre1-4", LESS, "2.0.7r-1"},
// if a version includes a dash, it should be the debrev dash - policy says so
{"0:0-0-0", GREATER, "0-0"},
// do we like strange versions? Yes we like strange versions…
{"0", EQUAL, "0"},
{"0", EQUAL, "00"},
// #205960
{"3.0~rc1-1", LESS, "3.0-1"},
// #573592 - debian policy 5.6.12
{"1.0", EQUAL, "1.0-0"},
{"0.2", LESS, "1.0-0"},
{"1.0", LESS, "1.0-0+b1"},
{"1.0", GREATER, "1.0-0~"},
// "steal" the testcases from (old perl) cupt
{"1.2.3", EQUAL, "1.2.3"}, // identical
{"4.4.3-2", EQUAL, "4.4.3-2"}, // identical
{"1:2ab:5", EQUAL, "1:2ab:5"}, // this is correct...
{"7:1-a:b-5", EQUAL, "7:1-a:b-5"}, // and this
{"57:1.2.3abYZ+~-4-5", EQUAL, "57:1.2.3abYZ+~-4-5"}, // and those too
{"1.2.3", EQUAL, "0:1.2.3"}, // zero epoch
{"1.2.3", EQUAL, "1.2.3-0"}, // zero revision
{"009", EQUAL, "9"}, // zeroes…
{"009ab5", EQUAL, "9ab5"}, // there as well
{"1.2.3", LESS, "1.2.3-1"}, // added non-zero revision
{"1.2.3", LESS, "1.2.4"}, // just bigger
{"1.2.4", GREATER, "1.2.3"}, // order doesn't matter
{"1.2.24", GREATER, "1.2.3"}, // bigger, eh?
{"0.10.0", GREATER, "0.8.7"}, // bigger, eh?
{"3.2", GREATER, "2.3"}, // major number rocks
{"1.3.2a", GREATER, "1.3.2"}, // letters rock
{"0.5.0~git", LESS, "0.5.0~git2"}, // numbers rock
{"2a", LESS, "21"}, // but not in all places
{"1.3.2a", LESS, "1.3.2b"}, // but there is another letter
{"1:1.2.3", GREATER, "1.2.4"}, // epoch rocks
{"1:1.2.3", LESS, "1:1.2.4"}, // bigger anyway
{"1.2a+~bCd3", LESS, "1.2a++"}, // tilde doesn't rock
{"1.2a+~bCd3", GREATER, "1.2a+~"}, // but first is longer!
{"5:2", GREATER, "304-2"}, // epoch rocks
{"5:2", LESS, "304:2"}, // so big epoch?
{"25:2", GREATER, "3:2"}, // 25 > 3, obviously
{"1:2:123", LESS, "1:12:3"}, // 12 > 2
{"1.2-5", LESS, "1.2-3-5"}, // 1.2 < 1.2-3
{"5.10.0", GREATER, "5.005"}, // preceding zeroes don't matters
{"3a9.8", LESS, "3.10.2"}, // letters are before all letter symbols
{"3a9.8", GREATER, "3~10"}, // but after the tilde
{"1.4+OOo3.0.0~", LESS, "1.4+OOo3.0.0-4"}, // another tilde check
{"2.4.7-1", LESS, "2.4.7-z"}, // revision comparing
{"1.002-1+b2", GREATER, "1.00"}, // whatever...
}
var (
p parser
cmp int
err error
)
for _, c := range cases {
cmp, err = p.Compare(c.v1, c.v2)
assert.Nil(t, err)
assert.Equal(t, c.expected, cmp, "%s vs. %s, = %d, expected %d", c.v1, c.v2, cmp, c.expected)
cmp, err = p.Compare(c.v2, c.v1)
assert.Nil(t, err)
assert.Equal(t, -c.expected, cmp, "%s vs. %s, = %d, expected %d", c.v2, c.v1, cmp, -c.expected)
}
}

@ -0,0 +1,114 @@
// Copyright 2016 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package versionfmt exposes functions to dynamically register formats used to
// parse Feature Versions.
package versionfmt
import (
"errors"
"sync"
"github.com/coreos/pkg/capnslog"
)
const (
// MinVersion is a special package version which is always sorted first.
MinVersion = "#MINV#"
// MaxVersion is a special package version which is always sorted last
MaxVersion = "#MAXV#"
)
var (
// ErrUnknownVersionFormat is returned when a function does not have enough
// context to determine the format of a version.
ErrUnknownVersionFormat = errors.New("unknown version format")
// ErrInvalidVersion is returned when a function needs to validate a version,
// but should return an error in the case where the version is invalid.
ErrInvalidVersion = errors.New("invalid version")
nlog = capnslog.NewPackageLogger("github.com/coreos/clair", "ext/versionfmt")
parsersM sync.Mutex
parsers = make(map[string]Parser)
)
// Parser represents any format that can compare two version strings.
type Parser interface {
// Valid attempts to parse a version string and returns its success.
Valid(string) bool
// Compare parses two different version strings.
// Returns 0 when equal, -1 when a < b, 1 when b < a.
Compare(a, b string) (int, error)
}
// RegisterParser provides a way to dynamically register an implementation of a
// Parser.
//
// If RegisterParser is called twice with the same name, the name is blank, or
// if the provided Parser is nil, this function panics.
func RegisterParser(name string, p Parser) {
if name == "" {
panic("Could not register a Parser with an empty name")
}
if p == nil {
panic("Could not register a nil Parser")
}
parsersM.Lock()
defer parsersM.Unlock()
if _, alreadyExists := parsers[name]; alreadyExists {
panic("Parser '" + name + "' is already registered")
}
parsers[name] = p
}
// GetParser returns the registered Parser with a provided name.
func GetParser(name string) (p Parser, exists bool) {
parsersM.Lock()
defer parsersM.Unlock()
p, exists = parsers[name]
return
}
// Valid is a helper function that will return an error if the version fails to
// validate with a given format.
func Valid(format, version string) error {
versionParser, exists := GetParser(format)
if !exists {
return ErrUnknownVersionFormat
}
if !versionParser.Valid(version) {
return ErrInvalidVersion
}
return nil
}
// Compare is a helper function that will compare two versions with a given
// format and return an error if there are any failures.
func Compare(format, versionA, versionB string) (int, error) {
versionParser, exists := GetParser(format)
if !exists {
return 0, ErrUnknownVersionFormat
}
return versionParser.Compare(versionA, versionB)
}

@ -0,0 +1,289 @@
// Copyright 2016 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rpm
import (
"errors"
"strconv"
"strings"
"unicode"
"github.com/coreos/clair/ext/versionfmt"
)
type version struct {
epoch int
version string
revision string
}
var (
minVersion = version{version: versionfmt.MinVersion}
maxVersion = version{version: versionfmt.MaxVersion}
versionAllowedSymbols = []rune{'.', '-', '+', '~', ':', '_'}
revisionAllowedSymbols = []rune{'.', '+', '~', '_'}
)
// newVersion function parses a string into a Version struct which can be compared
//
// The implementation is based on http://man.he.net/man5/deb-version
// on https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version
//
// It uses the dpkg-1.17.25's algorithm (lib/parsehelp.c)
func newVersion(str string) (version, error) {
var v version
// Trim leading and trailing space
str = strings.TrimSpace(str)
if len(str) == 0 {
return version{}, errors.New("Version string is empty")
}
// Max/Min versions
if str == maxVersion.String() {
return maxVersion, nil
}
if str == minVersion.String() {
return minVersion, nil
}
// Find epoch
sepepoch := strings.Index(str, ":")
if sepepoch > -1 {
intepoch, err := strconv.Atoi(str[:sepepoch])
if err == nil {
v.epoch = intepoch
} else {
return version{}, errors.New("epoch in version is not a number")
}
if intepoch < 0 {
return version{}, errors.New("epoch in version is negative")
}
} else {
v.epoch = 0
}
// Find version / revision
seprevision := strings.LastIndex(str, "-")
if seprevision > -1 {
v.version = str[sepepoch+1 : seprevision]
v.revision = str[seprevision+1:]
} else {
v.version = str[sepepoch+1:]
v.revision = ""
}
// Verify format
if len(v.version) == 0 {
return version{}, errors.New("No version")
}
if !unicode.IsDigit(rune(v.version[0])) {
return version{}, errors.New("version does not start with digit")
}
for i := 0; i < len(v.version); i = i + 1 {
r := rune(v.version[i])
if !unicode.IsDigit(r) && !unicode.IsLetter(r) && !containsRune(versionAllowedSymbols, r) {
return version{}, errors.New("invalid character in version")
}
}
for i := 0; i < len(v.revision); i = i + 1 {
r := rune(v.revision[i])
if !unicode.IsDigit(r) && !unicode.IsLetter(r) && !containsRune(revisionAllowedSymbols, r) {
return version{}, errors.New("invalid character in revision")
}
}
return v, nil
}
// newVersionUnsafe is just a wrapper around NewVersion that ignore potentiel
// parsing error. Useful for test purposes
func newVersionUnsafe(str string) version {
v, _ := newVersion(str)
return v
}
type parser struct{}
func (p parser) Valid(str string) bool {
_, err := newVersion(str)
return err == nil
}
// Compare function compares two Debian-like package version
//
// The implementation is based on http://man.he.net/man5/deb-version
// on https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version
//
// It uses the dpkg-1.17.25's algorithm (lib/version.c)
func (p parser) Compare(a, b string) (int, error) {
v1, err := newVersion(a)
if err != nil {
return 0, err
}
v2, err := newVersion(b)
if err != nil {
return 0, err
}
// Quick check
if v1 == v2 {
return 0, nil
}
// Max/Min comparison
if v1 == minVersion || v2 == maxVersion {
return -1, nil
}
if v2 == minVersion || v1 == maxVersion {
return 1, nil
}
// Compare epochs
if v1.epoch > v2.epoch {
return 1, nil
}
if v1.epoch < v2.epoch {
return -1, nil
}
// Compare version
rc := verrevcmp(v1.version, v2.version)
if rc != 0 {
return signum(rc), nil
}
// Compare revision
return signum(verrevcmp(v1.revision, v2.revision)), nil
}
// String returns the string representation of a Version.
func (v version) String() (s string) {
if v.epoch != 0 {
s = strconv.Itoa(v.epoch) + ":"
}
s += v.version
if v.revision != "" {
s += "-" + v.revision
}
return
}
func verrevcmp(t1, t2 string) int {
t1, rt1 := nextRune(t1)
t2, rt2 := nextRune(t2)
for rt1 != nil || rt2 != nil {
firstDiff := 0
for (rt1 != nil && !unicode.IsDigit(*rt1)) || (rt2 != nil && !unicode.IsDigit(*rt2)) {
ac := 0
bc := 0
if rt1 != nil {
ac = order(*rt1)
}
if rt2 != nil {
bc = order(*rt2)
}
if ac != bc {
return ac - bc
}
t1, rt1 = nextRune(t1)
t2, rt2 = nextRune(t2)
}
for rt1 != nil && *rt1 == '0' {
t1, rt1 = nextRune(t1)
}
for rt2 != nil && *rt2 == '0' {
t2, rt2 = nextRune(t2)
}
for rt1 != nil && unicode.IsDigit(*rt1) && rt2 != nil && unicode.IsDigit(*rt2) {
if firstDiff == 0 {
firstDiff = int(*rt1) - int(*rt2)
}
t1, rt1 = nextRune(t1)
t2, rt2 = nextRune(t2)
}
if rt1 != nil && unicode.IsDigit(*rt1) {
return 1
}
if rt2 != nil && unicode.IsDigit(*rt2) {
return -1
}
if firstDiff != 0 {
return firstDiff
}
}
return 0
}
// order compares runes using a modified ASCII table
// so that letters are sorted earlier than non-letters
// and so that tildes sorts before anything
func order(r rune) int {
if unicode.IsDigit(r) {
return 0
}
if unicode.IsLetter(r) {
return int(r)
}
if r == '~' {
return -1
}
return int(r) + 256
}
func nextRune(str string) (string, *rune) {
if len(str) >= 1 {
r := rune(str[0])
return str[1:], &r
}
return str, nil
}
func containsRune(s []rune, e rune) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
func signum(a int) int {
switch {
case a < 0:
return -1
case a > 0:
return +1
}
return 0
}
func init() {
versionfmt.RegisterParser("rpm", parser{})
}

@ -0,0 +1,197 @@
// Copyright 2016 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rpm
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
const (
LESS = -1
EQUAL = 0
GREATER = 1
)
func TestParse(t *testing.T) {
cases := []struct {
str string
ver version
err bool
}{
// Test 0
{"0", version{epoch: 0, version: "0", revision: ""}, false},
{"0:0", version{epoch: 0, version: "0", revision: ""}, false},
{"0:0-", version{epoch: 0, version: "0", revision: ""}, false},
{"0:0-0", version{epoch: 0, version: "0", revision: "0"}, false},
{"0:0.0-0.0", version{epoch: 0, version: "0.0", revision: "0.0"}, false},
// Test epoched
{"1:0", version{epoch: 1, version: "0", revision: ""}, false},
{"5:1", version{epoch: 5, version: "1", revision: ""}, false},
// Test multiple hypens
{"0:0-0-0", version{epoch: 0, version: "0-0", revision: "0"}, false},
{"0:0-0-0-0", version{epoch: 0, version: "0-0-0", revision: "0"}, false},
// Test multiple colons
{"0:0:0-0", version{epoch: 0, version: "0:0", revision: "0"}, false},
{"0:0:0:0-0", version{epoch: 0, version: "0:0:0", revision: "0"}, false},
// Test multiple hyphens and colons
{"0:0:0-0-0", version{epoch: 0, version: "0:0-0", revision: "0"}, false},
{"0:0-0:0-0", version{epoch: 0, version: "0-0:0", revision: "0"}, false},
// Test valid characters in version
{"0:09azAZ.-+~:_-0", version{epoch: 0, version: "09azAZ.-+~:_", revision: "0"}, false},
// Test valid characters in debian revision
{"0:0-azAZ09.+~_", version{epoch: 0, version: "0", revision: "azAZ09.+~_"}, false},
// Test version with leading and trailing spaces
{" 0:0-1", version{epoch: 0, version: "0", revision: "1"}, false},
{"0:0-1 ", version{epoch: 0, version: "0", revision: "1"}, false},
{" 0:0-1 ", version{epoch: 0, version: "0", revision: "1"}, false},
// Test empty version
{"", version{}, true},
{" ", version{}, true},
{"0:", version{}, true},
// Test version with embedded spaces
{"0:0 0-1", version{}, true},
// Test version with negative epoch
{"-1:0-1", version{}, true},
// Test invalid characters in epoch
{"a:0-0", version{}, true},
{"A:0-0", version{}, true},
// Test version not starting with a digit
{"0:abc3-0", version{}, true},
}
for _, c := range cases {
v, err := newVersion(c.str)
if c.err {
assert.Error(t, err, "When parsing '%s'", c.str)
} else {
assert.Nil(t, err, "When parsing '%s'", c.str)
}
assert.Equal(t, c.ver, v, "When parsing '%s'", c.str)
}
// Test invalid characters in version
versym := []rune{'!', '#', '@', '$', '%', '&', '/', '|', '\\', '<', '>', '(', ')', '[', ']', '{', '}', ';', ',', '=', '*', '^', '\''}
for _, r := range versym {
_, err := newVersion(strings.Join([]string{"0:0", string(r), "-0"}, ""))
assert.Error(t, err, "Parsing with invalid character '%s' in version should have failed", string(r))
}
// Test invalid characters in revision
versym = []rune{'!', '#', '@', '$', '%', '&', '/', '|', '\\', '<', '>', '(', ')', '[', ']', '{', '}', ':', ';', ',', '=', '*', '^', '\''}
for _, r := range versym {
_, err := newVersion(strings.Join([]string{"0:0-", string(r)}, ""))
assert.Error(t, err, "Parsing with invalid character '%s' in revision should have failed", string(r))
}
}
func TestParseAndCompare(t *testing.T) {
cases := []struct {
v1 string
expected int
v2 string
}{
{"7.6p2-4", GREATER, "7.6-0"},
{"1.0.3-3", GREATER, "1.0-1"},
{"1.3", GREATER, "1.2.2-2"},
{"1.3", GREATER, "1.2.2"},
// Some properties of text strings
{"0-pre", EQUAL, "0-pre"},
{"0-pre", LESS, "0-pree"},
{"1.1.6r2-2", GREATER, "1.1.6r-1"},
{"2.6b2-1", GREATER, "2.6b-2"},
{"98.1p5-1", LESS, "98.1-pre2-b6-2"},
{"0.4a6-2", GREATER, "0.4-1"},
{"1:3.0.5-2", LESS, "1:3.0.5.1"},
// epochs
{"1:0.4", GREATER, "10.3"},
{"1:1.25-4", LESS, "1:1.25-8"},
{"0:1.18.36", EQUAL, "1.18.36"},
{"1.18.36", GREATER, "1.18.35"},
{"0:1.18.36", GREATER, "1.18.35"},
// Funky, but allowed, characters in upstream version
{"9:1.18.36:5.4-20", LESS, "10:0.5.1-22"},
{"9:1.18.36:5.4-20", LESS, "9:1.18.36:5.5-1"},
{"9:1.18.36:5.4-20", LESS, " 9:1.18.37:4.3-22"},
{"1.18.36-0.17.35-18", GREATER, "1.18.36-19"},
// Junk
{"1:1.2.13-3", LESS, "1:1.2.13-3.1"},
{"2.0.7pre1-4", LESS, "2.0.7r-1"},
// if a version includes a dash, it should be the debrev dash - policy says so
{"0:0-0-0", GREATER, "0-0"},
// do we like strange versions? Yes we like strange versions…
{"0", EQUAL, "0"},
{"0", EQUAL, "00"},
// #205960
{"3.0~rc1-1", LESS, "3.0-1"},
// #573592 - debian policy 5.6.12
{"1.0", EQUAL, "1.0-0"},
{"0.2", LESS, "1.0-0"},
{"1.0", LESS, "1.0-0+b1"},
{"1.0", GREATER, "1.0-0~"},
// "steal" the testcases from (old perl) cupt
{"1.2.3", EQUAL, "1.2.3"}, // identical
{"4.4.3-2", EQUAL, "4.4.3-2"}, // identical
{"1:2ab:5", EQUAL, "1:2ab:5"}, // this is correct...
{"7:1-a:b-5", EQUAL, "7:1-a:b-5"}, // and this
{"57:1.2.3abYZ+~-4-5", EQUAL, "57:1.2.3abYZ+~-4-5"}, // and those too
{"1.2.3", EQUAL, "0:1.2.3"}, // zero epoch
{"1.2.3", EQUAL, "1.2.3-0"}, // zero revision
{"009", EQUAL, "9"}, // zeroes…
{"009ab5", EQUAL, "9ab5"}, // there as well
{"1.2.3", LESS, "1.2.3-1"}, // added non-zero revision
{"1.2.3", LESS, "1.2.4"}, // just bigger
{"1.2.4", GREATER, "1.2.3"}, // order doesn't matter
{"1.2.24", GREATER, "1.2.3"}, // bigger, eh?
{"0.10.0", GREATER, "0.8.7"}, // bigger, eh?
{"3.2", GREATER, "2.3"}, // major number rocks
{"1.3.2a", GREATER, "1.3.2"}, // letters rock
{"0.5.0~git", LESS, "0.5.0~git2"}, // numbers rock
{"2a", LESS, "21"}, // but not in all places
{"1.3.2a", LESS, "1.3.2b"}, // but there is another letter
{"1:1.2.3", GREATER, "1.2.4"}, // epoch rocks
{"1:1.2.3", LESS, "1:1.2.4"}, // bigger anyway
{"1.2a+~bCd3", LESS, "1.2a++"}, // tilde doesn't rock
{"1.2a+~bCd3", GREATER, "1.2a+~"}, // but first is longer!
{"5:2", GREATER, "304-2"}, // epoch rocks
{"5:2", LESS, "304:2"}, // so big epoch?
{"25:2", GREATER, "3:2"}, // 25 > 3, obviously
{"1:2:123", LESS, "1:12:3"}, // 12 > 2
{"1.2-5", LESS, "1.2-3-5"}, // 1.2 < 1.2-3
{"5.10.0", GREATER, "5.005"}, // preceding zeroes don't matters
{"3a9.8", LESS, "3.10.2"}, // letters are before all letter symbols
{"3a9.8", GREATER, "3~10"}, // but after the tilde
{"1.4+OOo3.0.0~", LESS, "1.4+OOo3.0.0-4"}, // another tilde check
{"2.4.7-1", LESS, "2.4.7-z"}, // revision comparing
{"1.002-1+b2", GREATER, "1.00"}, // whatever...
}
var (
p parser
cmp int
err error
)
for _, c := range cases {
cmp, err = p.Compare(c.v1, c.v2)
assert.Nil(t, err)
assert.Equal(t, c.expected, cmp, "%s vs. %s, = %d, expected %d", c.v1, c.v2, cmp, c.expected)
cmp, err = p.Compare(c.v2, c.v1)
assert.Nil(t, err)
assert.Equal(t, -c.expected, cmp, "%s vs. %s, = %d, expected %d", c.v2, c.v1, cmp, -c.expected)
}
}

@ -29,10 +29,14 @@ import (
"github.com/coreos/pkg/capnslog" "github.com/coreos/pkg/capnslog"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/ext/versionfmt"
"github.com/coreos/clair/updater" "github.com/coreos/clair/updater"
"github.com/coreos/clair/utils" "github.com/coreos/clair/utils"
cerrors "github.com/coreos/clair/utils/errors" cerrors "github.com/coreos/clair/utils/errors"
"github.com/coreos/clair/utils/types" "github.com/coreos/clair/utils/types"
// dpkg versioning is used to parse Alpine Linux packages.
_ "github.com/coreos/clair/ext/versionfmt/dpkg"
) )
const ( const (
@ -219,7 +223,7 @@ func parse33YAML(r io.Reader) (vulns []database.Vulnerability, err error) {
for _, pack := range file.Packages { for _, pack := range file.Packages {
pkg := pack.Pkg pkg := pack.Pkg
for _, fix := range pkg.Fixes { for _, fix := range pkg.Fixes {
version, err := types.NewVersion(pkg.Version) err = versionfmt.Valid("dpkg", pkg.Version)
if err != nil { if err != nil {
log.Warningf("could not parse package version '%s': %s. skipping", pkg.Version, err.Error()) log.Warningf("could not parse package version '%s': %s. skipping", pkg.Version, err.Error())
continue continue
@ -235,7 +239,7 @@ func parse33YAML(r io.Reader) (vulns []database.Vulnerability, err error) {
Namespace: database.Namespace{Name: "alpine:" + file.Distro}, Namespace: database.Namespace{Name: "alpine:" + file.Distro},
Name: pkg.Name, Name: pkg.Name,
}, },
Version: version, Version: pkg.Version,
}, },
}, },
}) })
@ -269,10 +273,10 @@ func parse34YAML(r io.Reader) (vulns []database.Vulnerability, err error) {
for _, pack := range file.Packages { for _, pack := range file.Packages {
pkg := pack.Pkg pkg := pack.Pkg
for versionStr, vulnStrs := range pkg.Fixes { for version, vulnStrs := range pkg.Fixes {
version, err := types.NewVersion(versionStr) err := versionfmt.Valid("dpkg", version)
if err != nil { if err != nil {
log.Warningf("could not parse package version '%s': %s. skipping", versionStr, err.Error()) log.Warningf("could not parse package version '%s': %s. skipping", version, err.Error())
continue continue
} }

@ -1,4 +1,4 @@
// Copyright 2015 clair authors // Copyright 2016 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -23,11 +23,16 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/coreos/pkg/capnslog"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/ext/versionfmt"
"github.com/coreos/clair/updater" "github.com/coreos/clair/updater"
cerrors "github.com/coreos/clair/utils/errors" cerrors "github.com/coreos/clair/utils/errors"
"github.com/coreos/clair/utils/types" "github.com/coreos/clair/utils/types"
"github.com/coreos/pkg/capnslog"
// dpkg versioning is used to parse Debian packages.
_ "github.com/coreos/clair/ext/versionfmt/dpkg"
) )
const ( const (
@ -168,23 +173,24 @@ func parseDebianJSON(data *jsonData) (vulnerabilities []database.Vulnerability,
} }
// Determine the version of the package the vulnerability affects. // Determine the version of the package the vulnerability affects.
var version types.Version var version string
var err error var err error
if releaseNode.FixedVersion == "0" { if releaseNode.FixedVersion == "0" {
// This means that the package is not affected by this vulnerability. // This means that the package is not affected by this vulnerability.
version = types.MinVersion version = versionfmt.MinVersion
} else if releaseNode.Status == "open" { } else if releaseNode.Status == "open" {
// Open means that the package is currently vulnerable in the latest // Open means that the package is currently vulnerable in the latest
// version of this Debian release. // version of this Debian release.
version = types.MaxVersion version = versionfmt.MaxVersion
} else if releaseNode.Status == "resolved" { } else if releaseNode.Status == "resolved" {
// Resolved means that the vulnerability has been fixed in // Resolved means that the vulnerability has been fixed in
// "fixed_version" (if affected). // "fixed_version" (if affected).
version, err = types.NewVersion(releaseNode.FixedVersion) err = versionfmt.Valid("dpkg", releaseNode.FixedVersion)
if err != nil { if err != nil {
log.Warningf("could not parse package version '%s': %s. skipping", releaseNode.FixedVersion, err.Error()) log.Warningf("could not parse package version '%s': %s. skipping", releaseNode.FixedVersion, err.Error())
continue continue
} }
version = releaseNode.FixedVersion
} }
// Create and add the feature version. // Create and add the feature version.

@ -1,4 +1,4 @@
// Copyright 2015 clair authors // Copyright 2016 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -21,6 +21,7 @@ import (
"testing" "testing"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/ext/versionfmt"
"github.com/coreos/clair/utils/types" "github.com/coreos/clair/utils/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -44,7 +45,7 @@ func TestDebianParser(t *testing.T) {
Namespace: database.Namespace{Name: "debian:8"}, Namespace: database.Namespace{Name: "debian:8"},
Name: "aptdaemon", Name: "aptdaemon",
}, },
Version: types.MaxVersion, Version: versionfmt.MaxVersion,
}, },
{ {
Feature: database.Feature{ Feature: database.Feature{
@ -52,7 +53,7 @@ func TestDebianParser(t *testing.T) {
Name: "aptdaemon", Name: "aptdaemon",
}, },
Version: types.NewVersionUnsafe("1.1.1+bzr982-1"), Version: "1.1.1+bzr982-1",
}, },
} }
@ -70,21 +71,21 @@ func TestDebianParser(t *testing.T) {
Namespace: database.Namespace{Name: "debian:8"}, Namespace: database.Namespace{Name: "debian:8"},
Name: "aptdaemon", Name: "aptdaemon",
}, },
Version: types.NewVersionUnsafe("0.7.0"), Version: "0.7.0",
}, },
{ {
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "debian:unstable"}, Namespace: database.Namespace{Name: "debian:unstable"},
Name: "aptdaemon", Name: "aptdaemon",
}, },
Version: types.NewVersionUnsafe("0.7.0"), Version: "0.7.0",
}, },
{ {
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "debian:8"}, Namespace: database.Namespace{Name: "debian:8"},
Name: "asterisk", Name: "asterisk",
}, },
Version: types.NewVersionUnsafe("0.5.56"), Version: "0.5.56",
}, },
} }
@ -102,7 +103,7 @@ func TestDebianParser(t *testing.T) {
Namespace: database.Namespace{Name: "debian:8"}, Namespace: database.Namespace{Name: "debian:8"},
Name: "asterisk", Name: "asterisk",
}, },
Version: types.MinVersion, Version: versionfmt.MinVersion,
}, },
} }

@ -1,4 +1,4 @@
// Copyright 2015 clair authors // Copyright 2016 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -24,10 +24,14 @@ import (
"strings" "strings"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/ext/versionfmt"
"github.com/coreos/clair/updater" "github.com/coreos/clair/updater"
cerrors "github.com/coreos/clair/utils/errors" cerrors "github.com/coreos/clair/utils/errors"
"github.com/coreos/clair/utils/types" "github.com/coreos/clair/utils/types"
"github.com/coreos/pkg/capnslog" "github.com/coreos/pkg/capnslog"
// rpm versioning is used to parse Oracle Linux packages.
_ "github.com/coreos/clair/ext/versionfmt/rpm"
) )
const ( const (
@ -98,7 +102,6 @@ func (f *OracleFetcher) FetchUpdate(datastore database.Datastore) (resp updater.
firstELSA = firstOracle5ELSA firstELSA = firstOracle5ELSA
} }
// Fetch the update list. // Fetch the update list.
r, err := http.Get(ovalURI) r, err := http.Get(ovalURI)
if err != nil { if err != nil {
@ -282,16 +285,20 @@ func toFeatureVersions(criteria criteria) []database.FeatureVersion {
} else if strings.Contains(c.Comment, " is earlier than ") { } else if strings.Contains(c.Comment, " is earlier than ") {
const prefixLen = len(" is earlier than ") const prefixLen = len(" is earlier than ")
featureVersion.Feature.Name = strings.TrimSpace(c.Comment[:strings.Index(c.Comment, " is earlier than ")]) featureVersion.Feature.Name = strings.TrimSpace(c.Comment[:strings.Index(c.Comment, " is earlier than ")])
featureVersion.Version, err = types.NewVersion(c.Comment[strings.Index(c.Comment, " is earlier than ")+prefixLen:]) version := c.Comment[strings.Index(c.Comment, " is earlier than ")+prefixLen:]
err := versionfmt.Valid("rpm", version)
if err != nil { if err != nil {
log.Warningf("could not parse package version '%s': %s. skipping", c.Comment[strings.Index(c.Comment, " is earlier than ")+prefixLen:], err.Error()) log.Warningf("could not parse package version '%s': %s. skipping", version, err.Error())
} else {
featureVersion.Version = version
} }
} }
} }
featureVersion.Feature.Namespace.Name = "oracle" + ":" + strconv.Itoa(osVersion) featureVersion.Feature.Namespace.Name = "oracle" + ":" + strconv.Itoa(osVersion)
featureVersion.Feature.Namespace.VersionFormat = "rpm"
if featureVersion.Feature.Namespace.Name != "" && featureVersion.Feature.Name != "" && featureVersion.Version.String() != "" { if featureVersion.Feature.Namespace.Name != "" && featureVersion.Feature.Name != "" && featureVersion.Version != "" {
featureVersionParameters[featureVersion.Feature.Namespace.Name+":"+featureVersion.Feature.Name] = featureVersion featureVersionParameters[featureVersion.Feature.Namespace.Name+":"+featureVersion.Feature.Name] = featureVersion
} else { } else {
log.Warningf("could not determine a valid package from criterions: %v", criterions) log.Warningf("could not determine a valid package from criterions: %v", criterions)

@ -1,4 +1,4 @@
// Copyright 2015 clair authors // Copyright 2016 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -23,6 +23,9 @@ import (
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/utils/types" "github.com/coreos/clair/utils/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
// rpm versioning is used to parse Oracle Linux packages.
_ "github.com/coreos/clair/ext/versionfmt/rpm"
) )
func TestOracleParser(t *testing.T) { func TestOracleParser(t *testing.T) {
@ -43,24 +46,33 @@ func TestOracleParser(t *testing.T) {
expectedFeatureVersions := []database.FeatureVersion{ expectedFeatureVersions := []database.FeatureVersion{
{ {
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "oracle:7"}, Namespace: database.Namespace{
Name: "xerces-c", Name: "oracle:7",
VersionFormat: "rpm",
},
Name: "xerces-c",
}, },
Version: types.NewVersionUnsafe("3.1.1-7.el7_1"), Version: "0:3.1.1-7.el7_1",
}, },
{ {
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "oracle:7"}, Namespace: database.Namespace{
Name: "xerces-c-devel", Name: "oracle:7",
VersionFormat: "rpm",
},
Name: "xerces-c-devel",
}, },
Version: types.NewVersionUnsafe("3.1.1-7.el7_1"), Version: "0:3.1.1-7.el7_1",
}, },
{ {
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "oracle:7"}, Namespace: database.Namespace{
Name: "xerces-c-doc", Name: "oracle:7",
VersionFormat: "rpm",
},
Name: "xerces-c-doc",
}, },
Version: types.NewVersionUnsafe("3.1.1-7.el7_1"), Version: "0:3.1.1-7.el7_1",
}, },
} }
@ -81,17 +93,23 @@ func TestOracleParser(t *testing.T) {
expectedFeatureVersions := []database.FeatureVersion{ expectedFeatureVersions := []database.FeatureVersion{
{ {
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "oracle:6"}, Namespace: database.Namespace{
Name: "firefox", Name: "oracle:6",
VersionFormat: "rpm",
},
Name: "firefox",
}, },
Version: types.NewVersionUnsafe("38.1.0-1.0.1.el6_6"), Version: "0:38.1.0-1.0.1.el6_6",
}, },
{ {
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "oracle:7"}, Namespace: database.Namespace{
Name: "firefox", Name: "oracle:7",
VersionFormat: "rpm",
},
Name: "firefox",
}, },
Version: types.NewVersionUnsafe("38.1.0-1.0.1.el7_1"), Version: "0:38.1.0-1.0.1.el7_1",
}, },
} }

@ -1,4 +1,4 @@
// Copyright 2015 clair authors // Copyright 2016 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -23,11 +23,16 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/coreos/pkg/capnslog"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/ext/versionfmt"
"github.com/coreos/clair/updater" "github.com/coreos/clair/updater"
cerrors "github.com/coreos/clair/utils/errors" cerrors "github.com/coreos/clair/utils/errors"
"github.com/coreos/clair/utils/types" "github.com/coreos/clair/utils/types"
"github.com/coreos/pkg/capnslog"
// rpm versioning is used to parse Oracle Linux packages.
_ "github.com/coreos/clair/ext/versionfmt/rpm"
) )
const ( const (
@ -283,9 +288,13 @@ func toFeatureVersions(criteria criteria) []database.FeatureVersion {
} else if strings.Contains(c.Comment, " is earlier than ") { } else if strings.Contains(c.Comment, " is earlier than ") {
const prefixLen = len(" is earlier than ") const prefixLen = len(" is earlier than ")
featureVersion.Feature.Name = strings.TrimSpace(c.Comment[:strings.Index(c.Comment, " is earlier than ")]) featureVersion.Feature.Name = strings.TrimSpace(c.Comment[:strings.Index(c.Comment, " is earlier than ")])
featureVersion.Version, err = types.NewVersion(c.Comment[strings.Index(c.Comment, " is earlier than ")+prefixLen:]) version := c.Comment[strings.Index(c.Comment, " is earlier than ")+prefixLen:]
err := versionfmt.Valid("rpm", version)
if err != nil { if err != nil {
log.Warningf("could not parse package version '%s': %s. skipping", c.Comment[strings.Index(c.Comment, " is earlier than ")+prefixLen:], err.Error()) log.Warningf("could not parse package version '%s': %s. skipping", version, err.Error())
} else {
featureVersion.Version = version
featureVersion.Feature.Namespace.VersionFormat = "rpm"
} }
} }
} }
@ -297,7 +306,7 @@ func toFeatureVersions(criteria criteria) []database.FeatureVersion {
continue continue
} }
if featureVersion.Feature.Namespace.Name != "" && featureVersion.Feature.Name != "" && featureVersion.Version.String() != "" { if featureVersion.Feature.Namespace.Name != "" && featureVersion.Feature.Name != "" && featureVersion.Version != "" {
featureVersionParameters[featureVersion.Feature.Namespace.Name+":"+featureVersion.Feature.Name] = featureVersion featureVersionParameters[featureVersion.Feature.Namespace.Name+":"+featureVersion.Feature.Name] = featureVersion
} else { } else {
log.Warningf("could not determine a valid package from criterions: %v", criterions) log.Warningf("could not determine a valid package from criterions: %v", criterions)

@ -1,4 +1,4 @@
// Copyright 2015 clair authors // Copyright 2016 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -23,6 +23,9 @@ import (
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/utils/types" "github.com/coreos/clair/utils/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
// rpm versioning is used to parse RHEL packages.
_ "github.com/coreos/clair/ext/versionfmt/rpm"
) )
func TestRHELParser(t *testing.T) { func TestRHELParser(t *testing.T) {
@ -41,24 +44,33 @@ func TestRHELParser(t *testing.T) {
expectedFeatureVersions := []database.FeatureVersion{ expectedFeatureVersions := []database.FeatureVersion{
{ {
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "centos:7"}, Namespace: database.Namespace{
Name: "xerces-c", Name: "centos:7",
VersionFormat: "rpm",
},
Name: "xerces-c",
}, },
Version: types.NewVersionUnsafe("3.1.1-7.el7_1"), Version: "0:3.1.1-7.el7_1",
}, },
{ {
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "centos:7"}, Namespace: database.Namespace{
Name: "xerces-c-devel", Name: "centos:7",
VersionFormat: "rpm",
},
Name: "xerces-c-devel",
}, },
Version: types.NewVersionUnsafe("3.1.1-7.el7_1"), Version: "0:3.1.1-7.el7_1",
}, },
{ {
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "centos:7"}, Namespace: database.Namespace{
Name: "xerces-c-doc", Name: "centos:7",
VersionFormat: "rpm",
},
Name: "xerces-c-doc",
}, },
Version: types.NewVersionUnsafe("3.1.1-7.el7_1"), Version: "0:3.1.1-7.el7_1",
}, },
} }
@ -79,17 +91,23 @@ func TestRHELParser(t *testing.T) {
expectedFeatureVersions := []database.FeatureVersion{ expectedFeatureVersions := []database.FeatureVersion{
{ {
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "centos:6"}, Namespace: database.Namespace{
Name: "firefox", Name: "centos:6",
VersionFormat: "rpm",
},
Name: "firefox",
}, },
Version: types.NewVersionUnsafe("38.1.0-1.el6_6"), Version: "0:38.1.0-1.el6_6",
}, },
{ {
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "centos:7"}, Namespace: database.Namespace{
Name: "firefox", Name: "centos:7",
VersionFormat: "rpm",
},
Name: "firefox",
}, },
Version: types.NewVersionUnsafe("38.1.0-1.el7_1"), Version: "0:38.1.0-1.el7_1",
}, },
} }

@ -26,12 +26,14 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/coreos/pkg/capnslog"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/ext/versionfmt"
"github.com/coreos/clair/updater" "github.com/coreos/clair/updater"
"github.com/coreos/clair/utils" "github.com/coreos/clair/utils"
cerrors "github.com/coreos/clair/utils/errors" cerrors "github.com/coreos/clair/utils/errors"
"github.com/coreos/clair/utils/types" "github.com/coreos/clair/utils/types"
"github.com/coreos/pkg/capnslog"
) )
const ( const (
@ -344,21 +346,22 @@ func parseUbuntuCVE(fileContent io.Reader) (vulnerability database.Vulnerability
continue continue
} }
var version types.Version var version string
if md["status"] == "released" { if md["status"] == "released" {
if md["note"] != "" { if md["note"] != "" {
var err error var err error
version, err = types.NewVersion(md["note"]) err = versionfmt.Valid("dpkg", md["note"])
if err != nil { if err != nil {
log.Warningf("could not parse package version '%s': %s. skipping", md["note"], err) log.Warningf("could not parse package version '%s': %s. skipping", md["note"], err)
} }
version = md["note"]
} }
} else if md["status"] == "not-affected" { } else if md["status"] == "not-affected" {
version = types.MinVersion version = versionfmt.MinVersion
} else { } else {
version = types.MaxVersion version = versionfmt.MaxVersion
} }
if version.String() == "" { if version == "" {
continue continue
} }

@ -1,4 +1,4 @@
// Copyright 2015 clair authors // Copyright 2016 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -20,9 +20,11 @@ import (
"runtime" "runtime"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/ext/versionfmt"
"github.com/coreos/clair/utils/types" "github.com/coreos/clair/utils/types"
"github.com/stretchr/testify/assert"
) )
func TestUbuntuParser(t *testing.T) { func TestUbuntuParser(t *testing.T) {
@ -48,21 +50,21 @@ func TestUbuntuParser(t *testing.T) {
Namespace: database.Namespace{Name: "ubuntu:14.04"}, Namespace: database.Namespace{Name: "ubuntu:14.04"},
Name: "libmspack", Name: "libmspack",
}, },
Version: types.MaxVersion, Version: versionfmt.MaxVersion,
}, },
{ {
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "ubuntu:15.04"}, Namespace: database.Namespace{Name: "ubuntu:15.04"},
Name: "libmspack", Name: "libmspack",
}, },
Version: types.NewVersionUnsafe("0.4-3"), Version: "0.4-3",
}, },
{ {
Feature: database.Feature{ Feature: database.Feature{
Namespace: database.Namespace{Name: "ubuntu:15.10"}, Namespace: database.Namespace{Name: "ubuntu:15.10"},
Name: "libmspack-anotherpkg", Name: "libmspack-anotherpkg",
}, },
Version: types.NewVersionUnsafe("0.1"), Version: "0.1",
}, },
} }

@ -1,12 +1,26 @@
// Copyright 2016 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package updater package updater
import ( import (
"fmt" "fmt"
"testing" "testing"
"github.com/coreos/clair/database"
"github.com/coreos/clair/utils/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/coreos/clair/database"
) )
func TestDoVulnerabilitiesNamespacing(t *testing.T) { func TestDoVulnerabilitiesNamespacing(t *testing.T) {
@ -15,7 +29,7 @@ func TestDoVulnerabilitiesNamespacing(t *testing.T) {
Namespace: database.Namespace{Name: "Namespace1"}, Namespace: database.Namespace{Name: "Namespace1"},
Name: "Feature1", Name: "Feature1",
}, },
Version: types.NewVersionUnsafe("0.1"), Version: "0.1",
} }
fv2 := database.FeatureVersion{ fv2 := database.FeatureVersion{
@ -23,7 +37,7 @@ func TestDoVulnerabilitiesNamespacing(t *testing.T) {
Namespace: database.Namespace{Name: "Namespace2"}, Namespace: database.Namespace{Name: "Namespace2"},
Name: "Feature1", Name: "Feature1",
}, },
Version: types.NewVersionUnsafe("0.2"), Version: "0.2",
} }
fv3 := database.FeatureVersion{ fv3 := database.FeatureVersion{
@ -31,7 +45,7 @@ func TestDoVulnerabilitiesNamespacing(t *testing.T) {
Namespace: database.Namespace{Name: "Namespace2"}, Namespace: database.Namespace{Name: "Namespace2"},
Name: "Feature2", Name: "Feature2",
}, },
Version: types.NewVersionUnsafe("0.3"), Version: "0.3",
} }
vulnerability := database.Vulnerability{ vulnerability := database.Vulnerability{

@ -18,10 +18,14 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"github.com/coreos/pkg/capnslog"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/utils/types" "github.com/coreos/clair/ext/versionfmt"
"github.com/coreos/clair/worker/detectors" "github.com/coreos/clair/worker/detectors"
"github.com/coreos/pkg/capnslog"
// dpkg versioning is used to parse apk packages.
_ "github.com/coreos/clair/ext/versionfmt/dpkg"
) )
var log = capnslog.NewPackageLogger("github.com/coreos/clair", "worker/detectors/packages") var log = capnslog.NewPackageLogger("github.com/coreos/clair", "worker/detectors/packages")
@ -55,17 +59,19 @@ func (d *detector) Detect(data map[string][]byte) ([]database.FeatureVersion, er
case line[:2] == "P:": case line[:2] == "P:":
ipkg.Feature.Name = line[2:] ipkg.Feature.Name = line[2:]
case line[:2] == "V:": case line[:2] == "V:":
var err error version := string(line[2:])
ipkg.Version, err = types.NewVersion(line[2:]) err := versionfmt.Valid("dpkg", version)
if err != nil { if err != nil {
log.Warningf("could not parse package version '%s': %s. skipping", line[2:], err.Error()) log.Warningf("could not parse package version '%s': %s. skipping", version, err.Error())
} else {
ipkg.Version = version
} }
} }
// If we have a whole feature, store it in the set and try to parse a new // If we have a whole feature, store it in the set and try to parse a new
// one. // one.
if ipkg.Feature.Name != "" && ipkg.Version.String() != "" { if ipkg.Feature.Name != "" && ipkg.Version != "" {
pkgSet[ipkg.Feature.Name+"#"+ipkg.Version.String()] = ipkg pkgSet[ipkg.Feature.Name+"#"+ipkg.Version] = ipkg
ipkg = database.FeatureVersion{} ipkg = database.FeatureVersion{}
} }
} }

@ -18,7 +18,6 @@ import (
"testing" "testing"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/utils/types"
"github.com/coreos/clair/worker/detectors/feature" "github.com/coreos/clair/worker/detectors/feature"
) )
@ -28,47 +27,47 @@ func TestAPKFeatureDetection(t *testing.T) {
FeatureVersions: []database.FeatureVersion{ FeatureVersions: []database.FeatureVersion{
{ {
Feature: database.Feature{Name: "musl"}, Feature: database.Feature{Name: "musl"},
Version: types.NewVersionUnsafe("1.1.14-r10"), Version: "1.1.14-r10",
}, },
{ {
Feature: database.Feature{Name: "busybox"}, Feature: database.Feature{Name: "busybox"},
Version: types.NewVersionUnsafe("1.24.2-r9"), Version: "1.24.2-r9",
}, },
{ {
Feature: database.Feature{Name: "alpine-baselayout"}, Feature: database.Feature{Name: "alpine-baselayout"},
Version: types.NewVersionUnsafe("3.0.3-r0"), Version: "3.0.3-r0",
}, },
{ {
Feature: database.Feature{Name: "alpine-keys"}, Feature: database.Feature{Name: "alpine-keys"},
Version: types.NewVersionUnsafe("1.1-r0"), Version: "1.1-r0",
}, },
{ {
Feature: database.Feature{Name: "zlib"}, Feature: database.Feature{Name: "zlib"},
Version: types.NewVersionUnsafe("1.2.8-r2"), Version: "1.2.8-r2",
}, },
{ {
Feature: database.Feature{Name: "libcrypto1.0"}, Feature: database.Feature{Name: "libcrypto1.0"},
Version: types.NewVersionUnsafe("1.0.2h-r1"), Version: "1.0.2h-r1",
}, },
{ {
Feature: database.Feature{Name: "libssl1.0"}, Feature: database.Feature{Name: "libssl1.0"},
Version: types.NewVersionUnsafe("1.0.2h-r1"), Version: "1.0.2h-r1",
}, },
{ {
Feature: database.Feature{Name: "apk-tools"}, Feature: database.Feature{Name: "apk-tools"},
Version: types.NewVersionUnsafe("2.6.7-r0"), Version: "2.6.7-r0",
}, },
{ {
Feature: database.Feature{Name: "scanelf"}, Feature: database.Feature{Name: "scanelf"},
Version: types.NewVersionUnsafe("1.1.6-r0"), Version: "1.1.6-r0",
}, },
{ {
Feature: database.Feature{Name: "musl-utils"}, Feature: database.Feature{Name: "musl-utils"},
Version: types.NewVersionUnsafe("1.1.14-r10"), Version: "1.1.14-r10",
}, },
{ {
Feature: database.Feature{Name: "libc-utils"}, Feature: database.Feature{Name: "libc-utils"},
Version: types.NewVersionUnsafe("0.7-r0"), Version: "0.7-r0",
}, },
}, },
Data: map[string][]byte{ Data: map[string][]byte{

@ -19,10 +19,14 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/coreos/pkg/capnslog"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/utils/types" "github.com/coreos/clair/ext/versionfmt"
"github.com/coreos/clair/worker/detectors" "github.com/coreos/clair/worker/detectors"
"github.com/coreos/pkg/capnslog"
// dpkg versioning is used to parse dpkg packages.
_ "github.com/coreos/clair/ext/versionfmt/dpkg"
) )
var ( var (
@ -60,7 +64,7 @@ func (detector *DpkgFeaturesDetector) Detect(data map[string][]byte) ([]database
// Defines the name of the package // Defines the name of the package
pkg.Feature.Name = strings.TrimSpace(strings.TrimPrefix(line, "Package: ")) pkg.Feature.Name = strings.TrimSpace(strings.TrimPrefix(line, "Package: "))
pkg.Version = types.Version{} pkg.Version = ""
} else if strings.HasPrefix(line, "Source: ") { } else if strings.HasPrefix(line, "Source: ") {
// Source line (Optionnal) // Source line (Optionnal)
// Gives the name of the source package // Gives the name of the source package
@ -74,28 +78,34 @@ func (detector *DpkgFeaturesDetector) Detect(data map[string][]byte) ([]database
pkg.Feature.Name = md["name"] pkg.Feature.Name = md["name"]
if md["version"] != "" { if md["version"] != "" {
pkg.Version, err = types.NewVersion(md["version"]) version := md["version"]
err = versionfmt.Valid("dpkg", version)
if err != nil { if err != nil {
log.Warningf("could not parse package version '%s': %s. skipping", line[1], err.Error()) log.Warningf("could not parse package version '%s': %s. skipping", string(line[1]), err.Error())
} else {
pkg.Version = version
} }
} }
} else if strings.HasPrefix(line, "Version: ") && pkg.Version.String() == "" { } else if strings.HasPrefix(line, "Version: ") && pkg.Version == "" {
// Version line // Version line
// Defines the version of the package // Defines the version of the package
// This version is less important than a version retrieved from a Source line // This version is less important than a version retrieved from a Source line
// because the Debian vulnerabilities often skips the epoch from the Version field // because the Debian vulnerabilities often skips the epoch from the Version field
// which is not present in the Source version, and because +bX revisions don't matter // which is not present in the Source version, and because +bX revisions don't matter
pkg.Version, err = types.NewVersion(strings.TrimPrefix(line, "Version: ")) version := strings.TrimPrefix(line, "Version: ")
err = versionfmt.Valid("dpkg", version)
if err != nil { if err != nil {
log.Warningf("could not parse package version '%s': %s. skipping", line[1], err.Error()) log.Warningf("could not parse package version '%s': %s. skipping", string(line[1]), err.Error())
} else {
pkg.Version = version
} }
} }
// Add the package to the result array if we have all the informations // Add the package to the result array if we have all the informations
if pkg.Feature.Name != "" && pkg.Version.String() != "" { if pkg.Feature.Name != "" && pkg.Version != "" {
packagesMap[pkg.Feature.Name+"#"+pkg.Version.String()] = pkg packagesMap[pkg.Feature.Name+"#"+pkg.Version] = pkg
pkg.Feature.Name = "" pkg.Feature.Name = ""
pkg.Version = types.Version{} pkg.Version = ""
} }
} }

@ -18,7 +18,6 @@ import (
"testing" "testing"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/utils/types"
"github.com/coreos/clair/worker/detectors/feature" "github.com/coreos/clair/worker/detectors/feature"
) )
@ -30,15 +29,15 @@ func TestDpkgFeatureDetection(t *testing.T) {
// Two packages from this source are installed, it should only appear one time // Two packages from this source are installed, it should only appear one time
{ {
Feature: database.Feature{Name: "pam"}, Feature: database.Feature{Name: "pam"},
Version: types.NewVersionUnsafe("1.1.8-3.1ubuntu3"), Version: "1.1.8-3.1ubuntu3",
}, },
{ {
Feature: database.Feature{Name: "makedev"}, // The source name and the package name are equals Feature: database.Feature{Name: "makedev"}, // The source name and the package name are equals
Version: types.NewVersionUnsafe("2.3.1-93ubuntu1"), // The version comes from the "Version:" line Version: "2.3.1-93ubuntu1", // The version comes from the "Version:" line
}, },
{ {
Feature: database.Feature{Name: "gcc-5"}, Feature: database.Feature{Name: "gcc-5"},
Version: types.NewVersionUnsafe("5.1.1-12ubuntu1"), // The version comes from the "Source:" line Version: "5.1.1-12ubuntu1", // The version comes from the "Source:" line
}, },
}, },
Data: map[string][]byte{ Data: map[string][]byte{

@ -20,12 +20,16 @@ import (
"os" "os"
"strings" "strings"
"github.com/coreos/pkg/capnslog"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/ext/versionfmt"
"github.com/coreos/clair/utils" "github.com/coreos/clair/utils"
cerrors "github.com/coreos/clair/utils/errors" cerrors "github.com/coreos/clair/utils/errors"
"github.com/coreos/clair/utils/types"
"github.com/coreos/clair/worker/detectors" "github.com/coreos/clair/worker/detectors"
"github.com/coreos/pkg/capnslog"
// rpm versioning is used to parse rpm packages.
_ "github.com/coreos/clair/ext/versionfmt/rpm"
) )
var log = capnslog.NewPackageLogger("github.com/coreos/clair", "rpm") var log = capnslog.NewPackageLogger("github.com/coreos/clair", "rpm")
@ -88,7 +92,8 @@ func (detector *RpmFeaturesDetector) Detect(data map[string][]byte) ([]database.
} }
// Parse version // Parse version
version, err := types.NewVersion(strings.Replace(line[1], "(none):", "", -1)) version := strings.Replace(line[1], "(none):", "", -1)
err := versionfmt.Valid("rpm", version)
if err != nil { if err != nil {
log.Warningf("could not parse package version '%s': %s. skipping", line[1], err.Error()) log.Warningf("could not parse package version '%s': %s. skipping", line[1], err.Error())
continue continue
@ -101,7 +106,7 @@ func (detector *RpmFeaturesDetector) Detect(data map[string][]byte) ([]database.
}, },
Version: version, Version: version,
} }
packagesMap[pkg.Feature.Name+"#"+pkg.Version.String()] = pkg packagesMap[pkg.Feature.Name+"#"+pkg.Version] = pkg
} }
// Convert the map to a slice // Convert the map to a slice

@ -1,4 +1,4 @@
// Copyright 2015 clair authors // Copyright 2016 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -18,7 +18,6 @@ import (
"testing" "testing"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/utils/types"
"github.com/coreos/clair/worker/detectors/feature" "github.com/coreos/clair/worker/detectors/feature"
) )
@ -31,12 +30,12 @@ func TestRpmFeatureDetection(t *testing.T) {
// Two packages from this source are installed, it should only appear once // Two packages from this source are installed, it should only appear once
{ {
Feature: database.Feature{Name: "centos-release"}, Feature: database.Feature{Name: "centos-release"},
Version: types.NewVersionUnsafe("7-1.1503.el7.centos.2.8"), Version: "7-1.1503.el7.centos.2.8",
}, },
// Two packages from this source are installed, it should only appear once // Two packages from this source are installed, it should only appear once
{ {
Feature: database.Feature{Name: "filesystem"}, Feature: database.Feature{Name: "filesystem"},
Version: types.NewVersionUnsafe("3.2-18.el7"), Version: "3.2-18.el7",
}, },
}, },
Data: map[string][]byte{ Data: map[string][]byte{

@ -180,7 +180,7 @@ func detectFeatureVersions(name string, data map[string][]byte, namespace *datab
parentFeatureNamespaces := make(map[string]database.Namespace) parentFeatureNamespaces := make(map[string]database.Namespace)
if parent != nil { if parent != nil {
for _, parentFeature := range parent.Features { for _, parentFeature := range parent.Features {
parentFeatureNamespaces[parentFeature.Feature.Name+":"+parentFeature.Version.String()] = parentFeature.Feature.Namespace parentFeatureNamespaces[parentFeature.Feature.Name+":"+parentFeature.Version] = parentFeature.Feature.Namespace
} }
} }
@ -191,7 +191,7 @@ func detectFeatureVersions(name string, data map[string][]byte, namespace *datab
continue continue
} }
if parentFeatureNamespace, ok := parentFeatureNamespaces[feature.Feature.Name+":"+feature.Version.String()]; ok { if parentFeatureNamespace, ok := parentFeatureNamespaces[feature.Feature.Name+":"+feature.Version]; ok {
// The FeatureVersion is present in the parent layer; associate with their Namespace. // The FeatureVersion is present in the parent layer; associate with their Namespace.
features[i].Feature.Namespace = parentFeatureNamespace features[i].Feature.Namespace = parentFeatureNamespace
continue continue

@ -23,7 +23,6 @@ import (
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
cerrors "github.com/coreos/clair/utils/errors" cerrors "github.com/coreos/clair/utils/errors"
"github.com/coreos/clair/utils/types"
// Register the required detectors. // Register the required detectors.
_ "github.com/coreos/clair/worker/detectors/data/docker" _ "github.com/coreos/clair/worker/detectors/data/docker"
@ -62,14 +61,14 @@ func TestProcessWithDistUpgrade(t *testing.T) {
// Create the list of FeatureVersions that should not been upgraded from one layer to another. // Create the list of FeatureVersions that should not been upgraded from one layer to another.
nonUpgradedFeatureVersions := []database.FeatureVersion{ nonUpgradedFeatureVersions := []database.FeatureVersion{
{Feature: database.Feature{Name: "libtext-wrapi18n-perl"}, Version: types.NewVersionUnsafe("0.06-7")}, {Feature: database.Feature{Name: "libtext-wrapi18n-perl"}, Version: "0.06-7"},
{Feature: database.Feature{Name: "libtext-charwidth-perl"}, Version: types.NewVersionUnsafe("0.04-7")}, {Feature: database.Feature{Name: "libtext-charwidth-perl"}, Version: "0.04-7"},
{Feature: database.Feature{Name: "libtext-iconv-perl"}, Version: types.NewVersionUnsafe("1.7-5")}, {Feature: database.Feature{Name: "libtext-iconv-perl"}, Version: "1.7-5"},
{Feature: database.Feature{Name: "mawk"}, Version: types.NewVersionUnsafe("1.3.3-17")}, {Feature: database.Feature{Name: "mawk"}, Version: "1.3.3-17"},
{Feature: database.Feature{Name: "insserv"}, Version: types.NewVersionUnsafe("1.14.0-5")}, {Feature: database.Feature{Name: "insserv"}, Version: "1.14.0-5"},
{Feature: database.Feature{Name: "db"}, Version: types.NewVersionUnsafe("5.1.29-5")}, {Feature: database.Feature{Name: "db"}, Version: "5.1.29-5"},
{Feature: database.Feature{Name: "ustr"}, Version: types.NewVersionUnsafe("1.0.4-3")}, {Feature: database.Feature{Name: "ustr"}, Version: "1.0.4-3"},
{Feature: database.Feature{Name: "xz-utils"}, Version: types.NewVersionUnsafe("5.1.1alpha+20120614-2")}, {Feature: database.Feature{Name: "xz-utils"}, Version: "5.1.1alpha+20120614-2"},
} }
// Process test layers. // Process test layers.

Loading…
Cancel
Save