clair/database/pgsql/vulnerability/vulnerability_test.go

475 lines
13 KiB
Go

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