diff --git a/common/db.go b/common/db.go index 3b55f807..c9b7f5fa 100644 --- a/common/db.go +++ b/common/db.go @@ -499,7 +499,6 @@ func LoadAppVulsTb(path string) (map[string][]AppModuleVul, error) { } for _, mn := range mns { - colon := strings.LastIndex(mn, ":") m := strings.ReplaceAll(mn, ":", ".") vf := vul[mn] @@ -508,14 +507,6 @@ func LoadAppVulsTb(path string) (map[string][]AppModuleVul, error) { } else { vul[m] = vf } - if m = mn[colon+1:]; len(m) > 0 { - key := fmt.Sprintf("jar:%s", m) - if _, ok := vul[key]; ok { - vul[key] = uniqueVulDb(append(vul[key], vf...)) - } else { - vul[key] = vf - } - } } return vul, nil } diff --git a/common/db_test.go b/common/db_test.go index 23c29002..2a91627a 100644 --- a/common/db_test.go +++ b/common/db_test.go @@ -162,6 +162,50 @@ func prepareMockDbPaths(t *testing.T, encryptKey []byte, timestamp time.Time) (s return dbPath, expandRoot } +func writeAppsTb(t *testing.T, dir string, entries ...AppModuleVul) { + t.Helper() + var buf bytes.Buffer + for _, e := range entries { + data, err := json.Marshal(e) + require.NoError(t, err) + buf.Write(data) + buf.WriteByte('\n') + } + require.NoError(t, os.WriteFile(filepath.Join(dir, "apps.tb"), buf.Bytes(), 0644)) +} + +func loadAppsTb(t *testing.T, entries ...AppModuleVul) map[string][]AppModuleVul { + t.Helper() + dir := t.TempDir() + writeAppsTb(t, dir, entries...) + vul, err := LoadAppVulsTb(dir) + require.NoError(t, err) + return vul +} + +func newJarVul(cveName, moduleName, severity, fixedBefore string) AppModuleVul { + return AppModuleVul{ + VulName: cveName, + AppName: "jar", + ModuleName: moduleName, + Severity: severity, + AffectedVer: []AppModuleVersion{{OpCode: "lt", Version: fixedBefore}}, + } +} + +func requireKeyExists(t *testing.T, vul map[string][]AppModuleVul, key string) []AppModuleVul { + t.Helper() + v, ok := vul[key] + require.True(t, ok, "expected key %q to exist", key) + return v +} + +func requireKeyAbsent(t *testing.T, vul map[string][]AppModuleVul, key string) { + t.Helper() + _, ok := vul[key] + require.False(t, ok, "key %q must NOT exist", key) +} + func TestLoadCveDB(t *testing.T) { testCases := []struct { name string @@ -284,3 +328,63 @@ func TestLoadCveDB(t *testing.T) { }) } } + +func TestLoadAppVulsTb_NoJarShortcutKeys(t *testing.T) { + vul := loadAppsTb(t, + newJarVul("CVE-2021-0341", "com.squareup.okhttp3:okhttp", "High", "4.9.3"), + newJarVul("CVE-2022-20621", "org.jenkins-ci.plugins:metrics", "Medium", "4.2.0"), + newJarVul("CVE-2018-1000011", "org.jvnet.hudson.plugins:findbugs:library", "High", "4.72"), + ) + + // --- Verify original groupId:artifactId keys ARE present --- + okhttp := requireKeyExists(t, vul, "com.squareup.okhttp3:okhttp") + requireKeyExists(t, vul, "org.jenkins-ci.plugins:metrics") + requireKeyExists(t, vul, "org.jvnet.hudson.plugins:findbugs:library") + + // --- Verify dot-separated backward-compat keys ARE present --- + okhttpDot := requireKeyExists(t, vul, "com.squareup.okhttp3.okhttp") + requireKeyExists(t, vul, "org.jenkins-ci.plugins.metrics") + + // --- Verify jar: shortcut keys are NOT present --- + for _, key := range []string{"jar:okhttp", "jar:metrics", "jar:library", "jar:findbugs:library"} { + requireKeyAbsent(t, vul, key) + } + + // --- Verify CVE data integrity for existing keys --- + require.Len(t, okhttp, 1) + require.Equal(t, "CVE-2021-0341", okhttp[0].VulName) + + // Dot-separated key should have same data + require.Len(t, okhttpDot, 1) + require.Equal(t, "CVE-2021-0341", okhttpDot[0].VulName) +} + +func TestLoadAppVulsTb_NoColonModuleName(t *testing.T) { + vul := loadAppsTb(t, AppModuleVul{ + VulName: "CVE-2099-0001", + AppName: "npm", + ModuleName: "lodash", + Severity: "High", + AffectedVer: []AppModuleVersion{{OpCode: "lt", Version: "4.17.21"}}, + }) + + requireKeyExists(t, vul, "lodash") + requireKeyAbsent(t, vul, "jar:lodash") + require.Len(t, vul, 1) +} + +func TestLoadAppVulsTb_MultipleVulsSameModule(t *testing.T) { + vul := loadAppsTb(t, + newJarVul("CVE-2021-0341", "com.squareup.okhttp3:okhttp", "High", "4.9.3"), + newJarVul("CVE-2016-2402", "com.squareup.okhttp3:okhttp", "Medium", "3.1.2"), + ) + + // Both CVEs should be under the same key + require.Len(t, vul["com.squareup.okhttp3:okhttp"], 2) + + // Dot-separated should also have both + require.Len(t, vul["com.squareup.okhttp3.okhttp"], 2) + + // No jar: key + requireKeyAbsent(t, vul, "jar:okhttp") +} diff --git a/cvetools/apps_test.go b/cvetools/apps_test.go index 6427a2cb..b592dbd4 100644 --- a/cvetools/apps_test.go +++ b/cvetools/apps_test.go @@ -1,10 +1,15 @@ package cvetools import ( + "bytes" + "encoding/json" + "os" + "path/filepath" "testing" "github.com/neuvector/neuvector/share/utils" "github.com/neuvector/scanner/common" + "github.com/neuvector/scanner/detectors" ) type versionTestCase struct { @@ -13,6 +18,19 @@ type versionTestCase struct { dbVer []common.AppModuleVersion } +func writeAppsTb(t *testing.T, entries []common.AppModuleVul) string { + t.Helper() + dir := t.TempDir() + var buf bytes.Buffer + for _, e := range entries { + data, _ := json.Marshal(e) + buf.Write(data) + buf.WriteByte('\n') + } + _ = os.WriteFile(filepath.Join(dir, "apps.tb"), buf.Bytes(), 0644) + return dir +} + func TestAffectedVersion(t *testing.T) { cases := []versionTestCase{ {result: false, version: "1.2.3", dbVer: []common.AppModuleVersion{}}, @@ -67,3 +85,248 @@ func TestFixedVersion(t *testing.T) { } } } + +// TestDetectAppVul_NoFalsePositiveJarOkhttp verifies that jar:okhttp +// (from an OpenTelemetry JAR's MANIFEST.MF fallback) does NOT match +// CVEs for com.squareup.okhttp3:okhttp. +func TestDetectAppVul_NoFalsePositiveJarOkhttp(t *testing.T) { + dir := writeAppsTb(t, []common.AppModuleVul{ + { + VulName: "CVE-2021-0341", + AppName: "jar", + ModuleName: "com.squareup.okhttp3:okhttp", + Description: "OkHttp hostname verification bypass", + Severity: "High", + AffectedVer: []common.AppModuleVersion{{OpCode: "lt", Version: "4.9.3"}}, + FixedVer: []common.AppModuleVersion{{OpCode: "gteq", Version: "4.9.3"}}, + }, + }) + + // Simulate what the scanner produces for opentelemetry-exporter-sender-okhttp-1.58.0.jar + // which lacks pom.properties -- MANIFEST.MF fallback produces jar:okhttp + apps := []detectors.AppFeatureVersion{ + { + AppName: "jar", + ModuleName: "jar:okhttp", + Version: "1.58.0", + FileName: "app/libs/opentelemetry-exporter-sender-okhttp-1.58.0.jar", + }, + } + + cv := &ScanTools{} + vuls := cv.DetectAppVul(dir, apps, "") + + if len(vuls) != 0 { + t.Errorf("expected 0 vulnerabilities for jar:okhttp (false positive), got %d", len(vuls)) + for _, v := range vuls { + t.Errorf(" false positive: %s matched against %s", v.Vf.Name, v.Ft.File) + } + } +} + +// TestDetectAppVul_NoFalsePositiveJarLibrary verifies that jar:library +// (from OpenTelemetry instrumentation JARs) does NOT match CVEs for +// Jenkins FindBugs Plugin. +func TestDetectAppVul_NoFalsePositiveJarLibrary(t *testing.T) { + dir := writeAppsTb(t, []common.AppModuleVul{ + { + VulName: "CVE-2018-1000011", + AppName: "jar", + ModuleName: "org.jvnet.hudson.plugins:findbugs:library", + Description: "Jenkins FindBugs Plugin XXE", + Severity: "High", + AffectedVer: []common.AppModuleVersion{{OpCode: "lt", Version: "4.72"}}, + }, + }) + + // OpenTelemetry instrumentation JARs produce jar:library from MANIFEST.MF + apps := []detectors.AppFeatureVersion{ + { + AppName: "jar", + ModuleName: "jar:library", + Version: "2.23.0-alpha", + FileName: "app/libs/opentelemetry-jdbc-2.23.0-alpha.jar", + }, + { + AppName: "jar", + ModuleName: "jar:library", + Version: "2.23.0-alpha", + FileName: "app/libs/opentelemetry-kafka-clients-2.6-2.23.0-alpha.jar", + }, + } + + cv := &ScanTools{} + vuls := cv.DetectAppVul(dir, apps, "") + + if len(vuls) != 0 { + t.Errorf("expected 0 vulnerabilities for jar:library (false positive), got %d", len(vuls)) + for _, v := range vuls { + t.Errorf(" false positive: %s matched against %s", v.Vf.Name, v.Ft.File) + } + } +} + +// TestDetectAppVul_NoFalsePositiveJarMetrics verifies that jar:metrics +// (from OpenTelemetry SDK) does NOT match CVEs for Jenkins Metrics Plugin. +func TestDetectAppVul_NoFalsePositiveJarMetrics(t *testing.T) { + dir := writeAppsTb(t, []common.AppModuleVul{ + { + VulName: "CVE-2022-20621", + AppName: "jar", + ModuleName: "org.jenkins-ci.plugins:metrics", + Description: "Jenkins Metrics Plugin plain text storage", + Severity: "Medium", + AffectedVer: []common.AppModuleVersion{{OpCode: "lt", Version: "4.2.0"}}, + }, + }) + + apps := []detectors.AppFeatureVersion{ + { + AppName: "jar", + ModuleName: "jar:metrics", + Version: "1.58.0", + FileName: "app/libs/opentelemetry-sdk-metrics-1.58.0.jar", + }, + } + + cv := &ScanTools{} + vuls := cv.DetectAppVul(dir, apps, "") + + if len(vuls) != 0 { + t.Errorf("expected 0 vulnerabilities for jar:metrics (false positive), got %d", len(vuls)) + } +} + +// TestDetectAppVul_NoFalsePositiveJarCommon verifies that jar:common +// does NOT match unrelated CVEs. +func TestDetectAppVul_NoFalsePositiveJarCommon(t *testing.T) { + dir := writeAppsTb(t, []common.AppModuleVul{ + { + VulName: "CVE-2024-46985", + AppName: "jar", + ModuleName: "org.example:common", + Description: "Some common library vulnerability", + Severity: "High", + AffectedVer: []common.AppModuleVersion{{OpCode: "lt", Version: "2.10.1"}}, + }, + }) + + apps := []detectors.AppFeatureVersion{ + { + AppName: "jar", + ModuleName: "jar:common", + Version: "1.58.0", + FileName: "app/libs/opentelemetry-common-1.58.0.jar", + }, + } + + cv := &ScanTools{} + vuls := cv.DetectAppVul(dir, apps, "") + + if len(vuls) != 0 { + t.Errorf("expected 0 vulnerabilities for jar:common (false positive), got %d", len(vuls)) + } +} + +// TestDetectAppVul_LegitimateExactMatch verifies that exact groupId:artifactId +// matches (from JARs with pom.properties) still work correctly. +func TestDetectAppVul_LegitimateExactMatch(t *testing.T) { + dir := writeAppsTb(t, []common.AppModuleVul{ + { + VulName: "CVE-2021-0341", + AppName: "jar", + ModuleName: "com.squareup.okhttp3:okhttp", + Description: "OkHttp hostname verification bypass", + Severity: "High", + AffectedVer: []common.AppModuleVersion{{OpCode: "lt", Version: "4.9.3"}}, + FixedVer: []common.AppModuleVersion{{OpCode: "gteq", Version: "4.9.3"}}, + }, + }) + + // JAR with pom.properties produces exact groupId:artifactId + apps := []detectors.AppFeatureVersion{ + { + AppName: "jar", + ModuleName: "com.squareup.okhttp3:okhttp", + Version: "4.9.1", // vulnerable version + FileName: "app/libs/okhttp-4.9.1.jar", + }, + } + + cv := &ScanTools{} + vuls := cv.DetectAppVul(dir, apps, "") + + if len(vuls) != 1 { + t.Fatalf("expected 1 vulnerability for exact match, got %d", len(vuls)) + } + if vuls[0].Vf.Name != "CVE-2021-0341" { + t.Errorf("expected CVE-2021-0341, got %s", vuls[0].Vf.Name) + } +} + +// TestDetectAppVul_LegitimateDotSeparatedMatch verifies that dot-separated +// module names (backward compat) still match correctly. +func TestDetectAppVul_LegitimateDotSeparatedMatch(t *testing.T) { + dir := writeAppsTb(t, []common.AppModuleVul{ + { + VulName: "CVE-2021-0341", + AppName: "jar", + ModuleName: "com.squareup.okhttp3:okhttp", + Description: "OkHttp hostname verification bypass", + Severity: "High", + AffectedVer: []common.AppModuleVersion{{OpCode: "lt", Version: "4.9.3"}}, + FixedVer: []common.AppModuleVersion{{OpCode: "gteq", Version: "4.9.3"}}, + }, + }) + + // Some older scanners produce dot-separated module names + apps := []detectors.AppFeatureVersion{ + { + AppName: "jar", + ModuleName: "com.squareup.okhttp3.okhttp", + Version: "4.9.1", + FileName: "app/libs/okhttp-4.9.1.jar", + }, + } + + cv := &ScanTools{} + vuls := cv.DetectAppVul(dir, apps, "") + + if len(vuls) != 1 { + t.Fatalf("expected 1 vulnerability for dot-separated match, got %d", len(vuls)) + } + if vuls[0].Vf.Name != "CVE-2021-0341" { + t.Errorf("expected CVE-2021-0341, got %s", vuls[0].Vf.Name) + } +} + +// TestDetectAppVul_FixedVersionNotReported verifies that a JAR at a fixed +// version is NOT reported as vulnerable. +func TestDetectAppVul_FixedVersionNotReported(t *testing.T) { + dir := writeAppsTb(t, []common.AppModuleVul{ + { + VulName: "CVE-2021-0341", + AppName: "jar", + ModuleName: "com.squareup.okhttp3:okhttp", + Description: "OkHttp hostname verification bypass", + Severity: "High", + AffectedVer: []common.AppModuleVersion{{OpCode: "lt", Version: "4.9.3"}}, + }, + }) + + apps := []detectors.AppFeatureVersion{ + { + AppName: "jar", + ModuleName: "com.squareup.okhttp3:okhttp", + Version: "5.3.1", // well past fixed version + FileName: "app/libs/okhttp-jvm-5.3.1.jar", + }, + } + + cv := &ScanTools{} + vuls := cv.DetectAppVul(dir, apps, "") + + if len(vuls) != 0 { + t.Errorf("expected 0 vulnerabilities for fixed version 5.3.1, got %d", len(vuls)) + } +}