Ubuntu Kernel Vulns Part 2: Matching (#19303)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- fleet now detects Ubuntu kernel vulnerabilities from the Canonical OVAL feed
|
||||
@@ -72,6 +72,12 @@ func Analyze(
|
||||
return nil, err
|
||||
}
|
||||
foundInBatch[hostID] = evalR
|
||||
|
||||
evalU, err := defs.EvalKernel(software)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
foundInBatch[hostID] = append(foundInBatch[hostID], evalU...)
|
||||
}
|
||||
|
||||
existingInBatch, err := ds.ListSoftwareVulnerabilitiesByHostIDsSource(ctx, hostIDs, source)
|
||||
|
||||
@@ -102,3 +102,99 @@ func (d Definition) CveVulnerabilities() []string {
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// intersect returns the intersection of two slices of uints.
|
||||
func intersect(a, b []uint) []uint {
|
||||
m := make(map[uint]bool)
|
||||
for _, v := range a {
|
||||
m[v] = true
|
||||
}
|
||||
|
||||
var r []uint
|
||||
for _, v := range b {
|
||||
if m[v] {
|
||||
r = append(r, v)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// unionAll returns the union of two slices of uints without duplicates.
|
||||
func unionAll(a, b []uint) []uint {
|
||||
m := make(map[uint]bool)
|
||||
var result []uint
|
||||
|
||||
for _, v := range a {
|
||||
if !m[v] {
|
||||
m[v] = true
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range b {
|
||||
if !m[v] {
|
||||
m[v] = true
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// findMatchingSoftware returns the software IDs that match the given OVAL criteria.
|
||||
func findMatchingSoftware(c Criteria, uTests map[int][]uint) []uint {
|
||||
switch c.Operator {
|
||||
case And:
|
||||
return findAndMatch(c, uTests)
|
||||
case Or:
|
||||
return findOrMatch(c, uTests)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findAndMatch finds the software that matches all the criteria using the AND operator
|
||||
func findAndMatch(c Criteria, uTests map[int][]uint) []uint {
|
||||
if c.Criteriums != nil {
|
||||
return intersectSoftware(c.Criteriums, uTests)
|
||||
}
|
||||
|
||||
matchingSoftware := make([]uint, 0)
|
||||
for _, subCriteria := range c.Criterias {
|
||||
subMatchingSoftware := findMatchingSoftware(*subCriteria, uTests)
|
||||
if len(matchingSoftware) == 0 {
|
||||
matchingSoftware = subMatchingSoftware
|
||||
} else {
|
||||
matchingSoftware = intersect(matchingSoftware, subMatchingSoftware)
|
||||
}
|
||||
}
|
||||
return matchingSoftware
|
||||
}
|
||||
|
||||
// intersectSoftware returns the intersection of the software IDs for the given criteria.
|
||||
func intersectSoftware(criteriums []int, uTests map[int][]uint) []uint {
|
||||
if len(criteriums) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
softwareSets := make([][]uint, 0, len(criteriums))
|
||||
for _, c := range criteriums {
|
||||
softwareSets = append(softwareSets, uTests[c])
|
||||
}
|
||||
|
||||
intersected := softwareSets[0]
|
||||
for _, s := range softwareSets[1:] {
|
||||
intersected = intersect(intersected, s)
|
||||
}
|
||||
|
||||
return intersected
|
||||
}
|
||||
|
||||
// findOrMatch finds the software that matches any of the criteria using the OR operator
|
||||
func findOrMatch(c Criteria, uTests map[int][]uint) []uint {
|
||||
matchingSoftware := make([]uint, 0)
|
||||
for _, subCriteria := range c.Criterias {
|
||||
subMatchingSoftware := findMatchingSoftware(*subCriteria, uTests)
|
||||
matchingSoftware = unionAll(matchingSoftware, subMatchingSoftware)
|
||||
}
|
||||
return matchingSoftware
|
||||
}
|
||||
|
||||
@@ -289,3 +289,183 @@ func TestOvalParsedDefinition(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntersect(t *testing.T) {
|
||||
a := []uint{1, 2, 3, 4}
|
||||
b := []uint{3, 4, 5, 6}
|
||||
expected := []uint{3, 4}
|
||||
result := intersect(a, b)
|
||||
require.ElementsMatch(t, expected, result)
|
||||
}
|
||||
|
||||
func TestUnion(t *testing.T) {
|
||||
a := []uint{1, 2, 3, 4}
|
||||
b := []uint{3, 4, 5, 6}
|
||||
expected := []uint{1, 2, 3, 4, 5, 6}
|
||||
result := unionAll(a, b)
|
||||
require.ElementsMatch(t, expected, result)
|
||||
|
||||
// a has duplicates
|
||||
a = []uint{1, 2, 3, 4, 4}
|
||||
expected = []uint{1, 2, 3, 4, 5, 6}
|
||||
result = unionAll(a, b)
|
||||
require.ElementsMatch(t, expected, result)
|
||||
|
||||
// b has duplicates
|
||||
a = []uint{1, 2, 3, 4}
|
||||
b = []uint{3, 4, 5, 6, 6}
|
||||
expected = []uint{1, 2, 3, 4, 5, 6}
|
||||
result = unionAll(a, b)
|
||||
require.ElementsMatch(t, expected, result)
|
||||
}
|
||||
|
||||
func TestFindAndMatch(t *testing.T) {
|
||||
// map of tests to softwareIDs
|
||||
criterionToSoftware := map[int][]uint{
|
||||
100: {1, 2, 3},
|
||||
200: {3, 4, 5},
|
||||
300: {5, 6, 7},
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
criteria Criteria
|
||||
expected []uint
|
||||
}{
|
||||
{
|
||||
// Criteria: 100 AND 200 must match
|
||||
criteria: Criteria{
|
||||
Operator: And,
|
||||
Criteriums: []int{100, 200},
|
||||
},
|
||||
expected: []uint{3},
|
||||
},
|
||||
{
|
||||
// Criteria: 100 and 200 and 300 must match
|
||||
criteria: Criteria{
|
||||
Operator: And,
|
||||
Criteriums: []int{100, 200, 300},
|
||||
},
|
||||
expected: []uint{},
|
||||
},
|
||||
{
|
||||
// Criteria: 100 must match
|
||||
criteria: Criteria{
|
||||
Operator: And,
|
||||
Criteriums: []int{100},
|
||||
},
|
||||
expected: []uint{1, 2, 3},
|
||||
},
|
||||
} {
|
||||
result := findAndMatch(tc.criteria, criterionToSoftware)
|
||||
require.ElementsMatch(t, tc.expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindOrMatch(t *testing.T) {
|
||||
// map of tests to softwareIDs
|
||||
criterionToSoftware := map[int][]uint{
|
||||
100: {1, 2, 3},
|
||||
200: {3, 4, 5},
|
||||
300: {5, 6, 7},
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
criteria Criteria
|
||||
expected []uint
|
||||
}{
|
||||
{
|
||||
// Criteria: 100 OR 200 must match
|
||||
criteria: Criteria{
|
||||
Operator: Or,
|
||||
Criteriums: nil,
|
||||
Criterias: []*Criteria{
|
||||
{
|
||||
Operator: And,
|
||||
Criteriums: []int{100},
|
||||
},
|
||||
{
|
||||
Operator: And,
|
||||
Criteriums: []int{200},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []uint{1, 2, 3, 4, 5},
|
||||
},
|
||||
{
|
||||
// Criteria: 100 OR 200 OR 300 must match
|
||||
criteria: Criteria{
|
||||
Operator: Or,
|
||||
Criteriums: nil,
|
||||
Criterias: []*Criteria{
|
||||
{
|
||||
Operator: And,
|
||||
Criteriums: []int{100},
|
||||
Criterias: nil,
|
||||
},
|
||||
{
|
||||
Operator: And,
|
||||
Criteriums: []int{200},
|
||||
Criterias: nil,
|
||||
},
|
||||
{
|
||||
Operator: And,
|
||||
Criteriums: []int{300},
|
||||
Criterias: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []uint{1, 2, 3, 4, 5, 6, 7},
|
||||
},
|
||||
{
|
||||
// Criteria: 100 OR 200 OR 300 must match
|
||||
criteria: Criteria{
|
||||
Operator: Or,
|
||||
Criteriums: nil,
|
||||
Criterias: []*Criteria{
|
||||
{
|
||||
Operator: And,
|
||||
Criteriums: []int{100},
|
||||
Criterias: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []uint{1, 2, 3},
|
||||
},
|
||||
} {
|
||||
result := findOrMatch(tc.criteria, criterionToSoftware)
|
||||
require.ElementsMatch(t, tc.expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindMatchingSoftware(t *testing.T) {
|
||||
criterionToSoftware := map[int][]uint{
|
||||
100: {1, 2, 3},
|
||||
200: {3, 4, 5},
|
||||
300: {5, 6, 7},
|
||||
400: {7, 8, 9},
|
||||
}
|
||||
|
||||
criteria := Criteria{
|
||||
Operator: And,
|
||||
Criteriums: nil,
|
||||
Criterias: []*Criteria{
|
||||
{
|
||||
Operator: Or,
|
||||
Criteriums: nil,
|
||||
Criterias: []*Criteria{
|
||||
{
|
||||
Operator: And,
|
||||
Criteriums: []int{100, 200},
|
||||
},
|
||||
{
|
||||
Operator: And,
|
||||
Criteriums: []int{300, 400},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
matchingSoftware := findMatchingSoftware(criteria, criterionToSoftware)
|
||||
require.ElementsMatch(t, []uint{3, 7}, matchingSoftware)
|
||||
}
|
||||
|
||||
@@ -6,4 +6,8 @@ type Result interface {
|
||||
// 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)
|
||||
|
||||
// EvalKernel evaluates the current OVAL definition against a list of installed kernel-image software,
|
||||
// returns all kernel-image vulnerabilities found. Currently only used for Ubuntu.
|
||||
EvalKernel([]fleet.Software) ([]fleet.SoftwareVulnerability, error)
|
||||
}
|
||||
|
||||
@@ -59,3 +59,8 @@ func (r RhelResult) Eval(ver fleet.OSVersion, software []fleet.Software) ([]flee
|
||||
|
||||
return vuln, nil
|
||||
}
|
||||
|
||||
// EvalUname is not implemented for Rhel based distros
|
||||
func (r RhelResult) EvalKernel(software []fleet.Software) ([]fleet.SoftwareVulnerability, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package oval_parsed
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
)
|
||||
|
||||
@@ -67,3 +71,44 @@ func (r UbuntuResult) Eval(ver fleet.OSVersion, software []fleet.Software) ([]fl
|
||||
|
||||
return vuln, nil
|
||||
}
|
||||
|
||||
var kernelImageRegex = regexp.MustCompile(`^linux-image-(\d+\.\d+\.\d+-\d+)-\w+`)
|
||||
|
||||
func (r UbuntuResult) EvalKernel(software []fleet.Software) ([]fleet.SoftwareVulnerability, error) {
|
||||
// Test Id => Matching software IDs
|
||||
uTests := make(map[int][]uint)
|
||||
for _, s := range software {
|
||||
if kernelImageRegex.MatchString(s.Name) {
|
||||
v, ok := strings.CutPrefix(s.Name, "linux-image-")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("linux kernel package %s does not match expected format:", s.Name)
|
||||
}
|
||||
|
||||
for i, u := range r.UnameTests {
|
||||
isMatch, err := u.Eval(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
uTests[i] = append(uTests[i], s.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vuln := make([]fleet.SoftwareVulnerability, 0)
|
||||
for _, d := range r.Definitions {
|
||||
swIDs := findMatchingSoftware(*d.Criteria, uTests)
|
||||
for _, v := range d.CveVulnerabilities() {
|
||||
for _, swID := range swIDs {
|
||||
vuln = append(vuln, fleet.SoftwareVulnerability{
|
||||
SoftwareID: swID,
|
||||
CVE: v,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return vuln, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package oval_parsed
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEvalKernel(t *testing.T) {
|
||||
r := NewUbuntuResult()
|
||||
r.AddDefinition(Definition{
|
||||
Criteria: &Criteria{
|
||||
Operator: And,
|
||||
Criteriums: []int{100, 200},
|
||||
},
|
||||
Vulnerabilities: []string{"CVE-2019-1234"},
|
||||
})
|
||||
r.AddUnameTest(100, &UnixUnameTest{
|
||||
States: []ObjectStateString{
|
||||
NewObjectStateString("less than", "0:5.15.0-1004"),
|
||||
},
|
||||
})
|
||||
r.AddUnameTest(200, &UnixUnameTest{
|
||||
States: []ObjectStateString{
|
||||
NewObjectStateString("pattern match", `5.15.0-\d+(-generic|-generic-64k|-generic-lpae|-lowlatency|-lowlatency-64k)`),
|
||||
},
|
||||
})
|
||||
|
||||
software := []fleet.Software{
|
||||
{ID: 1, Name: "linux-image-5.15.0-1003-generic"},
|
||||
{ID: 2, Name: "linux-image-5.15.0-1004-generic"},
|
||||
{ID: 3, Name: "linux-image-5.15.0-1005-generic"},
|
||||
{ID: 4, Name: "linux-image-5.15.0-1003-lowlatency"},
|
||||
{ID: 5, Name: "linux-image-5.15.0-1004-foo"},
|
||||
{ID: 6, Name: "linux-image-4.0.0-10-generic"},
|
||||
{ID: 7, Name: "linux-image-6.0.0-10-generic"},
|
||||
}
|
||||
|
||||
vuln, err := r.EvalKernel(software)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, vuln, []fleet.SoftwareVulnerability{
|
||||
{SoftwareID: 1, CVE: "CVE-2019-1234"},
|
||||
{SoftwareID: 4, CVE: "CVE-2019-1234"},
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,38 @@
|
||||
package oval_parsed
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/vulnerabilities/utils"
|
||||
)
|
||||
|
||||
type UnixUnameTest struct {
|
||||
States []ObjectStateString
|
||||
}
|
||||
|
||||
// Eval evaluates a kernel version against a UnameTest. Returns true
|
||||
// if the kernel version matches the test. Currently only used for Ubuntu.
|
||||
func (t UnixUnameTest) Eval(version string) (bool, error) {
|
||||
for _, s := range t.States {
|
||||
op, val := s.unpack()
|
||||
switch op {
|
||||
case LessThan:
|
||||
if utils.Rpmvercmp(version, val) != -1 {
|
||||
return false, nil
|
||||
}
|
||||
case PatternMatch:
|
||||
match, err := regexp.Compile(val)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !match.MatchString(version) {
|
||||
return false, nil
|
||||
}
|
||||
default:
|
||||
return false, fmt.Errorf("operation %q not supported for uname test", op)
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package oval_parsed
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEval(t *testing.T) {
|
||||
utest := UnixUnameTest{
|
||||
States: []ObjectStateString{
|
||||
NewObjectStateString("less than", "0:5.15.0-1004"),
|
||||
NewObjectStateString("pattern match", `5.15.0-\d+(-generic|-generic-64k|-generic-lpae|-lowlatency|-lowlatency-64k)`),
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
Name string
|
||||
Input string
|
||||
Expected bool
|
||||
}{
|
||||
{Name: "less than", Input: "5.15.0-1003-generic", Expected: true},
|
||||
{Name: "greater than", Input: "5.15.0-1005-generic", Expected: false},
|
||||
{Name: "equal", Input: "5.15.0-1004-generic", Expected: false},
|
||||
{Name: "alt pattern match", Input: "5.15.0-1003-lowlatency", Expected: true},
|
||||
{Name: "suffix doesn't match", Input: "5.15.0-1004-foo", Expected: false},
|
||||
{Name: "lower version fails pattern match", Input: "4.0.0-10-generic", Expected: false},
|
||||
{Name: "higher version fails pattern match", Input: "6.0.0-10-generic", Expected: false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
matches, err := utest.Eval(tc.Input)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.Expected, matches)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user