add registerable version formats

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

@ -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 {

@ -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})

@ -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{}

@ -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,

@ -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)

@ -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)

@ -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,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)
}

@ -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
}

@ -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.

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

@ -38,7 +38,7 @@ func (pgSQL *pgSQL) insertNamespace(namespace database.Namespace) (int, error) {
defer observeQueryTime("insertNamespace", "all", time.Now())
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)

@ -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)
}

@ -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,
},
}

@ -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,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`

@ -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'),

@ -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,
},
},
}

@ -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.

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

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

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

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

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

@ -29,10 +29,14 @@ import (
"github.com/coreos/pkg/capnslog"
"github.com/coreos/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
}

@ -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.

@ -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,
},
}

@ -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)

@ -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",
},
}

@ -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)

@ -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",
},
}

@ -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
}

@ -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",
},
}

@ -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{

@ -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{}
}
}

@ -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{

@ -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 = ""
}
}

@ -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{

@ -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

@ -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{

@ -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

@ -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.

Loading…
Cancel
Save