Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CWS] reduce complexity of the approver discovery #11508

Merged
merged 1 commit into from
Apr 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/security/secl/compiler/eval/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ func TestPartial(t *testing.T) {
{Expr: `process.name =~ "/usr/sbin/*" && process.uid == 0 && process.is_root`, Field: "process.uid", IsDiscarder: true},
}

ctx := NewContext(unsafe.Pointer(&event))
ctx := NewContext(event.GetPointer())

for _, test := range tests {
model := &testModel{}
Expand Down
6 changes: 6 additions & 0 deletions pkg/security/secl/compiler/eval/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ func (s *State) UpdateFieldValues(field Field, value FieldValue) error {
if !ok {
values = []FieldValue{}
}
for _, v := range values {
if v == value {
return nil
}
}

values = append(values, value)
s.fieldValues[field] = values
return s.model.ValidateField(field, value)
Expand Down
134 changes: 134 additions & 0 deletions pkg/security/secl/rules/approvers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.

package rules

import (
"github.com/DataDog/datadog-agent/pkg/security/secl/compiler/eval"
)

// Approvers are just filter values indexed by field
type Approvers map[eval.Field]FilterValues

// isAnApprover returns whether the given value is an approver for the given rule
func isAnApprover(event eval.Event, ctx *eval.Context, rule *Rule, field eval.Field, value interface{}) (bool, error) {
if err := event.SetFieldValue(field, value); err != nil {
return false, err
}
origResult, err := rule.PartialEval(ctx, field)
if err != nil {
return false, err
}

notValue, err := eval.NotOfValue(value)
if err != nil {
return false, err
}

if err := event.SetFieldValue(field, notValue); err != nil {
return false, err
}
notResult, err := rule.PartialEval(ctx, field)
if err != nil {
return false, err
}

if origResult && !notResult {
return true, nil
}

return false, nil
}

func bitmaskCombinations(bitmasks []int) []int {
if len(bitmasks) == 0 {
return nil
}

var result []int
for i := 0; i < (1 << len(bitmasks)); i++ {
var mask int
for j, value := range bitmasks {
if (i & (1 << j)) > 0 {
mask |= value
}
}
result = append(result, mask)
}

return result
}

// GetApprovers returns approvers for the given rules
func GetApprovers(rules []*Rule, event eval.Event, fieldCaps FieldCapabilities) (Approvers, error) {
approvers := make(Approvers)

ctx := eval.NewContext(event.GetPointer())

// for each rule we should at least find one approver otherwise we will return no approver for the field
for _, rule := range rules {
var bestFilterField eval.Field
var bestFilterValues FilterValues
var bestFilterWeight int

LOOP:
for _, fieldCap := range fieldCaps {
field := fieldCap.Field

var filterValues FilterValues
var bitmasks []int

for _, value := range rule.GetFieldValues(field) {
switch value.Type {
case eval.ScalarValueType, eval.PatternValueType:
isAnApprover, err := isAnApprover(event, ctx, rule, field, value.Value)
if err != nil {
return nil, err
}

if isAnApprover {
filterValues = filterValues.Merge(FilterValue{Field: field, Value: value.Value, Type: value.Type})
} else if fieldCap.Types&eval.BitmaskValueType == 0 {
// if not a bitmask we need to have all the value as approvers
// basically a list of values ex: in ["test123", "test456"]
continue LOOP
}
case eval.BitmaskValueType:
bitmasks = append(bitmasks, value.Value.(int))
}
}

for _, bitmask := range bitmaskCombinations(bitmasks) {
isAnApprover, err := isAnApprover(event, ctx, rule, field, bitmask)
if err != nil {
return nil, err
}

if isAnApprover {
filterValues = filterValues.Merge(FilterValue{Field: field, Value: bitmask, Type: eval.BitmaskValueType})
}
}

if len(filterValues) == 0 || !fieldCaps.Validate(filterValues) {
continue
}

if bestFilterValues == nil || fieldCap.FilterWeight > bestFilterWeight {
bestFilterField = field
bestFilterValues = filterValues
bestFilterWeight = fieldCap.FilterWeight
}
}

// no filter value for a rule thus no approver for the event type
if bestFilterValues == nil {
return nil, nil
}

approvers[bestFilterField] = append(approvers[bestFilterField], bestFilterValues...)
}

return approvers, nil
}
83 changes: 0 additions & 83 deletions pkg/security/secl/rules/bucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ import (
"github.com/DataDog/datadog-agent/pkg/security/secl/compiler/eval"
)

// Approvers are just filter values indexed by field
type Approvers map[eval.Field]FilterValues

// RuleBucket groups rules with the same event type
type RuleBucket struct {
rules []*Rule
Expand Down Expand Up @@ -48,83 +45,3 @@ func (rb *RuleBucket) AddRule(rule *Rule) error {
func (rb *RuleBucket) GetRules() []*Rule {
return rb.rules
}

// FieldCombinations - array all the combinations of field
type FieldCombinations [][]eval.Field

func (a FieldCombinations) Len() int { return len(a) }
func (a FieldCombinations) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a FieldCombinations) Less(i, j int) bool { return len(a[i]) < len(a[j]) }

func fieldCombinations(fields []eval.Field) FieldCombinations {
var result FieldCombinations

for i := 1; i < (1 << len(fields)); i++ {
var subResult []eval.Field
for j, field := range fields {
if (i & (1 << j)) > 0 {
subResult = append(subResult, field)
}
}
result = append(result, subResult)
}

// order the list with the single field first
sort.Sort(result)

return result
}

func getFilterValuesWeight(filterValues FilterValues, fieldCaps FieldCapabilities) int {
var maxWeight int
for _, filterValue := range filterValues {
for _, fc := range fieldCaps {
if filterValue.Field == fc.Field && fc.FilterWeight > maxWeight {
maxWeight = fc.FilterWeight
}
}
}
return maxWeight
}

// GetApprovers returns the approvers for an event
func (rb *RuleBucket) GetApprovers(event eval.Event, fieldCaps FieldCapabilities) (Approvers, error) {
fcs := fieldCombinations(fieldCaps.GetFields())

approvers := make(Approvers)
for _, rule := range rb.rules {
truthTable, err := newTruthTable(rule.Rule, event)
if err != nil {
return nil, err
}

var filterValues FilterValues
var maxWeight int

for _, fields := range fcs {
candidates := truthTable.getApprovers(fields...)

// only one combination of the approvers is required, keep the best one meaning
// the one with the highest weight
if len(candidates) > 0 && fieldCaps.Validate(candidates) {
// as fieldCombinations return all the combinations in the less fields order
// we keep the highest weight with a minimal set of fields.
if weight := getFilterValuesWeight(candidates, fieldCaps); filterValues == nil || weight > maxWeight {
filterValues = candidates
maxWeight = weight
}
}
}

if len(filterValues) == 0 {
return nil, &ErrNoApprover{Fields: fieldCaps.GetFields()}
}

// dispatch per field
for _, filterValue := range filterValues {
approvers[filterValue.Field] = approvers[filterValue.Field].Merge(filterValue)
}
}

return approvers, nil
}
8 changes: 1 addition & 7 deletions pkg/security/secl/rules/filter_values.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,14 @@ type FilterValue struct {
Field eval.Field
Value interface{}
Type eval.FieldValueType

// indicate process.uid == process.gid for example
isScalar bool
// opposite value of the field Value
notValue interface{}
not bool
}

// Merge merges to FilterValues ensuring there is no duplicate value
func (fv FilterValues) Merge(n ...FilterValue) FilterValues {
LOOP:
for _, v1 := range n {
for _, v2 := range fv {
if v1.Field == v2.Field && v1.Value == v2.Value && v1.not == v2.not {
if v1.Field == v2.Field && v1.Value == v2.Value {
continue LOOP
}
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/security/secl/rules/ruleset.go
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ func (rs *RuleSet) GetApprovers(fieldCaps map[eval.EventType]FieldCapabilities)
}

eventApprovers, err := rs.GetEventApprovers(eventType, caps)
if err != nil {
if err != nil || len(eventApprovers) == 0 {
continue
}
approvers[eventType] = eventApprovers
Expand All @@ -412,7 +412,7 @@ func (rs *RuleSet) GetEventApprovers(eventType eval.EventType, fieldCaps FieldCa
return nil, ErrNoEventTypeBucket{EventType: eventType}
}

return bucket.GetApprovers(rs.eventCtor(), fieldCaps)
return GetApprovers(bucket.rules, rs.eventCtor(), fieldCaps)
}

// GetFieldValues returns all the values of the given field
Expand Down
Loading