Fixed iCloud false positives (#12551)

Added new type `CPEMatchingRule` used for fixing false positives caused by 'bad' entries in the NVD dataset.
This commit is contained in:
Juan Fernandez
2023-06-30 13:05:34 -04:00
committed by GitHub
parent cc8a2d0605
commit 91f048e98a
12 changed files with 564 additions and 16 deletions
+3
View File
@@ -0,0 +1,3 @@
- Added CPEMatchingRule type, used for correcting false positives caused by incorrect entries in the
NVD dataset.
- Fixed false positives for iCloud on macOS.
+1 -1
View File
@@ -49,7 +49,7 @@ func getLatestReleaseNotes(vulnPath string) (ReleaseNotes, error) {
return relNotes, nil
}
// collectVulnerabilities compares 'software' againts all 'release notes' returning all detected
// collectVulnerabilities compares 'software' against all 'release notes' returning all detected
// vulnerabilities.
func collectVulnerabilities(
software *fleet.Software,
@@ -49,7 +49,7 @@ func (or *ReleaseNote) Valid() bool {
return len(or.Version) != 0 && len(or.SecurityUpdates) != 0
}
// CmpVersion compares the release note version againts 'otherVer' returning:
// CmpVersion compares the release note version against 'otherVer' returning:
// -1 if rel. note version < other version
// 0 if rel. note version == other version
// 1 if rel. note version > other version
@@ -0,0 +1,108 @@
package nvd
import (
"errors"
"fmt"
"strings"
"github.com/Masterminds/semver"
"github.com/facebookincubator/nvdtools/wfn"
)
// CPEMatchingRuleSpec allows you to match against a CPE. Version ranges are supported via SemVer constraints.
type CPEMatchingRuleSpec struct {
Vendor string // Software vendor.
Product string // Software product name.
TargetSW string // Target software, this usually corresponds to the target OS.
// Specifies a version constraint. See https://pkg.go.dev/github.com/Masterminds/semver@v1.5.0#hdr-Checking_Version_Constraints
// for reference.
SemVerConstraint string
}
func (rule CPEMatchingRuleSpec) getCPEMeta() *wfn.Attributes {
return &wfn.Attributes{
Vendor: rule.Vendor,
Product: rule.Product,
TargetSW: rule.TargetSW,
}
}
// CPEMatchingRule allows you to express a matching rule based on some CPE properties, one or more
// CVEs and one or more SemVer constraint. This is used to 'fix' false positives resulting from bad
// data in the NVD dataset itself.
// For example: https://nvd.nist.gov/vuln/detail/CVE-2017-13797, one of the CPE entries specified is
// cpe:2.3:a:apple:icloud:*:*:*:*:*:*:*:* which will match with any iCloud installation, but the
// vulnerability in question only affects iCloud on Windows up to 7.0.x.
type CPEMatchingRule struct {
CPESpecs []CPEMatchingRuleSpec
// Set of CVEs that this rule targets
CVEs map[string]struct{}
}
// CPEMatches returns true if both the provided CPE match the rule.
func (rule CPEMatchingRule) CPEMatches(cpeMeta *wfn.Attributes) bool {
if cpeMeta == nil {
return false
}
ver, err := semver.NewVersion(wfn.StripSlashes(cpeMeta.Version))
if err != nil {
return false
}
for _, spec := range rule.CPESpecs {
// The SemVer constraint is validated at instantiation time, so it should be ok to ignore the error.
constraint, _ := semver.NewConstraint(spec.SemVerConstraint)
if cpeMeta.MatchWithoutVersion(spec.getCPEMeta()) && constraint.Check(ver) {
return true
}
}
return false
}
// Validate validates the rule, returns an error if there's something wrong.
func (rule CPEMatchingRule) Validate() error {
validateCPEPart := func(errPrefix, val string) error {
switch strings.TrimSpace(val) {
case "":
return fmt.Errorf("%s can't be empty", errPrefix)
case "*":
return fmt.Errorf("%s can't be 'ANY'", errPrefix)
case "-":
return fmt.Errorf("%s can't be 'NA'", errPrefix)
default:
return nil
}
}
for _, spec := range rule.CPESpecs {
// Validate CPE parts
if err := validateCPEPart("Vendor", spec.Vendor); err != nil {
return err
}
if err := validateCPEPart("Product", spec.Product); err != nil {
return err
}
if err := validateCPEPart("TargetSW", spec.TargetSW); err != nil {
return err
}
// Validate SemVerConstraint
if _, err := semver.NewConstraint(spec.SemVerConstraint); err != nil {
return err
}
}
// Validate CVEs entries
if len(rule.CVEs) == 0 {
return errors.New("At least one CVE is required")
}
for cve := range rule.CVEs {
if strings.TrimSpace(cve) == "" {
return errors.New("CVE can't be empty")
}
}
return nil
}
@@ -0,0 +1,244 @@
package nvd
import (
"errors"
"testing"
"github.com/facebookincubator/nvdtools/wfn"
"github.com/stretchr/testify/require"
)
func TestCPEProcessingRule(t *testing.T) {
buildRule := func() CPEMatchingRule {
return CPEMatchingRule{
CPESpecs: []CPEMatchingRuleSpec{
{
Vendor: "microsoft",
Product: "word",
TargetSW: "windows",
SemVerConstraint: "1.2.3",
},
},
CVEs: map[string]struct{}{"CVE-123": {}},
}
}
buildCPEMeta := func() *wfn.Attributes {
cpeMeta, err := wfn.Parse("cpe:2.3:a:microsoft:word:1.2.3:*:*:*:*:windows:*:*")
require.NoError(t, err)
return cpeMeta
}
t.Run("getCPEAttrs", func(t *testing.T) {
rule := buildRule()
result := rule.CPESpecs[0].getCPEMeta()
require.NotNil(t, result)
expected := wfn.Attributes{
Vendor: rule.CPESpecs[0].Vendor,
Product: rule.CPESpecs[0].Product,
TargetSW: rule.CPESpecs[0].TargetSW,
}
require.True(t, expected.MatchWithoutVersion(result))
})
t.Run("Matches", func(t *testing.T) {
t.Run("is a match", func(t *testing.T) {
rule := buildRule()
cpeMeta := buildCPEMeta()
require.True(t, rule.CPEMatches(cpeMeta))
})
t.Run("CPEMeta info is null", func(t *testing.T) {
rule := buildRule()
require.False(t, rule.CPEMatches(nil))
})
t.Run("CPEs don't match", func(t *testing.T) {
rule := buildRule()
rule.CPESpecs[0].Vendor = "AMD"
cpeMeta := buildCPEMeta()
require.False(t, rule.CPEMatches(cpeMeta))
})
t.Run("SemVer is a range constraint", func(t *testing.T) {
rule := buildRule()
rule.CPESpecs[0].SemVerConstraint = "1.0.0 - 2.0.0"
cpeMeta := buildCPEMeta()
require.True(t, rule.CPEMatches(cpeMeta))
})
})
t.Run("Validate", func(t *testing.T) {
testCases := []struct {
rule CPEMatchingRule
err error
}{
{
rule: CPEMatchingRule{
CPESpecs: []CPEMatchingRuleSpec{
{
Vendor: "",
Product: "word",
TargetSW: "windows",
SemVerConstraint: "1.2.3",
},
},
CVEs: map[string]struct{}{"CVE-123": {}},
}, err: errors.New("Vendor can't be empty"),
},
{
rule: CPEMatchingRule{
CPESpecs: []CPEMatchingRuleSpec{
{
Vendor: "*",
Product: "word",
TargetSW: "windows",
SemVerConstraint: "1.2.3",
},
},
CVEs: map[string]struct{}{"CVE-123": {}},
}, err: errors.New("Vendor can't be 'ANY'"),
},
{
rule: CPEMatchingRule{
CPESpecs: []CPEMatchingRuleSpec{
{
Vendor: "-",
Product: "word",
TargetSW: "windows",
SemVerConstraint: "1.2.3",
},
},
CVEs: map[string]struct{}{"CVE-123": {}},
}, err: errors.New("Vendor can't be 'NA'"),
},
{
rule: CPEMatchingRule{
CPESpecs: []CPEMatchingRuleSpec{
{
Vendor: "microsoft",
Product: "",
TargetSW: "windows",
SemVerConstraint: "1.2.3",
},
},
CVEs: map[string]struct{}{"CVE-123": {}},
}, err: errors.New("Product can't be empty"),
},
{
rule: CPEMatchingRule{
CPESpecs: []CPEMatchingRuleSpec{
{
Vendor: "microsoft",
Product: "*",
TargetSW: "windows",
SemVerConstraint: "1.2.3",
},
},
CVEs: map[string]struct{}{"CVE-123": {}},
}, err: errors.New("Product can't be 'ANY'"),
},
{
rule: CPEMatchingRule{
CPESpecs: []CPEMatchingRuleSpec{
{
Vendor: "microsoft",
Product: "-",
TargetSW: "windows",
SemVerConstraint: "1.2.3",
},
},
CVEs: map[string]struct{}{"CVE-123": {}},
}, err: errors.New("Product can't be 'NA'"),
},
{
rule: CPEMatchingRule{
CPESpecs: []CPEMatchingRuleSpec{
{
Vendor: "microsoft",
Product: "word",
TargetSW: "",
SemVerConstraint: "1.2.3",
},
},
CVEs: map[string]struct{}{"CVE-123": {}},
}, err: errors.New("TargetSW can't be empty"),
},
{
rule: CPEMatchingRule{
CPESpecs: []CPEMatchingRuleSpec{
{
Vendor: "microsoft",
Product: "word",
TargetSW: "*",
SemVerConstraint: "1.2.3",
},
},
CVEs: map[string]struct{}{"CVE-123": {}},
}, err: errors.New("TargetSW can't be 'ANY'"),
},
{
rule: CPEMatchingRule{
CPESpecs: []CPEMatchingRuleSpec{
{
Vendor: "microsoft",
Product: "word",
TargetSW: "-",
SemVerConstraint: "1.2.3",
},
},
CVEs: map[string]struct{}{"CVE-123": {}},
}, err: errors.New("TargetSW can't be 'NA'"),
},
{
rule: CPEMatchingRule{
CPESpecs: []CPEMatchingRuleSpec{
{
Vendor: "microsoft",
Product: "word",
TargetSW: "windows",
SemVerConstraint: ".as.-as",
},
},
CVEs: map[string]struct{}{"CVE-123": {}},
}, err: errors.New("improper constraint: .as.-as"),
},
{
rule: CPEMatchingRule{
CPESpecs: []CPEMatchingRuleSpec{
{
Vendor: "microsoft",
Product: "word",
TargetSW: "windows",
SemVerConstraint: "1.2.3",
},
},
}, err: errors.New("At least one CVE is required"),
},
{
rule: CPEMatchingRule{
CPESpecs: []CPEMatchingRuleSpec{
{
Vendor: "microsoft",
Product: "word",
TargetSW: "windows",
SemVerConstraint: "1.2.3",
},
},
CVEs: map[string]struct{}{"": {}, " ": {}, "CVE-123": {}},
}, err: errors.New("CVE can't be empty"),
},
}
for _, tc := range testCases {
result := tc.rule.Validate()
require.Equal(t, tc.err, result)
}
})
}
@@ -0,0 +1,128 @@
package nvd
import (
"fmt"
)
type CPEMatchingRules []CPEMatchingRule
// GetKnownNVDBugRules returns a list of CPEMatchingRules used for
// ignoring false positives detected during the NVD vuln. detection process.
func GetKnownNVDBugRules() (CPEMatchingRules, error) {
rules := CPEMatchingRules{
CPEMatchingRule{
CPESpecs: []CPEMatchingRuleSpec{
{
Vendor: "apple",
Product: "icloud",
TargetSW: "windows",
SemVerConstraint: "< 7.1",
},
},
CVEs: map[string]struct{}{
"CVE-2017-13797": {},
},
},
CPEMatchingRule{
CPESpecs: []CPEMatchingRuleSpec{
{
Vendor: "apple",
Product: "icloud",
TargetSW: "windows",
SemVerConstraint: "<= 6.1.1",
},
},
CVEs: map[string]struct{}{
"CVE-2016-4613": {},
"CVE-2017-2383": {},
},
},
CPEMatchingRule{
CPESpecs: []CPEMatchingRuleSpec{
{
Vendor: "apple",
Product: "icloud",
TargetSW: "windows",
SemVerConstraint: "<= 6.1.0",
},
},
CVEs: map[string]struct{}{
"CVE-2017-2366": {},
},
},
CPEMatchingRule{
CPESpecs: []CPEMatchingRuleSpec{
{
Vendor: "apple",
Product: "icloud",
TargetSW: "windows",
SemVerConstraint: "<= 6.0.0",
},
},
CVEs: map[string]struct{}{
"CVE-2016-4613": {},
"CVE-2016-7583": {},
},
},
CPEMatchingRule{
CPESpecs: []CPEMatchingRuleSpec{
{
Vendor: "apple",
Product: "icloud",
TargetSW: "windows",
SemVerConstraint: "<= 6.0.1",
},
},
CVEs: map[string]struct{}{
"CVE-2016-4692": {},
"CVE-2016-4743": {},
"CVE-2016-7578": {},
"CVE-2016-7586": {},
"CVE-2016-7587": {},
"CVE-2016-7589": {},
"CVE-2016-7592": {},
"CVE-2016-7598": {},
"CVE-2016-7599": {},
"CVE-2016-7610": {},
"CVE-2016-7611": {},
"CVE-2016-7614": {},
"CVE-2016-7632": {},
"CVE-2016-7635": {},
"CVE-2016-7639": {},
"CVE-2016-7640": {},
"CVE-2016-7641": {},
"CVE-2016-7642": {},
"CVE-2016-7645": {},
"CVE-2016-7646": {},
"CVE-2016-7648": {},
"CVE-2016-7649": {},
"CVE-2016-7652": {},
"CVE-2016-7654": {},
"CVE-2016-7656": {},
},
},
}
for i, rule := range rules {
if err := rule.Validate(); err != nil {
return nil, fmt.Errorf("invalid rule %d: %w", i, err)
}
}
return rules, nil
}
// FindMatch returns the first matching rule
func (rules CPEMatchingRules) FindMatch(cve string) (*CPEMatchingRule, bool) {
for _, rule := range rules {
if _, ok := rule.CVEs[cve]; ok {
return &rule, true
}
}
return nil, false
}
+24 -1
View File
@@ -132,11 +132,25 @@ func TranslateCPEToCVE(
return nil, nil
}
knownNVDBugRules, err := GetKnownNVDBugRules()
if err != nil {
return nil, err
}
// we are using a map here to remove any duplicates - a vulnerability can be present in more than one
// NVD feed file.
vulns := make(map[string]fleet.SoftwareVulnerability)
for _, file := range files {
foundVulns, err := checkCVEs(ctx, ds, logger, parsed, file, collectVulns)
foundVulns, err := checkCVEs(
ctx,
ds,
logger,
parsed,
file,
collectVulns,
knownNVDBugRules,
)
if err != nil {
return nil, err
}
@@ -179,6 +193,7 @@ func checkCVEs(
softwareCPEs []softwareCPEWithNVDMeta,
file string,
collectVulns bool,
knownNVDBugRules CPEMatchingRules,
) ([]fleet.SoftwareVulnerability, error) {
dict, err := cvefeed.LoadJSONDictionary(file)
if err != nil {
@@ -218,6 +233,14 @@ func checkCVEs(
continue
}
if rule, ok := knownNVDBugRules.FindMatch(
matches.CVE.ID(),
); ok {
if !rule.CPEMatches(softwareCPE.meta) {
continue
}
}
vuln := fleet.SoftwareVulnerability{
SoftwareID: softwareCPE.SoftwareID,
CVE: matches.CVE.ID(),
+51 -9
View File
@@ -109,25 +109,26 @@ var firefox93WindowsVulnerabilities = []string{
}
var cvetests = []struct {
cpe string
cves []string
cpe string
excludedCVEs []string
includedCVEs []string
// continuesToUpdate indicates if the product/software
// continues to register new CVE vulnerabilities.
continuestoUpdate bool
}{
{
cpe: "cpe:2.3:a:1password:1password:3.9.9:*:*:*:*:macos:*:*",
cves: []string{"CVE-2012-6369"},
includedCVEs: []string{"CVE-2012-6369"},
continuestoUpdate: false,
},
{
cpe: "cpe:2.3:a:1password:1password:3.9.9:*:*:*:*:*:*:*",
cves: []string{"CVE-2012-6369"},
includedCVEs: []string{"CVE-2012-6369"},
continuestoUpdate: false,
},
{
cpe: "cpe:2.3:a:pypa:pip:9.0.3:*:*:*:*:python:*:*",
cves: []string{
includedCVEs: []string{
"CVE-2019-20916",
"CVE-2021-3572",
},
@@ -135,12 +136,49 @@ var cvetests = []struct {
},
{
cpe: "cpe:2.3:a:mozilla:firefox:93.0:*:*:*:*:windows:*:*",
cves: firefox93WindowsVulnerabilities,
includedCVEs: firefox93WindowsVulnerabilities,
continuestoUpdate: true,
},
{
cpe: "cpe:2.3:a:mozilla:firefox:93.0.100:*:*:*:*:windows:*:*",
cves: firefox93WindowsVulnerabilities,
includedCVEs: firefox93WindowsVulnerabilities,
continuestoUpdate: true,
},
{
cpe: "cpe:2.3:a:apple:icloud:1.0:*:*:*:*:macos:*:*",
excludedCVEs: []string{
"CVE-2017-13797",
"CVE-2017-2383",
"CVE-2017-2366",
"CVE-2016-4613",
"CVE-2016-4692",
"CVE-2016-4743",
"CVE-2016-7578",
"CVE-2016-7583",
"CVE-2016-7586",
"CVE-2016-7587",
"CVE-2016-7589",
"CVE-2016-7592",
"CVE-2016-7598",
"CVE-2016-7599",
"CVE-2016-7610",
"CVE-2016-7611",
"CVE-2016-7614",
"CVE-2016-7632",
"CVE-2016-7635",
"CVE-2016-7639",
"CVE-2016-7640",
"CVE-2016-7641",
"CVE-2016-7642",
"CVE-2016-7645",
"CVE-2016-7646",
"CVE-2016-7648",
"CVE-2016-7649",
"CVE-2016-7652",
"CVE-2016-7654",
"CVE-2016-7656",
"CVE-2017-2383",
},
continuestoUpdate: true,
},
}
@@ -219,12 +257,16 @@ func TestTranslateCPEToCVE(t *testing.T) {
// Given that new vulnerabilities can be found on these
// packages/products, we check that at least the
// known ones are found.
for _, cve := range tt.cves {
for _, cve := range tt.includedCVEs {
require.Contains(t, cvesFound, cve, tt.cpe)
}
} else {
// Check for exact match of CVEs found.
require.ElementsMatch(t, cvesFound, tt.cves, tt.cpe)
require.ElementsMatch(t, cvesFound, tt.includedCVEs, tt.cpe)
}
for _, cve := range tt.excludedCVEs {
require.NotContains(t, cvesFound, cve, tt.cpe)
}
require.True(t, ds.DeleteOutOfDateVulnerabilitiesFuncInvoked)
@@ -15,7 +15,7 @@ type DpkgInfoTest struct {
StateMatch StateMatchType
}
// Eval evaluates the given dpkg info test againts a host's installed packages.
// Eval evaluates the given dpkg info test against a host's installed packages.
// If test evaluates to true, returns all Software involved with the test match, otherwise will
// return nil.
func (t *DpkgInfoTest) Eval(packages []fleet.Software) ([]fleet.Software, error) {
@@ -18,7 +18,7 @@ func (sta ObjectStateString) unpack() (OperationType, string) {
return NewOperationType(parts[0]), parts[1]
}
// Eval evaluates the provided value againts the encoded value in sta according to the encoded
// Eval evaluates the provided value against the encoded value in sta according to the encoded
// operation.
func (sta ObjectStateString) Eval(other string) (bool, error) {
op, val := sta.unpack()
+1 -1
View File
@@ -3,7 +3,7 @@ package oval_parsed
import "github.com/fleetdm/fleet/v4/server/fleet"
type Result interface {
// Eval evaluates the current OVAL definition againts an OS version and a list of installed software, returns all software
// Eval evaluates the current OVAL definition against an OS version and a list of installed software, returns all software
// vulnerabilities found.
Eval(fleet.OSVersion, []fleet.Software) ([]fleet.SoftwareVulnerability, error)
}
@@ -12,7 +12,7 @@ type RpmInfoTest struct {
StateMatch StateMatchType
}
// Eval evaluates the given test againts a host's installed packages.
// Eval evaluates the given test against a host's installed packages.
// If test evaluates to true, returns all Software involved with the test match, otherwise will
// return nil.
func (t *RpmInfoTest) Eval(packages []fleet.Software) ([]fleet.Software, error) {