diff --git a/api/v1/models.go b/api/v1/models.go index b9a1afd4..ed52e77c 100644 --- a/api/v1/models.go +++ b/api/v1/models.go @@ -21,16 +21,18 @@ import ( "fmt" "time" - "github.com/coreos/clair/database" - "github.com/coreos/clair/utils/types" "github.com/coreos/pkg/capnslog" "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") type Error struct { - Message string `json:"Layer` + Message string `json:"Layer"` } type Layer struct { @@ -63,7 +65,8 @@ func LayerFromDatabaseModel(dbLayer database.Layer, withFeatures, withVulnerabil feature := Feature{ Name: dbFeatureVersion.Feature.Name, NamespaceName: dbFeatureVersion.Feature.Namespace.Name, - Version: dbFeatureVersion.Version.String(), + VersionFormat: dbFeatureVersion.Feature.Namespace.VersionFormat, + Version: dbFeatureVersion.Version, AddedBy: dbFeatureVersion.AddedBy.Name, } @@ -77,8 +80,8 @@ func LayerFromDatabaseModel(dbLayer database.Layer, withFeatures, withVulnerabil Metadata: dbVuln.Metadata, } - if dbVuln.FixedBy != types.MaxVersion { - vuln.FixedBy = dbVuln.FixedBy.String() + if dbVuln.FixedBy != versionfmt.MaxVersion { + vuln.FixedBy = dbVuln.FixedBy } feature.Vulnerabilities = append(feature.Vulnerabilities, vuln) } @@ -90,7 +93,8 @@ func LayerFromDatabaseModel(dbLayer database.Layer, withFeatures, withVulnerabil } type Namespace struct { - Name string `json:"Name,omitempty"` + Name string `json:"Name,omitempty"` + VersionFormat string `json:"VersionFormat,omitempty"` } type Vulnerability struct { @@ -153,44 +157,51 @@ func VulnerabilityFromDatabaseModel(dbVuln database.Vulnerability, withFixedIn b type Feature struct { Name string `json:"Name,omitempty"` NamespaceName string `json:"NamespaceName,omitempty"` + VersionFormat string `json:"VersionFormat,omitempty"` Version string `json:"Version,omitempty"` Vulnerabilities []Vulnerability `json:"Vulnerabilities,omitempty"` AddedBy string `json:"AddedBy,omitempty"` } func FeatureFromDatabaseModel(dbFeatureVersion database.FeatureVersion) Feature { - versionStr := dbFeatureVersion.Version.String() - if versionStr == types.MaxVersion.String() { - versionStr = "None" + version := dbFeatureVersion.Version + if version == versionfmt.MaxVersion { + version = "None" } return Feature{ Name: dbFeatureVersion.Feature.Name, NamespaceName: dbFeatureVersion.Feature.Namespace.Name, - Version: versionStr, + VersionFormat: dbFeatureVersion.Feature.Namespace.VersionFormat, + Version: version, AddedBy: dbFeatureVersion.AddedBy.Name, } } -func (f Feature) DatabaseModel() (database.FeatureVersion, error) { - var version types.Version +func (f Feature) DatabaseModel() (fv database.FeatureVersion, err error) { + var version string if f.Version == "None" { - version = types.MaxVersion + version = versionfmt.MaxVersion } else { - var err error - version, err = types.NewVersion(f.Version) + err = versionfmt.Valid(f.VersionFormat, f.Version) if err != nil { - return database.FeatureVersion{}, err + return } + version = f.Version } - return database.FeatureVersion{ + fv = database.FeatureVersion{ Feature: database.Feature{ - Name: f.Name, - Namespace: database.Namespace{Name: f.NamespaceName}, + Name: f.Name, + Namespace: database.Namespace{ + Name: f.NamespaceName, + VersionFormat: f.VersionFormat, + }, }, Version: version, - }, nil + } + + return } type Notification struct { diff --git a/api/v1/routes.go b/api/v1/routes.go index 482840bf..ed82ebe2 100644 --- a/api/v1/routes.go +++ b/api/v1/routes.go @@ -179,7 +179,10 @@ func getNamespaces(w http.ResponseWriter, r *http.Request, p httprouter.Params, } var namespaces []Namespace 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}) diff --git a/database/models.go b/database/models.go index a44291b8..898d2c9a 100644 --- a/database/models.go +++ b/database/models.go @@ -40,7 +40,8 @@ type Layer struct { type Namespace struct { Model - Name string + Name string + VersionFormat string } type Feature struct { @@ -54,7 +55,7 @@ type FeatureVersion struct { Model Feature Feature - Version types.Version + Version string AffectedBy []Vulnerability // 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 // is already about a specific Feature/FeatureVersion. - FixedBy types.Version `json:",omitempty"` + FixedBy string `json:",omitempty"` } type MetadataMap map[string]interface{} diff --git a/database/pgsql/complex_test.go b/database/pgsql/complex_test.go index 46ba504a..cd585a68 100644 --- a/database/pgsql/complex_test.go +++ b/database/pgsql/complex_test.go @@ -1,4 +1,4 @@ -// Copyright 2015 clair authors +// 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. @@ -29,6 +29,9 @@ import ( "github.com/coreos/clair/database" "github.com/coreos/clair/utils" "github.com/coreos/clair/utils/types" + + // dpkg versioning is used to parse test packages. + _ "github.com/coreos/clair/ext/versionfmt/dpkg" ) const ( @@ -46,8 +49,11 @@ func TestRaceAffects(t *testing.T) { // Insert the Feature on which we'll work. feature := database.Feature{ - Namespace: database.Namespace{Name: "TestRaceAffectsFeatureNamespace1"}, - Name: "TestRaceAffecturesFeature1", + Namespace: database.Namespace{ + Name: "TestRaceAffectsFeatureNamespace1", + VersionFormat: "dpkg", + }, + Name: "TestRaceAffecturesFeature1", } _, err = datastore.insertFeature(feature) if err != nil { @@ -66,7 +72,7 @@ func TestRaceAffects(t *testing.T) { featureVersions[i] = database.FeatureVersion{ Feature: feature, - Version: types.NewVersionUnsafe(strconv.Itoa(version)), + Version: strconv.Itoa(version), } } @@ -86,7 +92,7 @@ func TestRaceAffects(t *testing.T) { FixedIn: []database.FeatureVersion{ { Feature: feature, - Version: types.NewVersionUnsafe(strconv.Itoa(version)), + Version: strconv.Itoa(version), }, }, Severity: types.Unknown, @@ -126,7 +132,7 @@ func TestRaceAffects(t *testing.T) { var expectedAffectedNames []string for _, featureVersion := range featureVersions { - featureVersionVersion, _ := strconv.Atoi(featureVersion.Version.String()) + featureVersionVersion, _ := strconv.Atoi(featureVersion.Version) // Get actual affects. rows, err := datastore.Query(searchComplexTestFeatureVersionAffects, diff --git a/database/pgsql/feature.go b/database/pgsql/feature.go index c0b5a350..35a21a69 100644 --- a/database/pgsql/feature.go +++ b/database/pgsql/feature.go @@ -1,4 +1,4 @@ -// Copyright 2015 clair authors +// 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. @@ -16,11 +16,13 @@ package pgsql import ( "database/sql" + "fmt" + "strings" "time" "github.com/coreos/clair/database" + "github.com/coreos/clair/ext/versionfmt" cerrors "github.com/coreos/clair/utils/errors" - "github.com/coreos/clair/utils/types" ) 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 } -func (pgSQL *pgSQL) insertFeatureVersion(featureVersion database.FeatureVersion) (id int, err error) { - if featureVersion.Version.String() == "" { +func (pgSQL *pgSQL) insertFeatureVersion(fv database.FeatureVersion) (id int, err error) { + 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") } // 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 { promCacheQueriesTotal.WithLabelValues("featureversion").Inc() id, found := pgSQL.cache.Get(cacheIndex) @@ -82,30 +86,29 @@ func (pgSQL *pgSQL) insertFeatureVersion(featureVersion database.FeatureVersion) // Find or create Feature first. t := time.Now() - featureID, err := pgSQL.insertFeature(featureVersion.Feature) + featureID, err := pgSQL.insertFeature(fv.Feature) observeQueryTime("insertFeatureVersion", "insertFeature", t) if err != nil { return 0, err } - featureVersion.Feature.ID = featureID + fv.Feature.ID = featureID // Try to find the FeatureVersion. // // 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. - err = pgSQL.QueryRow(searchFeatureVersion, featureID, &featureVersion.Version). - Scan(&featureVersion.ID) + err = pgSQL.QueryRow(searchFeatureVersion, featureID, fv.Version).Scan(&fv.ID) if err != nil && err != sql.ErrNoRows { return 0, handleError("searchFeatureVersion", err) } if err == 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. @@ -132,8 +135,7 @@ func (pgSQL *pgSQL) insertFeatureVersion(featureVersion database.FeatureVersion) var created bool t = time.Now() - err = tx.QueryRow(soiFeatureVersion, featureID, &featureVersion.Version). - Scan(&created, &featureVersion.ID) + err = tx.QueryRow(soiFeatureVersion, featureID, fv.Version).Scan(&created, &fv.ID) observeQueryTime("insertFeatureVersion", "soiFeatureVersion", t) if err != nil { @@ -147,16 +149,16 @@ func (pgSQL *pgSQL) insertFeatureVersion(featureVersion database.FeatureVersion) tx.Commit() 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 // Vulnerability_Affects_FeatureVersion. t = time.Now() - err = linkFeatureVersionToVulnerabilities(tx, featureVersion) + err = linkFeatureVersionToVulnerabilities(tx, fv) observeQueryTime("insertFeatureVersion", "linkFeatureVersionToVulnerabilities", t) if err != nil { @@ -171,10 +173,10 @@ func (pgSQL *pgSQL) insertFeatureVersion(featureVersion database.FeatureVersion) } 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 @@ -195,7 +197,7 @@ func (pgSQL *pgSQL) insertFeatureVersions(featureVersions []database.FeatureVers type vulnerabilityAffectsFeatureVersion struct { vulnerabilityID int fixedInID int - fixedInVersion types.Version + fixedInVersion string } 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) } - 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 // Vulnerability, thus, this FeatureVersion is affected by it. affects = append(affects, affect) diff --git a/database/pgsql/feature_test.go b/database/pgsql/feature_test.go index a857c30f..cef5c037 100644 --- a/database/pgsql/feature_test.go +++ b/database/pgsql/feature_test.go @@ -1,4 +1,4 @@ -// Copyright 2015 clair authors +// 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. @@ -20,7 +20,9 @@ import ( "github.com/stretchr/testify/assert" "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) { @@ -45,8 +47,11 @@ func TestInsertFeature(t *testing.T) { // Insert Feature and ensure we can find it. feature := database.Feature{ - Namespace: database.Namespace{Name: "TestInsertFeatureNamespace1"}, - Name: "TestInsertFeature1", + Namespace: database.Namespace{ + Name: "TestInsertFeatureNamespace1", + VersionFormat: "dpkg", + }, + Name: "TestInsertFeature1", } id1, err := datastore.insertFeature(feature) assert.Nil(t, err) @@ -58,28 +63,34 @@ func TestInsertFeature(t *testing.T) { for _, invalidFeatureVersion := range []database.FeatureVersion{ { Feature: database.Feature{}, - Version: types.NewVersionUnsafe("1.0"), + Version: "1.0", }, { Feature: database.Feature{ Namespace: database.Namespace{}, Name: "TestInsertFeature2", }, - Version: types.NewVersionUnsafe("1.0"), + Version: "1.0", }, { Feature: database.Feature{ - Namespace: database.Namespace{Name: "TestInsertFeatureNamespace2"}, - Name: "TestInsertFeature2", + Namespace: database.Namespace{ + Name: "TestInsertFeatureNamespace2", + VersionFormat: "dpkg", + }, + Name: "TestInsertFeature2", }, - Version: types.NewVersionUnsafe(""), + Version: "", }, { Feature: database.Feature{ - Namespace: database.Namespace{Name: "TestInsertFeatureNamespace2"}, - Name: "TestInsertFeature2", + Namespace: database.Namespace{ + Name: "TestInsertFeatureNamespace2", + VersionFormat: "dpkg", + }, + Name: "TestInsertFeature2", }, - Version: types.NewVersionUnsafe("bad version"), + Version: "bad version", }, } { id3, err := datastore.insertFeatureVersion(invalidFeatureVersion) @@ -90,10 +101,13 @@ func TestInsertFeature(t *testing.T) { // Insert FeatureVersion and ensure we can find it. featureVersion := database.FeatureVersion{ Feature: database.Feature{ - Namespace: database.Namespace{Name: "TestInsertFeatureNamespace1"}, - Name: "TestInsertFeature1", + Namespace: database.Namespace{ + Name: "TestInsertFeatureNamespace1", + VersionFormat: "dpkg", + }, + Name: "TestInsertFeature1", }, - Version: types.NewVersionUnsafe("2:3.0-imba"), + Version: "2:3.0-imba", } id4, err := datastore.insertFeatureVersion(featureVersion) assert.Nil(t, err) diff --git a/database/pgsql/keyvalue_test.go b/database/pgsql/keyvalue_test.go index 5c7cc43f..4a8b6593 100644 --- a/database/pgsql/keyvalue_test.go +++ b/database/pgsql/keyvalue_test.go @@ -1,4 +1,4 @@ -// Copyright 2015 clair authors +// 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. diff --git a/database/pgsql/layer.go b/database/pgsql/layer.go index 66a41000..aab7cfe0 100644 --- a/database/pgsql/layer.go +++ b/database/pgsql/layer.go @@ -16,12 +16,14 @@ package pgsql import ( "database/sql" + "strings" "time" + "github.com/guregu/null/zero" + "github.com/coreos/clair/database" "github.com/coreos/clair/utils" cerrors "github.com/coreos/clair/utils/errors" - "github.com/guregu/null/zero" ) 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()) // Find the layer - var layer database.Layer - var parentID zero.Int - var parentName zero.String - var namespaceID zero.Int - var namespaceName sql.NullString + var ( + layer database.Layer + parentID zero.Int + parentName zero.String + nsID zero.Int + nsName sql.NullString + nsVersionFormat sql.NullString + ) 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) if err != nil { @@ -54,10 +68,11 @@ func (pgSQL *pgSQL) FindLayer(name string, withFeatures, withVulnerabilities boo Name: parentName.String, } } - if !namespaceID.IsZero() { + if !nsID.IsZero() { layer.Namespace = &database.Namespace{ - Model: database.Model{ID: int(namespaceID.Int64)}, - Name: namespaceName.String, + Model: database.Model{ID: int(nsID.Int64)}, + Name: nsName.String, + VersionFormat: nsVersionFormat.String, } } @@ -125,12 +140,20 @@ func getLayerFeatureVersions(tx *sql.Tx, layerID int) ([]database.FeatureVersion var modification string mapFeatureVersions := make(map[int]database.FeatureVersion) for rows.Next() { - var featureVersion database.FeatureVersion - - err = rows.Scan(&featureVersion.ID, &modification, &featureVersion.Feature.Namespace.ID, - &featureVersion.Feature.Namespace.Name, &featureVersion.Feature.ID, - &featureVersion.Feature.Name, &featureVersion.ID, &featureVersion.Version, - &featureVersion.AddedBy.ID, &featureVersion.AddedBy.Name) + var fv database.FeatureVersion + err = rows.Scan( + &fv.ID, + &modification, + &fv.Feature.Namespace.ID, + &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 { return featureVersions, handleError("searchLayerFeatureVersion.Scan()", err) } @@ -138,9 +161,9 @@ func getLayerFeatureVersions(tx *sql.Tx, layerID int) ([]database.FeatureVersion // Do transitive closure. switch modification { case "add": - mapFeatureVersions[featureVersion.ID] = featureVersion + mapFeatureVersions[fv.ID] = fv case "del": - delete(mapFeatureVersions, featureVersion.ID) + delete(mapFeatureVersions, fv.ID) default: log.Warningf("unknown Layer_diff_FeatureVersion's modification: %s", modification) return featureVersions, database.ErrInconsistent @@ -182,9 +205,18 @@ func loadAffectedBy(tx *sql.Tx, featureVersions []database.FeatureVersion) error var featureversionID int for rows.Next() { var vulnerability database.Vulnerability - err := rows.Scan(&featureversionID, &vulnerability.ID, &vulnerability.Name, - &vulnerability.Description, &vulnerability.Link, &vulnerability.Severity, - &vulnerability.Metadata, &vulnerability.Namespace.Name, &vulnerability.FixedBy) + err := rows.Scan( + &featureversionID, + &vulnerability.ID, + &vulnerability.Name, + &vulnerability.Description, + &vulnerability.Link, + &vulnerability.Severity, + &vulnerability.Metadata, + &vulnerability.Namespace.Name, + &vulnerability.Namespace.VersionFormat, + &vulnerability.FixedBy, + ) if err != nil { return handleError("searchFeatureVersionVulnerability.Scan()", err) } @@ -374,9 +406,9 @@ func createNV(features []database.FeatureVersion) (map[string]*database.FeatureV sliceNV := make([]string, 0, len(features)) for i := 0; i < len(features); i++ { - featureVersion := &features[i] - nv := featureVersion.Feature.Namespace.Name + ":" + featureVersion.Feature.Name + ":" + featureVersion.Version.String() - mapNV[nv] = featureVersion + fv := &features[i] + nv := strings.Join([]string{fv.Feature.Namespace.Name, fv.Feature.Name, fv.Version}, ":") + mapNV[nv] = fv sliceNV = append(sliceNV, nv) } diff --git a/database/pgsql/layer_test.go b/database/pgsql/layer_test.go index c45cbbed..48637678 100644 --- a/database/pgsql/layer_test.go +++ b/database/pgsql/layer_test.go @@ -1,4 +1,4 @@ -// Copyright 2015 clair authors +// 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. @@ -23,6 +23,9 @@ import ( "github.com/coreos/clair/database" cerrors "github.com/coreos/clair/utils/errors" "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) { @@ -67,9 +70,9 @@ func TestFindLayer(t *testing.T) { switch featureVersion.Feature.Name { case "wechat": - assert.Equal(t, types.NewVersionUnsafe("0.5"), featureVersion.Version) + assert.Equal(t, "0.5", featureVersion.Version) case "openssl": - assert.Equal(t, types.NewVersionUnsafe("1.0"), featureVersion.Version) + assert.Equal(t, "1.0", featureVersion.Version) default: t.Errorf("unexpected package %s for layer-1", featureVersion.Feature.Name) } @@ -83,9 +86,9 @@ func TestFindLayer(t *testing.T) { switch featureVersion.Feature.Name { case "wechat": - assert.Equal(t, types.NewVersionUnsafe("0.5"), featureVersion.Version) + assert.Equal(t, "0.5", featureVersion.Version) 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) { 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, "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, types.NewVersionUnsafe("2.0"), featureVersion.AffectedBy[0].FixedBy) + assert.Equal(t, "2.0", featureVersion.AffectedBy[0].FixedBy) } default: 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) { f1 := database.FeatureVersion{ Feature: database.Feature{ - Namespace: database.Namespace{Name: "TestInsertLayerNamespace2"}, - Name: "TestInsertLayerFeature1", + Namespace: database.Namespace{ + Name: "TestInsertLayerNamespace2", + VersionFormat: "dpkg", + }, + Name: "TestInsertLayerFeature1", }, - Version: types.NewVersionUnsafe("1.0"), + Version: "1.0", } f2 := database.FeatureVersion{ Feature: database.Feature{ - Namespace: database.Namespace{Name: "TestInsertLayerNamespace2"}, - Name: "TestInsertLayerFeature2", + Namespace: database.Namespace{ + Name: "TestInsertLayerNamespace2", + VersionFormat: "dpkg", + }, + Name: "TestInsertLayerFeature2", }, - Version: types.NewVersionUnsafe("0.34"), + Version: "0.34", } f3 := database.FeatureVersion{ Feature: database.Feature{ - Namespace: database.Namespace{Name: "TestInsertLayerNamespace2"}, - Name: "TestInsertLayerFeature3", + Namespace: database.Namespace{ + Name: "TestInsertLayerNamespace2", + VersionFormat: "dpkg", + }, + Name: "TestInsertLayerFeature3", }, - Version: types.NewVersionUnsafe("0.56"), + Version: "0.56", } f4 := database.FeatureVersion{ Feature: database.Feature{ - Namespace: database.Namespace{Name: "TestInsertLayerNamespace3"}, - Name: "TestInsertLayerFeature2", + Namespace: database.Namespace{ + Name: "TestInsertLayerNamespace3", + VersionFormat: "dpkg", + }, + Name: "TestInsertLayerFeature2", }, - Version: types.NewVersionUnsafe("0.34"), + Version: "0.34", } f5 := database.FeatureVersion{ Feature: database.Feature{ - Namespace: database.Namespace{Name: "TestInsertLayerNamespace3"}, - Name: "TestInsertLayerFeature3", + Namespace: database.Namespace{ + Name: "TestInsertLayerNamespace3", + VersionFormat: "dpkg", + }, + Name: "TestInsertLayerFeature3", }, - Version: types.NewVersionUnsafe("0.56"), + Version: "0.56", } f6 := database.FeatureVersion{ Feature: database.Feature{ - Namespace: database.Namespace{Name: "TestInsertLayerNamespace3"}, - Name: "TestInsertLayerFeature4", + Namespace: database.Namespace{ + Name: "TestInsertLayerNamespace3", + VersionFormat: "dpkg", + }, + Name: "TestInsertLayerFeature4", }, - Version: types.NewVersionUnsafe("0.666"), + Version: "0.666", } layers := []database.Layer{ @@ -185,16 +206,22 @@ func testInsertLayerTree(t *testing.T, datastore database.Datastore) { Name: "TestInsertLayer1", }, { - Name: "TestInsertLayer2", - Parent: &database.Layer{Name: "TestInsertLayer1"}, - Namespace: &database.Namespace{Name: "TestInsertLayerNamespace1"}, + Name: "TestInsertLayer2", + Parent: &database.Layer{Name: "TestInsertLayer1"}, + Namespace: &database.Namespace{ + Name: "TestInsertLayerNamespace1", + VersionFormat: "dpkg", + }, }, // This layer changes the namespace and adds Features. { - Name: "TestInsertLayer3", - Parent: &database.Layer{Name: "TestInsertLayer2"}, - Namespace: &database.Namespace{Name: "TestInsertLayerNamespace2"}, - Features: []database.FeatureVersion{f1, f2, f3}, + Name: "TestInsertLayer3", + Parent: &database.Layer{Name: "TestInsertLayer2"}, + Namespace: &database.Namespace{ + 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. { @@ -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 // Namespaces should then remain unchanged. { - Name: "TestInsertLayer4b", - Parent: &database.Layer{Name: "TestInsertLayer3"}, - Namespace: &database.Namespace{Name: "TestInsertLayerNamespace3"}, + Name: "TestInsertLayer4b", + Parent: &database.Layer{Name: "TestInsertLayer3"}, + Namespace: &database.Namespace{ + Name: "TestInsertLayerNamespace3", + VersionFormat: "dpkg", + }, Features: []database.FeatureVersion{ // Deletes TestInsertLayerFeature1. // 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) { f7 := database.FeatureVersion{ Feature: database.Feature{ - Namespace: database.Namespace{Name: "TestInsertLayerNamespace3"}, - Name: "TestInsertLayerFeature7", + Namespace: database.Namespace{ + Name: "TestInsertLayerNamespace3", + VersionFormat: "dpkg", + }, + Name: "TestInsertLayerFeature7", }, - Version: types.NewVersionUnsafe("0.01"), + Version: "0.01", } l3, _ := datastore.FindLayer("TestInsertLayer3", true, false) l3u := database.Layer{ - Name: l3.Name, - Parent: l3.Parent, - Namespace: &database.Namespace{Name: "TestInsertLayerNamespaceUpdated1"}, - Features: []database.FeatureVersion{f7}, + Name: l3.Name, + Parent: l3.Parent, + Namespace: &database.Namespace{ + Name: "TestInsertLayerNamespaceUpdated1", + VersionFormat: "dpkg", + }, + Features: []database.FeatureVersion{f7}, } l4u := database.Layer{ @@ -347,5 +383,5 @@ func testInsertLayerDelete(t *testing.T, datastore database.Datastore) { func cmpFV(a, b database.FeatureVersion) bool { return a.Feature.Name == b.Feature.Name && a.Feature.Namespace.Name == b.Feature.Namespace.Name && - a.Version.String() == b.Version.String() + a.Version == b.Version } diff --git a/database/pgsql/lock_test.go b/database/pgsql/lock_test.go index f2330f6f..cbd2d999 100644 --- a/database/pgsql/lock_test.go +++ b/database/pgsql/lock_test.go @@ -1,4 +1,4 @@ -// Copyright 2015 clair authors +// 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. diff --git a/database/pgsql/migrations/00006_add_version_format.go b/database/pgsql/migrations/00006_add_version_format.go new file mode 100644 index 00000000..b1e894e7 --- /dev/null +++ b/database/pgsql/migrations/00006_add_version_format.go @@ -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;`, + }), + }) +} diff --git a/database/pgsql/namespace.go b/database/pgsql/namespace.go index 3c85c784..941db0d1 100644 --- a/database/pgsql/namespace.go +++ b/database/pgsql/namespace.go @@ -38,7 +38,7 @@ func (pgSQL *pgSQL) insertNamespace(namespace database.Namespace) (int, error) { defer observeQueryTime("insertNamespace", "all", time.Now()) var id int - err := pgSQL.QueryRow(soiNamespace, namespace.Name).Scan(&id) + err := pgSQL.QueryRow(soiNamespace, namespace.Name, namespace.VersionFormat).Scan(&id) if err != nil { return 0, handleError("soiNamespace", err) } @@ -58,14 +58,14 @@ func (pgSQL *pgSQL) ListNamespaces() (namespaces []database.Namespace, err error defer rows.Close() 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 { return namespaces, handleError("listNamespace.Scan()", err) } - namespaces = append(namespaces, namespace) + namespaces = append(namespaces, ns) } if err = rows.Err(); err != nil { return namespaces, handleError("listNamespace.Rows()", err) diff --git a/database/pgsql/namespace_test.go b/database/pgsql/namespace_test.go index b9bf96fe..52f1c319 100644 --- a/database/pgsql/namespace_test.go +++ b/database/pgsql/namespace_test.go @@ -1,4 +1,4 @@ -// Copyright 2015 clair authors +// 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. @@ -21,6 +21,9 @@ import ( "github.com/stretchr/testify/assert" "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) { @@ -37,9 +40,15 @@ func TestInsertNamespace(t *testing.T) { assert.Zero(t, id0) // 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) - id2, err := datastore.insertNamespace(database.Namespace{Name: "TestInsertNamespace1"}) + id2, err := datastore.insertNamespace(database.Namespace{ + Name: "TestInsertNamespace1", + VersionFormat: "dpkg", + }) assert.Nil(t, err) assert.Equal(t, id1, id2) } diff --git a/database/pgsql/notification_test.go b/database/pgsql/notification_test.go index 3f90f349..afff0fd2 100644 --- a/database/pgsql/notification_test.go +++ b/database/pgsql/notification_test.go @@ -1,4 +1,4 @@ -// Copyright 2015 clair authors +// 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. @@ -21,8 +21,12 @@ import ( "github.com/stretchr/testify/assert" "github.com/coreos/clair/database" + "github.com/coreos/clair/ext/versionfmt" cerrors "github.com/coreos/clair/utils/errors" "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) { @@ -39,13 +43,19 @@ func TestNotification(t *testing.T) { // Create some data. f1 := database.Feature{ - Name: "TestNotificationFeature1", - Namespace: database.Namespace{Name: "TestNotificationNamespace1"}, + Name: "TestNotificationFeature1", + Namespace: database.Namespace{ + Name: "TestNotificationNamespace1", + VersionFormat: "dpkg", + }, } f2 := database.Feature{ - Name: "TestNotificationFeature2", - Namespace: database.Namespace{Name: "TestNotificationNamespace1"}, + Name: "TestNotificationFeature2", + Namespace: database.Namespace{ + Name: "TestNotificationNamespace1", + VersionFormat: "dpkg", + }, } l1 := database.Layer{ @@ -53,7 +63,7 @@ func TestNotification(t *testing.T) { Features: []database.FeatureVersion{ { Feature: f1, - Version: types.NewVersionUnsafe("0.1"), + Version: "0.1", }, }, } @@ -63,7 +73,7 @@ func TestNotification(t *testing.T) { Features: []database.FeatureVersion{ { Feature: f1, - Version: types.NewVersionUnsafe("0.2"), + Version: "0.2", }, }, } @@ -73,7 +83,7 @@ func TestNotification(t *testing.T) { Features: []database.FeatureVersion{ { Feature: f1, - Version: types.NewVersionUnsafe("0.3"), + Version: "0.3", }, }, } @@ -83,7 +93,7 @@ func TestNotification(t *testing.T) { Features: []database.FeatureVersion{ { Feature: f2, - Version: types.NewVersionUnsafe("0.1"), + Version: "0.1", }, }, } @@ -105,7 +115,7 @@ func TestNotification(t *testing.T) { FixedIn: []database.FeatureVersion{ { Feature: f1, - Version: types.NewVersionUnsafe("1.0"), + Version: "1.0", }, }, } @@ -165,11 +175,11 @@ func TestNotification(t *testing.T) { v1b.FixedIn = []database.FeatureVersion{ { Feature: f1, - Version: types.MinVersion, + Version: versionfmt.MinVersion, }, { Feature: f2, - Version: types.MaxVersion, + Version: versionfmt.MaxVersion, }, } diff --git a/database/pgsql/pgsql_test.go b/database/pgsql/pgsql_test.go index 58c516cc..b9ee97af 100644 --- a/database/pgsql/pgsql_test.go +++ b/database/pgsql/pgsql_test.go @@ -1,4 +1,4 @@ -// Copyright 2015 clair authors +// 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. diff --git a/database/pgsql/queries.go b/database/pgsql/queries.go index d84e4de2..49ccd014 100644 --- a/database/pgsql/queries.go +++ b/database/pgsql/queries.go @@ -29,8 +29,8 @@ const ( // namespace.go soiNamespace = ` WITH new_namespace AS ( - INSERT INTO Namespace(name) - SELECT CAST($1 AS VARCHAR) + INSERT INTO Namespace(name, version_format) + SELECT CAST($1 AS VARCHAR), CAST($2 AS VARCHAR) WHERE NOT EXISTS (SELECT name FROM Namespace WHERE name = $1) RETURNING id ) @@ -39,7 +39,7 @@ const ( SELECT id FROM new_namespace` searchNamespace = `SELECT id FROM Namespace WHERE name = $1` - listNamespace = `SELECT id, name FROM Namespace` + listNamespace = `SELECT id, name, version_format FROM Namespace` // feature.go soiFeature = ` @@ -72,12 +72,12 @@ const ( WHERE feature_id = $1` insertVulnerabilityAffectsFeatureVersion = ` - INSERT INTO Vulnerability_Affects_FeatureVersion(vulnerability_id, - featureversion_id, fixedin_id) VALUES($1, $2, $3)` + INSERT INTO Vulnerability_Affects_FeatureVersion(vulnerability_id, featureversion_id, fixedin_id) + VALUES($1, $2, $3)` // layer.go 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 LEFT JOIN Layer p ON l.parent_id = p.id LEFT JOIN Namespace n ON l.namespace_id = n.id @@ -93,7 +93,7 @@ const ( FROM Layer l, layer_tree lt 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 JOIN ( SELECT row_number() over (ORDER BY depth DESC), id, name FROM layer_tree @@ -103,7 +103,7 @@ const ( searchFeatureVersionVulnerability = ` 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, Namespace vn, Vulnerability_FixedIn_Feature vfif WHERE vafv.featureversion_id = ANY($1::integer[]) @@ -140,7 +140,7 @@ const ( // vulnerability.go 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` searchVulnerabilityForUpdate = ` FOR UPDATE OF v` searchVulnerabilityByNamespaceAndName = ` WHERE n.name = $1 AND v.name = $2 AND v.deleted_at IS NULL` diff --git a/database/pgsql/testdata/data.sql b/database/pgsql/testdata/data.sql index 7a48ef64..49a92985 100644 --- a/database/pgsql/testdata/data.sql +++ b/database/pgsql/testdata/data.sql @@ -12,9 +12,9 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -INSERT INTO namespace (id, name) VALUES - (1, 'debian:7'), - (2, 'debian:8'); +INSERT INTO namespace (id, name, version_format) VALUES + (1, 'debian:7', 'dpkg'), + (2, 'debian:8', 'dpkg'); INSERT INTO feature (id, namespace_id, name) VALUES (1, 1, 'wechat'), diff --git a/database/pgsql/vulnerability.go b/database/pgsql/vulnerability.go index 022aafec..201fa549 100644 --- a/database/pgsql/vulnerability.go +++ b/database/pgsql/vulnerability.go @@ -1,4 +1,4 @@ -// Copyright 2015 clair authors +// 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. @@ -22,9 +22,9 @@ import ( "time" "github.com/coreos/clair/database" + "github.com/coreos/clair/ext/versionfmt" "github.com/coreos/clair/utils" cerrors "github.com/coreos/clair/utils/errors" - "github.com/coreos/clair/utils/types" "github.com/guregu/null/zero" ) @@ -60,6 +60,7 @@ func (pgSQL *pgSQL) ListVulnerabilities(namespaceName string, limit int, startID &vulnerability.Name, &vulnerability.Namespace.ID, &vulnerability.Namespace.Name, + &vulnerability.Namespace.VersionFormat, &vulnerability.Description, &vulnerability.Link, &vulnerability.Severity, @@ -117,6 +118,7 @@ func scanVulnerability(queryer Queryer, queryName string, vulnerabilityRow *sql. &vulnerability.Name, &vulnerability.Namespace.ID, &vulnerability.Namespace.Name, + &vulnerability.Namespace.VersionFormat, &vulnerability.Description, &vulnerability.Link, &vulnerability.Severity, @@ -163,7 +165,7 @@ func scanVulnerability(queryer Queryer, queryName string, vulnerabilityRow *sql. Namespace: vulnerability.Namespace, Name: featureVersionFeatureName.String, }, - Version: types.NewVersionUnsafe(featureVersionVersion.String), + Version: featureVersionVersion.String, } vulnerability.FixedIn = append(vulnerability.FixedIn, featureVersion) } @@ -182,7 +184,6 @@ func (pgSQL *pgSQL) InsertVulnerabilities(vulnerabilities []database.Vulnerabili for _, vulnerability := range vulnerabilities { err := pgSQL.insertVulnerability(vulnerability, false, generateNotifications) if err != nil { - fmt.Printf("%#v\n", vulnerability) return err } } @@ -274,7 +275,7 @@ func (pgSQL *pgSQL) insertVulnerability(vulnerability database.Vulnerability, on // for diffing existing vulnerabilities. var fixedIn []database.FeatureVersion for _, fv := range vulnerability.FixedIn { - if fv.Version != types.MinVersion { + if fv.Version != versionfmt.MinVersion { fixedIn = append(fixedIn, fv) } } @@ -350,7 +351,7 @@ func applyFixedInDiff(currentList, diff []database.FeatureVersion) ([]database.F different := false 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, // in which case we would be in the "inBothNames". continue @@ -363,7 +364,7 @@ func applyFixedInDiff(currentList, diff []database.FeatureVersion) ([]database.F for _, name := range inBothNames { fv := diffMap[name] - if fv.Version == types.MinVersion { + if fv.Version == versionfmt.MinVersion { // MinVersion means that the Feature doesn't affect the Vulnerability anymore. delete(currentMap, name) different = true @@ -453,7 +454,7 @@ func (pgSQL *pgSQL) insertVulnerabilityFixedInFeatureVersions(tx *sql.Tx, vulner } // 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 { return err } @@ -462,7 +463,7 @@ func (pgSQL *pgSQL) insertVulnerabilityFixedInFeatureVersions(tx *sql.Tx, vulner 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. // TODO(Quentin-M): LIMIT rows, err := tx.Query(searchFeatureVersionByFeature, featureID) @@ -480,7 +481,11 @@ func linkVulnerabilityToFeatureVersions(tx *sql.Tx, fixedInID, vulnerabilityID, 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, // thus, this FeatureVersion is affected by it. affecteds = append(affecteds, affected) @@ -494,8 +499,7 @@ func linkVulnerabilityToFeatureVersions(tx *sql.Tx, fixedInID, vulnerabilityID, // Insert into Vulnerability_Affects_FeatureVersion. for _, affected := range affecteds { // TODO(Quentin-M): Batch me. - _, err := tx.Exec(insertVulnerabilityAffectsFeatureVersion, vulnerabilityID, - affected.ID, fixedInID) + _, err := tx.Exec(insertVulnerabilityAffectsFeatureVersion, vulnerabilityID, affected.ID, fixedInID) if err != nil { return handleError("insertVulnerabilityAffectsFeatureVersion", err) } @@ -534,7 +538,7 @@ func (pgSQL *pgSQL) DeleteVulnerabilityFix(vulnerabilityNamespace, vulnerability Name: vulnerabilityNamespace, }, }, - Version: types.MinVersion, + Version: versionfmt.MinVersion, }, }, } diff --git a/database/pgsql/vulnerability_test.go b/database/pgsql/vulnerability_test.go index 86edd1c1..125cedb5 100644 --- a/database/pgsql/vulnerability_test.go +++ b/database/pgsql/vulnerability_test.go @@ -1,4 +1,4 @@ -// Copyright 2015 clair authors +// 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. @@ -21,8 +21,12 @@ import ( "github.com/stretchr/testify/assert" "github.com/coreos/clair/database" + "github.com/coreos/clair/ext/versionfmt" cerrors "github.com/coreos/clair/utils/errors" "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) { @@ -43,15 +47,18 @@ func TestFindVulnerability(t *testing.T) { Description: "A vulnerability affecting OpenSSL < 2.0 on Debian 7.0", Link: "http://google.com/#q=CVE-OPENSSL-1-DEB7", Severity: types.High, - Namespace: database.Namespace{Name: "debian:7"}, + Namespace: database.Namespace{ + Name: "debian:7", + VersionFormat: "dpkg", + }, FixedIn: []database.FeatureVersion{ { Feature: database.Feature{Name: "openssl"}, - Version: types.NewVersionUnsafe("2.0"), + Version: "2.0", }, { 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{ Name: "CVE-NOPE", Description: "A vulnerability affecting nothing", - Namespace: database.Namespace{Name: "debian:7"}, - Severity: types.Unknown, + Namespace: database.Namespace{ + Name: "debian:7", + VersionFormat: "dpkg", + }, + Severity: types.Unknown, } v2f, err := datastore.FindVulnerability("debian:7", "CVE-NOPE") @@ -106,58 +116,64 @@ func TestInsertVulnerability(t *testing.T) { defer datastore.Close() // Create some data. - n1 := database.Namespace{Name: "TestInsertVulnerabilityNamespace1"} - n2 := database.Namespace{Name: "TestInsertVulnerabilityNamespace2"} + n1 := database.Namespace{ + Name: "TestInsertVulnerabilityNamespace1", + VersionFormat: "dpkg", + } + n2 := database.Namespace{ + Name: "TestInsertVulnerabilityNamespace2", + VersionFormat: "dpkg", + } f1 := database.FeatureVersion{ Feature: database.Feature{ Name: "TestInsertVulnerabilityFeatureVersion1", Namespace: n1, }, - Version: types.NewVersionUnsafe("1.0"), + Version: "1.0", } f2 := database.FeatureVersion{ Feature: database.Feature{ Name: "TestInsertVulnerabilityFeatureVersion1", Namespace: n2, }, - Version: types.NewVersionUnsafe("1.0"), + Version: "1.0", } f3 := database.FeatureVersion{ Feature: database.Feature{ Name: "TestInsertVulnerabilityFeatureVersion2", }, - Version: types.MaxVersion, + Version: versionfmt.MaxVersion, } f4 := database.FeatureVersion{ Feature: database.Feature{ Name: "TestInsertVulnerabilityFeatureVersion2", }, - Version: types.NewVersionUnsafe("1.4"), + Version: "1.4", } f5 := database.FeatureVersion{ Feature: database.Feature{ Name: "TestInsertVulnerabilityFeatureVersion3", }, - Version: types.NewVersionUnsafe("1.5"), + Version: "1.5", } f6 := database.FeatureVersion{ Feature: database.Feature{ Name: "TestInsertVulnerabilityFeatureVersion4", }, - Version: types.NewVersionUnsafe("0.1"), + Version: "0.1", } f7 := database.FeatureVersion{ Feature: database.Feature{ Name: "TestInsertVulnerabilityFeatureVersion5", }, - Version: types.MaxVersion, + Version: versionfmt.MaxVersion, } f8 := database.FeatureVersion{ Feature: database.Feature{ Name: "TestInsertVulnerabilityFeatureVersion5", }, - Version: types.MinVersion, + Version: versionfmt.MinVersion, } // Insert invalid vulnerabilities. diff --git a/ext/versionfmt/dpkg/parser.go b/ext/versionfmt/dpkg/parser.go new file mode 100644 index 00000000..e091b1dd --- /dev/null +++ b/ext/versionfmt/dpkg/parser.go @@ -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{}) +} diff --git a/ext/versionfmt/dpkg/parser_test.go b/ext/versionfmt/dpkg/parser_test.go new file mode 100644 index 00000000..e4897211 --- /dev/null +++ b/ext/versionfmt/dpkg/parser_test.go @@ -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) + } +} diff --git a/ext/versionfmt/driver.go b/ext/versionfmt/driver.go new file mode 100644 index 00000000..b277e0ab --- /dev/null +++ b/ext/versionfmt/driver.go @@ -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) +} diff --git a/ext/versionfmt/rpm/parser.go b/ext/versionfmt/rpm/parser.go new file mode 100644 index 00000000..b5f9eaf1 --- /dev/null +++ b/ext/versionfmt/rpm/parser.go @@ -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{}) +} diff --git a/ext/versionfmt/rpm/parser_test.go b/ext/versionfmt/rpm/parser_test.go new file mode 100644 index 00000000..f754d621 --- /dev/null +++ b/ext/versionfmt/rpm/parser_test.go @@ -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) + } +} diff --git a/updater/fetchers/alpine/alpine.go b/updater/fetchers/alpine/alpine.go index 49f445f6..64f66e69 100644 --- a/updater/fetchers/alpine/alpine.go +++ b/updater/fetchers/alpine/alpine.go @@ -29,10 +29,14 @@ import ( "github.com/coreos/pkg/capnslog" "github.com/coreos/clair/database" + "github.com/coreos/clair/ext/versionfmt" "github.com/coreos/clair/updater" "github.com/coreos/clair/utils" cerrors "github.com/coreos/clair/utils/errors" "github.com/coreos/clair/utils/types" + + // dpkg versioning is used to parse Alpine Linux packages. + _ "github.com/coreos/clair/ext/versionfmt/dpkg" ) const ( @@ -219,7 +223,7 @@ func parse33YAML(r io.Reader) (vulns []database.Vulnerability, err error) { for _, pack := range file.Packages { pkg := pack.Pkg for _, fix := range pkg.Fixes { - version, err := types.NewVersion(pkg.Version) + err = versionfmt.Valid("dpkg", pkg.Version) if err != nil { log.Warningf("could not parse package version '%s': %s. skipping", pkg.Version, err.Error()) continue @@ -235,7 +239,7 @@ func parse33YAML(r io.Reader) (vulns []database.Vulnerability, err error) { Namespace: database.Namespace{Name: "alpine:" + file.Distro}, 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 { pkg := pack.Pkg - for versionStr, vulnStrs := range pkg.Fixes { - version, err := types.NewVersion(versionStr) + for version, vulnStrs := range pkg.Fixes { + err := versionfmt.Valid("dpkg", version) 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 } diff --git a/updater/fetchers/debian/debian.go b/updater/fetchers/debian/debian.go index e21bd3f2..dca65357 100644 --- a/updater/fetchers/debian/debian.go +++ b/updater/fetchers/debian/debian.go @@ -1,4 +1,4 @@ -// Copyright 2015 clair authors +// 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. @@ -23,11 +23,16 @@ import ( "net/http" "strings" + "github.com/coreos/pkg/capnslog" + "github.com/coreos/clair/database" + "github.com/coreos/clair/ext/versionfmt" "github.com/coreos/clair/updater" cerrors "github.com/coreos/clair/utils/errors" "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 ( @@ -168,23 +173,24 @@ func parseDebianJSON(data *jsonData) (vulnerabilities []database.Vulnerability, } // Determine the version of the package the vulnerability affects. - var version types.Version + var version string var err error if releaseNode.FixedVersion == "0" { // This means that the package is not affected by this vulnerability. - version = types.MinVersion + version = versionfmt.MinVersion } else if releaseNode.Status == "open" { // Open means that the package is currently vulnerable in the latest // version of this Debian release. - version = types.MaxVersion + version = versionfmt.MaxVersion } else if releaseNode.Status == "resolved" { // Resolved means that the vulnerability has been fixed in // "fixed_version" (if affected). - version, err = types.NewVersion(releaseNode.FixedVersion) + err = versionfmt.Valid("dpkg", releaseNode.FixedVersion) if err != nil { log.Warningf("could not parse package version '%s': %s. skipping", releaseNode.FixedVersion, err.Error()) continue } + version = releaseNode.FixedVersion } // Create and add the feature version. diff --git a/updater/fetchers/debian/debian_test.go b/updater/fetchers/debian/debian_test.go index e092909d..495e7b51 100644 --- a/updater/fetchers/debian/debian_test.go +++ b/updater/fetchers/debian/debian_test.go @@ -1,4 +1,4 @@ -// Copyright 2015 clair authors +// 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. @@ -21,6 +21,7 @@ import ( "testing" "github.com/coreos/clair/database" + "github.com/coreos/clair/ext/versionfmt" "github.com/coreos/clair/utils/types" "github.com/stretchr/testify/assert" ) @@ -44,7 +45,7 @@ func TestDebianParser(t *testing.T) { Namespace: database.Namespace{Name: "debian:8"}, Name: "aptdaemon", }, - Version: types.MaxVersion, + Version: versionfmt.MaxVersion, }, { Feature: database.Feature{ @@ -52,7 +53,7 @@ func TestDebianParser(t *testing.T) { 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"}, Name: "aptdaemon", }, - Version: types.NewVersionUnsafe("0.7.0"), + Version: "0.7.0", }, { Feature: database.Feature{ Namespace: database.Namespace{Name: "debian:unstable"}, Name: "aptdaemon", }, - Version: types.NewVersionUnsafe("0.7.0"), + Version: "0.7.0", }, { Feature: database.Feature{ Namespace: database.Namespace{Name: "debian:8"}, 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"}, Name: "asterisk", }, - Version: types.MinVersion, + Version: versionfmt.MinVersion, }, } diff --git a/updater/fetchers/oracle/oracle.go b/updater/fetchers/oracle/oracle.go index 5d8f942c..44029cd7 100644 --- a/updater/fetchers/oracle/oracle.go +++ b/updater/fetchers/oracle/oracle.go @@ -1,4 +1,4 @@ -// Copyright 2015 clair authors +// 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. @@ -24,10 +24,14 @@ import ( "strings" "github.com/coreos/clair/database" + "github.com/coreos/clair/ext/versionfmt" "github.com/coreos/clair/updater" cerrors "github.com/coreos/clair/utils/errors" "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 ( @@ -98,7 +102,6 @@ func (f *OracleFetcher) FetchUpdate(datastore database.Datastore) (resp updater. firstELSA = firstOracle5ELSA } - // Fetch the update list. r, err := http.Get(ovalURI) if err != nil { @@ -282,16 +285,20 @@ func toFeatureVersions(criteria criteria) []database.FeatureVersion { } else if strings.Contains(c.Comment, " is earlier than ") { const prefixLen = len(" 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 { - 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.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 } else { log.Warningf("could not determine a valid package from criterions: %v", criterions) diff --git a/updater/fetchers/oracle/oracle_test.go b/updater/fetchers/oracle/oracle_test.go index 70855fdf..a0daa204 100644 --- a/updater/fetchers/oracle/oracle_test.go +++ b/updater/fetchers/oracle/oracle_test.go @@ -1,4 +1,4 @@ -// Copyright 2015 clair authors +// 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. @@ -23,6 +23,9 @@ import ( "github.com/coreos/clair/database" "github.com/coreos/clair/utils/types" "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) { @@ -43,24 +46,33 @@ func TestOracleParser(t *testing.T) { expectedFeatureVersions := []database.FeatureVersion{ { Feature: database.Feature{ - Namespace: database.Namespace{Name: "oracle:7"}, - Name: "xerces-c", + Namespace: database.Namespace{ + 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{ - Namespace: database.Namespace{Name: "oracle:7"}, - Name: "xerces-c-devel", + Namespace: database.Namespace{ + 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{ - Namespace: database.Namespace{Name: "oracle:7"}, - Name: "xerces-c-doc", + Namespace: database.Namespace{ + 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{ { Feature: database.Feature{ - Namespace: database.Namespace{Name: "oracle:6"}, - Name: "firefox", + Namespace: database.Namespace{ + 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{ - Namespace: database.Namespace{Name: "oracle:7"}, - Name: "firefox", + Namespace: database.Namespace{ + 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", }, } diff --git a/updater/fetchers/rhel/rhel.go b/updater/fetchers/rhel/rhel.go index 43d1d5fe..286d4500 100644 --- a/updater/fetchers/rhel/rhel.go +++ b/updater/fetchers/rhel/rhel.go @@ -1,4 +1,4 @@ -// Copyright 2015 clair authors +// 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. @@ -23,11 +23,16 @@ import ( "strconv" "strings" + "github.com/coreos/pkg/capnslog" + "github.com/coreos/clair/database" + "github.com/coreos/clair/ext/versionfmt" "github.com/coreos/clair/updater" cerrors "github.com/coreos/clair/utils/errors" "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 ( @@ -283,9 +288,13 @@ func toFeatureVersions(criteria criteria) []database.FeatureVersion { } else if strings.Contains(c.Comment, " is earlier than ") { const prefixLen = len(" 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 { - 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 } - 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 } else { log.Warningf("could not determine a valid package from criterions: %v", criterions) diff --git a/updater/fetchers/rhel/rhel_test.go b/updater/fetchers/rhel/rhel_test.go index 0e9ddbd0..8e3a215c 100644 --- a/updater/fetchers/rhel/rhel_test.go +++ b/updater/fetchers/rhel/rhel_test.go @@ -1,4 +1,4 @@ -// Copyright 2015 clair authors +// 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. @@ -23,6 +23,9 @@ import ( "github.com/coreos/clair/database" "github.com/coreos/clair/utils/types" "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) { @@ -41,24 +44,33 @@ func TestRHELParser(t *testing.T) { expectedFeatureVersions := []database.FeatureVersion{ { Feature: database.Feature{ - Namespace: database.Namespace{Name: "centos:7"}, - Name: "xerces-c", + Namespace: database.Namespace{ + 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{ - Namespace: database.Namespace{Name: "centos:7"}, - Name: "xerces-c-devel", + Namespace: database.Namespace{ + 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{ - Namespace: database.Namespace{Name: "centos:7"}, - Name: "xerces-c-doc", + Namespace: database.Namespace{ + 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{ { Feature: database.Feature{ - Namespace: database.Namespace{Name: "centos:6"}, - Name: "firefox", + Namespace: database.Namespace{ + 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{ - Namespace: database.Namespace{Name: "centos:7"}, - Name: "firefox", + Namespace: database.Namespace{ + Name: "centos:7", + VersionFormat: "rpm", + }, + Name: "firefox", }, - Version: types.NewVersionUnsafe("38.1.0-1.el7_1"), + Version: "0:38.1.0-1.el7_1", }, } diff --git a/updater/fetchers/ubuntu/ubuntu.go b/updater/fetchers/ubuntu/ubuntu.go index d5436906..d171ab90 100644 --- a/updater/fetchers/ubuntu/ubuntu.go +++ b/updater/fetchers/ubuntu/ubuntu.go @@ -26,12 +26,14 @@ import ( "strconv" "strings" + "github.com/coreos/pkg/capnslog" + "github.com/coreos/clair/database" + "github.com/coreos/clair/ext/versionfmt" "github.com/coreos/clair/updater" "github.com/coreos/clair/utils" cerrors "github.com/coreos/clair/utils/errors" "github.com/coreos/clair/utils/types" - "github.com/coreos/pkg/capnslog" ) const ( @@ -344,21 +346,22 @@ func parseUbuntuCVE(fileContent io.Reader) (vulnerability database.Vulnerability continue } - var version types.Version + var version string if md["status"] == "released" { if md["note"] != "" { var err error - version, err = types.NewVersion(md["note"]) + err = versionfmt.Valid("dpkg", md["note"]) if err != nil { log.Warningf("could not parse package version '%s': %s. skipping", md["note"], err) } + version = md["note"] } } else if md["status"] == "not-affected" { - version = types.MinVersion + version = versionfmt.MinVersion } else { - version = types.MaxVersion + version = versionfmt.MaxVersion } - if version.String() == "" { + if version == "" { continue } diff --git a/updater/fetchers/ubuntu/ubuntu_test.go b/updater/fetchers/ubuntu/ubuntu_test.go index d76d457e..e1c84637 100644 --- a/updater/fetchers/ubuntu/ubuntu_test.go +++ b/updater/fetchers/ubuntu/ubuntu_test.go @@ -1,4 +1,4 @@ -// Copyright 2015 clair authors +// 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. @@ -20,9 +20,11 @@ import ( "runtime" "testing" + "github.com/stretchr/testify/assert" + "github.com/coreos/clair/database" + "github.com/coreos/clair/ext/versionfmt" "github.com/coreos/clair/utils/types" - "github.com/stretchr/testify/assert" ) func TestUbuntuParser(t *testing.T) { @@ -48,21 +50,21 @@ func TestUbuntuParser(t *testing.T) { Namespace: database.Namespace{Name: "ubuntu:14.04"}, Name: "libmspack", }, - Version: types.MaxVersion, + Version: versionfmt.MaxVersion, }, { Feature: database.Feature{ Namespace: database.Namespace{Name: "ubuntu:15.04"}, Name: "libmspack", }, - Version: types.NewVersionUnsafe("0.4-3"), + Version: "0.4-3", }, { Feature: database.Feature{ Namespace: database.Namespace{Name: "ubuntu:15.10"}, Name: "libmspack-anotherpkg", }, - Version: types.NewVersionUnsafe("0.1"), + Version: "0.1", }, } diff --git a/updater/updater_test.go b/updater/updater_test.go index 5b6df0f0..c3672571 100644 --- a/updater/updater_test.go +++ b/updater/updater_test.go @@ -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 import ( "fmt" "testing" - "github.com/coreos/clair/database" - "github.com/coreos/clair/utils/types" "github.com/stretchr/testify/assert" + + "github.com/coreos/clair/database" ) func TestDoVulnerabilitiesNamespacing(t *testing.T) { @@ -15,7 +29,7 @@ func TestDoVulnerabilitiesNamespacing(t *testing.T) { Namespace: database.Namespace{Name: "Namespace1"}, Name: "Feature1", }, - Version: types.NewVersionUnsafe("0.1"), + Version: "0.1", } fv2 := database.FeatureVersion{ @@ -23,7 +37,7 @@ func TestDoVulnerabilitiesNamespacing(t *testing.T) { Namespace: database.Namespace{Name: "Namespace2"}, Name: "Feature1", }, - Version: types.NewVersionUnsafe("0.2"), + Version: "0.2", } fv3 := database.FeatureVersion{ @@ -31,7 +45,7 @@ func TestDoVulnerabilitiesNamespacing(t *testing.T) { Namespace: database.Namespace{Name: "Namespace2"}, Name: "Feature2", }, - Version: types.NewVersionUnsafe("0.3"), + Version: "0.3", } vulnerability := database.Vulnerability{ diff --git a/worker/detectors/feature/apk/apk.go b/worker/detectors/feature/apk/apk.go index 7fd39571..db641b74 100644 --- a/worker/detectors/feature/apk/apk.go +++ b/worker/detectors/feature/apk/apk.go @@ -18,10 +18,14 @@ import ( "bufio" "bytes" + "github.com/coreos/pkg/capnslog" + "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/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") @@ -55,17 +59,19 @@ func (d *detector) Detect(data map[string][]byte) ([]database.FeatureVersion, er case line[:2] == "P:": ipkg.Feature.Name = line[2:] case line[:2] == "V:": - var err error - ipkg.Version, err = types.NewVersion(line[2:]) + version := string(line[2:]) + err := versionfmt.Valid("dpkg", version) 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 // one. - if ipkg.Feature.Name != "" && ipkg.Version.String() != "" { - pkgSet[ipkg.Feature.Name+"#"+ipkg.Version.String()] = ipkg + if ipkg.Feature.Name != "" && ipkg.Version != "" { + pkgSet[ipkg.Feature.Name+"#"+ipkg.Version] = ipkg ipkg = database.FeatureVersion{} } } diff --git a/worker/detectors/feature/apk/apk_test.go b/worker/detectors/feature/apk/apk_test.go index 803a6657..ccf9b4c0 100644 --- a/worker/detectors/feature/apk/apk_test.go +++ b/worker/detectors/feature/apk/apk_test.go @@ -18,7 +18,6 @@ import ( "testing" "github.com/coreos/clair/database" - "github.com/coreos/clair/utils/types" "github.com/coreos/clair/worker/detectors/feature" ) @@ -28,47 +27,47 @@ func TestAPKFeatureDetection(t *testing.T) { FeatureVersions: []database.FeatureVersion{ { Feature: database.Feature{Name: "musl"}, - Version: types.NewVersionUnsafe("1.1.14-r10"), + Version: "1.1.14-r10", }, { Feature: database.Feature{Name: "busybox"}, - Version: types.NewVersionUnsafe("1.24.2-r9"), + Version: "1.24.2-r9", }, { Feature: database.Feature{Name: "alpine-baselayout"}, - Version: types.NewVersionUnsafe("3.0.3-r0"), + Version: "3.0.3-r0", }, { Feature: database.Feature{Name: "alpine-keys"}, - Version: types.NewVersionUnsafe("1.1-r0"), + Version: "1.1-r0", }, { Feature: database.Feature{Name: "zlib"}, - Version: types.NewVersionUnsafe("1.2.8-r2"), + Version: "1.2.8-r2", }, { Feature: database.Feature{Name: "libcrypto1.0"}, - Version: types.NewVersionUnsafe("1.0.2h-r1"), + Version: "1.0.2h-r1", }, { Feature: database.Feature{Name: "libssl1.0"}, - Version: types.NewVersionUnsafe("1.0.2h-r1"), + Version: "1.0.2h-r1", }, { Feature: database.Feature{Name: "apk-tools"}, - Version: types.NewVersionUnsafe("2.6.7-r0"), + Version: "2.6.7-r0", }, { Feature: database.Feature{Name: "scanelf"}, - Version: types.NewVersionUnsafe("1.1.6-r0"), + Version: "1.1.6-r0", }, { Feature: database.Feature{Name: "musl-utils"}, - Version: types.NewVersionUnsafe("1.1.14-r10"), + Version: "1.1.14-r10", }, { Feature: database.Feature{Name: "libc-utils"}, - Version: types.NewVersionUnsafe("0.7-r0"), + Version: "0.7-r0", }, }, Data: map[string][]byte{ diff --git a/worker/detectors/feature/dpkg/dpkg.go b/worker/detectors/feature/dpkg/dpkg.go index 6154d2ce..6b658d14 100644 --- a/worker/detectors/feature/dpkg/dpkg.go +++ b/worker/detectors/feature/dpkg/dpkg.go @@ -19,10 +19,14 @@ import ( "regexp" "strings" + "github.com/coreos/pkg/capnslog" + "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/pkg/capnslog" + + // dpkg versioning is used to parse dpkg packages. + _ "github.com/coreos/clair/ext/versionfmt/dpkg" ) var ( @@ -60,7 +64,7 @@ func (detector *DpkgFeaturesDetector) Detect(data map[string][]byte) ([]database // Defines the name of the package pkg.Feature.Name = strings.TrimSpace(strings.TrimPrefix(line, "Package: ")) - pkg.Version = types.Version{} + pkg.Version = "" } else if strings.HasPrefix(line, "Source: ") { // Source line (Optionnal) // 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"] if md["version"] != "" { - pkg.Version, err = types.NewVersion(md["version"]) + version := md["version"] + err = versionfmt.Valid("dpkg", version) 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 // Defines the version of the package // 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 // 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 { - 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 - if pkg.Feature.Name != "" && pkg.Version.String() != "" { - packagesMap[pkg.Feature.Name+"#"+pkg.Version.String()] = pkg + if pkg.Feature.Name != "" && pkg.Version != "" { + packagesMap[pkg.Feature.Name+"#"+pkg.Version] = pkg pkg.Feature.Name = "" - pkg.Version = types.Version{} + pkg.Version = "" } } diff --git a/worker/detectors/feature/dpkg/dpkg_test.go b/worker/detectors/feature/dpkg/dpkg_test.go index d6f26d8c..30f8ec5e 100644 --- a/worker/detectors/feature/dpkg/dpkg_test.go +++ b/worker/detectors/feature/dpkg/dpkg_test.go @@ -18,7 +18,6 @@ import ( "testing" "github.com/coreos/clair/database" - "github.com/coreos/clair/utils/types" "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 { 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 - Version: types.NewVersionUnsafe("2.3.1-93ubuntu1"), // The version comes from the "Version:" line + Feature: database.Feature{Name: "makedev"}, // The source name and the package name are equals + Version: "2.3.1-93ubuntu1", // The version comes from the "Version:" line }, { Feature: database.Feature{Name: "gcc-5"}, - Version: 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{ diff --git a/worker/detectors/feature/rpm/rpm.go b/worker/detectors/feature/rpm/rpm.go index b5012639..342fdd60 100644 --- a/worker/detectors/feature/rpm/rpm.go +++ b/worker/detectors/feature/rpm/rpm.go @@ -20,12 +20,16 @@ import ( "os" "strings" + "github.com/coreos/pkg/capnslog" + "github.com/coreos/clair/database" + "github.com/coreos/clair/ext/versionfmt" "github.com/coreos/clair/utils" cerrors "github.com/coreos/clair/utils/errors" - "github.com/coreos/clair/utils/types" "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") @@ -88,7 +92,8 @@ func (detector *RpmFeaturesDetector) Detect(data map[string][]byte) ([]database. } // 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 { log.Warningf("could not parse package version '%s': %s. skipping", line[1], err.Error()) continue @@ -101,7 +106,7 @@ func (detector *RpmFeaturesDetector) Detect(data map[string][]byte) ([]database. }, Version: version, } - packagesMap[pkg.Feature.Name+"#"+pkg.Version.String()] = pkg + packagesMap[pkg.Feature.Name+"#"+pkg.Version] = pkg } // Convert the map to a slice diff --git a/worker/detectors/feature/rpm/rpm_test.go b/worker/detectors/feature/rpm/rpm_test.go index 5e359aba..d937eaf9 100644 --- a/worker/detectors/feature/rpm/rpm_test.go +++ b/worker/detectors/feature/rpm/rpm_test.go @@ -1,4 +1,4 @@ -// Copyright 2015 clair authors +// 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. @@ -18,7 +18,6 @@ import ( "testing" "github.com/coreos/clair/database" - "github.com/coreos/clair/utils/types" "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 { 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 { Feature: database.Feature{Name: "filesystem"}, - Version: types.NewVersionUnsafe("3.2-18.el7"), + Version: "3.2-18.el7", }, }, Data: map[string][]byte{ diff --git a/worker/worker.go b/worker/worker.go index d8db314c..c3d2de3d 100644 --- a/worker/worker.go +++ b/worker/worker.go @@ -180,7 +180,7 @@ func detectFeatureVersions(name string, data map[string][]byte, namespace *datab parentFeatureNamespaces := make(map[string]database.Namespace) if parent != nil { 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 } - 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. features[i].Feature.Namespace = parentFeatureNamespace continue diff --git a/worker/worker_test.go b/worker/worker_test.go index 31829e44..6f3829be 100644 --- a/worker/worker_test.go +++ b/worker/worker_test.go @@ -23,7 +23,6 @@ import ( "github.com/coreos/clair/database" cerrors "github.com/coreos/clair/utils/errors" - "github.com/coreos/clair/utils/types" // Register the required detectors. _ "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. nonUpgradedFeatureVersions := []database.FeatureVersion{ - {Feature: database.Feature{Name: "libtext-wrapi18n-perl"}, Version: types.NewVersionUnsafe("0.06-7")}, - {Feature: database.Feature{Name: "libtext-charwidth-perl"}, Version: types.NewVersionUnsafe("0.04-7")}, - {Feature: database.Feature{Name: "libtext-iconv-perl"}, Version: types.NewVersionUnsafe("1.7-5")}, - {Feature: database.Feature{Name: "mawk"}, Version: types.NewVersionUnsafe("1.3.3-17")}, - {Feature: database.Feature{Name: "insserv"}, Version: types.NewVersionUnsafe("1.14.0-5")}, - {Feature: database.Feature{Name: "db"}, Version: types.NewVersionUnsafe("5.1.29-5")}, - {Feature: database.Feature{Name: "ustr"}, Version: types.NewVersionUnsafe("1.0.4-3")}, - {Feature: database.Feature{Name: "xz-utils"}, Version: types.NewVersionUnsafe("5.1.1alpha+20120614-2")}, + {Feature: database.Feature{Name: "libtext-wrapi18n-perl"}, Version: "0.06-7"}, + {Feature: database.Feature{Name: "libtext-charwidth-perl"}, Version: "0.04-7"}, + {Feature: database.Feature{Name: "libtext-iconv-perl"}, Version: "1.7-5"}, + {Feature: database.Feature{Name: "mawk"}, Version: "1.3.3-17"}, + {Feature: database.Feature{Name: "insserv"}, Version: "1.14.0-5"}, + {Feature: database.Feature{Name: "db"}, Version: "5.1.29-5"}, + {Feature: database.Feature{Name: "ustr"}, Version: "1.0.4-3"}, + {Feature: database.Feature{Name: "xz-utils"}, Version: "5.1.1alpha+20120614-2"}, } // Process test layers.