// Copyright 2017 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 vulnerability import ( "database/sql" "math/rand" "strconv" "testing" "github.com/pborman/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/coreos/clair/database" "github.com/coreos/clair/database/pgsql/feature" "github.com/coreos/clair/database/pgsql/namespace" "github.com/coreos/clair/database/pgsql/testutil" "github.com/coreos/clair/ext/versionfmt" "github.com/coreos/clair/ext/versionfmt/dpkg" "github.com/coreos/clair/pkg/strutil" ) func TestInsertVulnerabilities(t *testing.T) { store, cleanup := testutil.CreateTestDBWithFixture(t, "InsertVulnerabilities") defer cleanup() ns1 := database.Namespace{ Name: "name", VersionFormat: "random stuff", } ns2 := database.Namespace{ Name: "debian:7", VersionFormat: "dpkg", } // invalid vulnerability v1 := database.Vulnerability{ Name: "invalid", Namespace: ns1, } vwa1 := database.VulnerabilityWithAffected{ Vulnerability: v1, } // valid vulnerability v2 := database.Vulnerability{ Name: "valid", Namespace: ns2, Severity: database.UnknownSeverity, } vwa2 := database.VulnerabilityWithAffected{ Vulnerability: v2, } tx, err := store.Begin() require.Nil(t, err) // empty err = InsertVulnerabilities(tx, []database.VulnerabilityWithAffected{}) assert.Nil(t, err) // invalid content: vwa1 is invalid err = InsertVulnerabilities(tx, []database.VulnerabilityWithAffected{vwa1, vwa2}) assert.NotNil(t, err) tx = testutil.RestartTransaction(store, tx, false) // invalid content: duplicated input err = InsertVulnerabilities(tx, []database.VulnerabilityWithAffected{vwa2, vwa2}) assert.NotNil(t, err) tx = testutil.RestartTransaction(store, tx, false) // valid content err = InsertVulnerabilities(tx, []database.VulnerabilityWithAffected{vwa2}) assert.Nil(t, err) tx = testutil.RestartTransaction(store, tx, true) // ensure the content is in database vulns, err := FindVulnerabilities(tx, []database.VulnerabilityID{{Name: "valid", Namespace: "debian:7"}}) if assert.Nil(t, err) && assert.Len(t, vulns, 1) { assert.True(t, vulns[0].Valid) } tx = testutil.RestartTransaction(store, tx, false) // valid content: vwa2 removed and inserted err = DeleteVulnerabilities(tx, []database.VulnerabilityID{{Name: vwa2.Name, Namespace: vwa2.Namespace.Name}}) assert.Nil(t, err) err = InsertVulnerabilities(tx, []database.VulnerabilityWithAffected{vwa2}) assert.Nil(t, err) require.Nil(t, tx.Rollback()) } func TestCachingVulnerable(t *testing.T) { tx, cleanup := testutil.CreateTestTxWithFixtures(t, "CachingVulnerable") defer cleanup() ns := database.Namespace{ Name: "debian:8", VersionFormat: dpkg.ParserName, } f := database.NamespacedFeature{ Feature: database.Feature{ Name: "openssl", Version: "1.0", VersionFormat: dpkg.ParserName, Type: database.SourcePackage, }, Namespace: ns, } vuln := database.VulnerabilityWithAffected{ Vulnerability: database.Vulnerability{ Name: "CVE-YAY", Namespace: ns, Severity: database.HighSeverity, }, Affected: []database.AffectedFeature{ { Namespace: ns, FeatureName: "openssl", FeatureType: database.SourcePackage, AffectedVersion: "2.0", FixedInVersion: "2.1", }, }, } vuln2 := database.VulnerabilityWithAffected{ Vulnerability: database.Vulnerability{ Name: "CVE-YAY2", Namespace: ns, Severity: database.HighSeverity, }, Affected: []database.AffectedFeature{ { Namespace: ns, FeatureName: "openssl", FeatureType: database.SourcePackage, AffectedVersion: "2.1", FixedInVersion: "2.2", }, }, } vulnFixed1 := database.VulnerabilityWithFixedIn{ Vulnerability: database.Vulnerability{ Name: "CVE-YAY", Namespace: ns, Severity: database.HighSeverity, }, FixedInVersion: "2.1", } vulnFixed2 := database.VulnerabilityWithFixedIn{ Vulnerability: database.Vulnerability{ Name: "CVE-YAY2", Namespace: ns, Severity: database.HighSeverity, }, FixedInVersion: "2.2", } require.Nil(t, InsertVulnerabilities(tx, []database.VulnerabilityWithAffected{vuln, vuln2})) r, err := FindAffectedNamespacedFeatures(tx, []database.NamespacedFeature{f}) assert.Nil(t, err) assert.Len(t, r, 1) for _, anf := range r { if assert.True(t, anf.Valid) && assert.Len(t, anf.AffectedBy, 2) { for _, a := range anf.AffectedBy { if a.Name == "CVE-YAY" { assert.Equal(t, vulnFixed1, a) } else if a.Name == "CVE-YAY2" { assert.Equal(t, vulnFixed2, a) } else { t.FailNow() } } } } } func TestFindVulnerabilities(t *testing.T) { tx, cleanup := testutil.CreateTestTxWithFixtures(t, "FindVulnerabilities") defer cleanup() vuln, err := FindVulnerabilities(tx, []database.VulnerabilityID{ {Name: "CVE-OPENSSL-1-DEB7", Namespace: "debian:7"}, {Name: "CVE-NOPE", Namespace: "debian:7"}, {Name: "CVE-NOT HERE"}, }) ns := database.Namespace{ Name: "debian:7", VersionFormat: "dpkg", } expectedExisting := []database.VulnerabilityWithAffected{ { Vulnerability: database.Vulnerability{ Namespace: ns, Name: "CVE-OPENSSL-1-DEB7", Description: "A vulnerability affecting OpenSSL < 2.0 on Debian 7.0", Link: "http://google.com/#q=CVE-OPENSSL-1-DEB7", Severity: database.HighSeverity, }, Affected: []database.AffectedFeature{ { FeatureName: "openssl", FeatureType: database.SourcePackage, AffectedVersion: "2.0", FixedInVersion: "2.0", Namespace: ns, }, { FeatureName: "libssl", FeatureType: database.SourcePackage, AffectedVersion: "1.9-abc", FixedInVersion: "1.9-abc", Namespace: ns, }, }, }, { Vulnerability: database.Vulnerability{ Namespace: ns, Name: "CVE-NOPE", Description: "A vulnerability affecting nothing", Severity: database.UnknownSeverity, }, }, } expectedExistingMap := map[database.VulnerabilityID]database.VulnerabilityWithAffected{} for _, v := range expectedExisting { expectedExistingMap[database.VulnerabilityID{Name: v.Name, Namespace: v.Namespace.Name}] = v } nonexisting := database.VulnerabilityWithAffected{ Vulnerability: database.Vulnerability{Name: "CVE-NOT HERE"}, } if assert.Nil(t, err) { for _, v := range vuln { if v.Valid { key := database.VulnerabilityID{ Name: v.Name, Namespace: v.Namespace.Name, } expected, ok := expectedExistingMap[key] if assert.True(t, ok, "vulnerability not found: "+key.Name+":"+key.Namespace) { testutil.AssertVulnerabilityWithAffectedEqual(t, expected, v.VulnerabilityWithAffected) } } else if !assert.Equal(t, nonexisting, v.VulnerabilityWithAffected) { t.FailNow() } } } // same vulnerability r, err := FindVulnerabilities(tx, []database.VulnerabilityID{ {Name: "CVE-OPENSSL-1-DEB7", Namespace: "debian:7"}, {Name: "CVE-OPENSSL-1-DEB7", Namespace: "debian:7"}, }) if assert.Nil(t, err) { for _, vuln := range r { if assert.True(t, vuln.Valid) { expected, _ := expectedExistingMap[database.VulnerabilityID{Name: "CVE-OPENSSL-1-DEB7", Namespace: "debian:7"}] testutil.AssertVulnerabilityWithAffectedEqual(t, expected, vuln.VulnerabilityWithAffected) } } } } func TestDeleteVulnerabilities(t *testing.T) { tx, cleanup := testutil.CreateTestTxWithFixtures(t, "DeleteVulnerabilities") defer cleanup() remove := []database.VulnerabilityID{} // empty case assert.Nil(t, DeleteVulnerabilities(tx, remove)) // invalid case remove = append(remove, database.VulnerabilityID{}) assert.NotNil(t, DeleteVulnerabilities(tx, remove)) // valid case validRemove := []database.VulnerabilityID{ {Name: "CVE-OPENSSL-1-DEB7", Namespace: "debian:7"}, {Name: "CVE-NOPE", Namespace: "debian:7"}, } assert.Nil(t, DeleteVulnerabilities(tx, validRemove)) vuln, err := FindVulnerabilities(tx, validRemove) if assert.Nil(t, err) { for _, v := range vuln { assert.False(t, v.Valid) } } } func TestFindVulnerabilityIDs(t *testing.T) { tx, cleanup := testutil.CreateTestTxWithFixtures(t, "FindVulnerabilityIDs") defer cleanup() ids, err := FindLatestDeletedVulnerabilityIDs(tx, []database.VulnerabilityID{{Name: "CVE-DELETED", Namespace: "debian:7"}}) if assert.Nil(t, err) { if !(assert.Len(t, ids, 1) && assert.True(t, ids[0].Valid) && assert.Equal(t, 3, int(ids[0].Int64))) { assert.Fail(t, "") } } ids, err = FindNotDeletedVulnerabilityIDs(tx, []database.VulnerabilityID{{Name: "CVE-NOPE", Namespace: "debian:7"}}) if assert.Nil(t, err) { if !(assert.Len(t, ids, 1) && assert.True(t, ids[0].Valid) && assert.Equal(t, 2, int(ids[0].Int64))) { assert.Fail(t, "") } } } func TestFindAffectedNamespacedFeatures(t *testing.T) { tx, cleanup := testutil.CreateTestTxWithFixtures(t, "FindAffectedNamespacedFeatures") defer cleanup() ns := database.NamespacedFeature{ Feature: database.Feature{ Name: "openssl", Version: "1.0", VersionFormat: "dpkg", Type: database.SourcePackage, }, Namespace: database.Namespace{ Name: "debian:7", VersionFormat: "dpkg", }, } ans, err := FindAffectedNamespacedFeatures(tx, []database.NamespacedFeature{ns}) if assert.Nil(t, err) && assert.Len(t, ans, 1) && assert.True(t, ans[0].Valid) && assert.Len(t, ans[0].AffectedBy, 1) { assert.Equal(t, "CVE-OPENSSL-1-DEB7", ans[0].AffectedBy[0].Name) } } func genRandomVulnerabilityAndNamespacedFeature(t *testing.T, store *sql.DB) ([]database.NamespacedFeature, []database.VulnerabilityWithAffected) { tx, err := store.Begin() if err != nil { panic(err) } numFeatures := 100 numVulnerabilities := 100 featureName := "TestFeature" featureVersionFormat := dpkg.ParserName // Insert the namespace on which we'll work. ns := database.Namespace{ Name: "TestRaceAffectsFeatureNamespace1", VersionFormat: dpkg.ParserName, } if !assert.Nil(t, namespace.PersistNamespaces(tx, []database.Namespace{ns})) { t.FailNow() } // Generate Distinct random features features := make([]database.Feature, numFeatures) nsFeatures := make([]database.NamespacedFeature, numFeatures) for i := 0; i < numFeatures; i++ { version := rand.Intn(numFeatures) features[i] = *database.NewSourcePackage(featureName, strconv.Itoa(version), featureVersionFormat) nsFeatures[i] = database.NamespacedFeature{ Namespace: ns, Feature: features[i], } } if !assert.Nil(t, feature.PersistFeatures(tx, features)) { t.FailNow() } // Generate vulnerabilities. vulnerabilities := []database.VulnerabilityWithAffected{} for i := 0; i < numVulnerabilities; i++ { // any version less than this is vulnerable version := rand.Intn(numFeatures) + 1 vulnerability := database.VulnerabilityWithAffected{ Vulnerability: database.Vulnerability{ Name: uuid.New(), Namespace: ns, Severity: database.UnknownSeverity, }, Affected: []database.AffectedFeature{ { Namespace: ns, FeatureName: featureName, FeatureType: database.SourcePackage, AffectedVersion: strconv.Itoa(version), FixedInVersion: strconv.Itoa(version), }, }, } vulnerabilities = append(vulnerabilities, vulnerability) } tx.Commit() return nsFeatures, vulnerabilities } func TestVulnChangeAffectsVulnerableFeatures(t *testing.T) { db, cleanup := testutil.CreateTestDB(t, "caching") defer cleanup() nsFeatures, vulnerabilities := genRandomVulnerabilityAndNamespacedFeature(t, db) tx, err := db.Begin() require.Nil(t, err) require.Nil(t, feature.PersistNamespacedFeatures(tx, nsFeatures)) require.Nil(t, tx.Commit()) tx, err = db.Begin() require.Nil(t, InsertVulnerabilities(tx, vulnerabilities)) require.Nil(t, tx.Commit()) tx, err = db.Begin() require.Nil(t, err) defer tx.Rollback() affected, err := FindAffectedNamespacedFeatures(tx, nsFeatures) require.Nil(t, err) for _, ansf := range affected { require.True(t, ansf.Valid) expectedAffectedNames := []string{} for _, vuln := range vulnerabilities { if ok, err := versionfmt.InRange(dpkg.ParserName, ansf.Version, vuln.Affected[0].AffectedVersion); err == nil { if ok { expectedAffectedNames = append(expectedAffectedNames, vuln.Name) } } } actualAffectedNames := []string{} for _, s := range ansf.AffectedBy { actualAffectedNames = append(actualAffectedNames, s.Name) } require.Len(t, strutil.Difference(expectedAffectedNames, actualAffectedNames), 0, "\nvulns: %#v\nfeature:%#v\nexpected:%#v\nactual:%#v", vulnerabilities, ansf.NamespacedFeature, expectedAffectedNames, actualAffectedNames) require.Len(t, strutil.Difference(actualAffectedNames, expectedAffectedNames), 0) } }