github.com/DataDog/datadog-agent/pkg/security/secl@v0.55.0-devel.0.20240517055856-10c4965fea94/rules/evaluation_set_test.go (about) 1 // Unless explicitly stated otherwise all files in this repository are licensed 2 // under the Apache License Version 2.0. 3 // This product includes software developed at Datadog (https://www.datadoghq.com/). 4 // Copyright 2016-present Datadog, Inc. 5 6 //go:build linux 7 8 // Package rules holds rules related files 9 package rules 10 11 import ( 12 "fmt" 13 "path/filepath" 14 "strings" 15 "testing" 16 17 "github.com/DataDog/datadog-agent/pkg/security/secl/compiler/eval" 18 "github.com/google/go-cmp/cmp" 19 "github.com/google/go-cmp/cmp/cmpopts" 20 "github.com/hashicorp/go-multierror" 21 "github.com/stretchr/testify/assert" 22 ) 23 24 // Tests 25 26 func TestEvaluationSet_GetPolicies(t *testing.T) { 27 type fields struct { 28 RuleSets map[eval.RuleSetTagValue]*RuleSet 29 } 30 tests := []struct { 31 name string 32 fields fields 33 want []*Policy 34 }{ 35 { 36 name: "duplicated policies", 37 fields: fields{ 38 RuleSets: map[eval.RuleSetTagValue]*RuleSet{ 39 DefaultRuleSetTagValue: { 40 policies: []*Policy{ 41 {Name: "policy 1"}, 42 {Name: "policy 2"}, 43 }}, 44 "threat_score": { 45 policies: []*Policy{ 46 {Name: "policy 3"}, 47 {Name: "policy 2"}, 48 }}, 49 "special": { 50 policies: []*Policy{ 51 {Name: "policy 3"}, 52 {Name: "policy 2"}, 53 }}, 54 }, 55 }, 56 want: []*Policy{{Name: "policy 1"}, 57 {Name: "policy 2"}, {Name: "policy 3"}, 58 }, 59 }, 60 } 61 62 for _, tt := range tests { 63 t.Run(tt.name, func(t *testing.T) { 64 es := &EvaluationSet{ 65 RuleSets: tt.fields.RuleSets, 66 } 67 assert.Equalf(t, len(tt.want), len(es.GetPolicies()), "GetPolicies()") 68 for _, policy := range tt.want { 69 assert.Contains(t, es.GetPolicies(), policy) 70 } 71 }) 72 } 73 } 74 75 // go test -v github.com/DataDog/datadog-agent/pkg/security/secl/rules --run="TestEvaluationSet_LoadPolicies_Overriding" 76 func TestEvaluationSet_LoadPolicies_Overriding(t *testing.T) { 77 type fields struct { 78 Providers []PolicyProvider 79 TagValues []eval.RuleSetTagValue 80 } 81 tests := []struct { 82 name string 83 fields fields 84 want func(t assert.TestingT, fields fields, got *EvaluationSet, msgs ...interface{}) bool 85 wantErr func(t assert.TestingT, err *multierror.Error, msgs ...interface{}) bool 86 wantRules map[eval.RuleID]*Rule 87 }{ 88 { 89 name: "duplicate IDs", 90 fields: fields{ 91 Providers: []PolicyProvider{ 92 dummyDirProvider{ 93 dummyLoadPoliciesFunc: func() ([]*Policy, *multierror.Error) { 94 return []*Policy{{ 95 Name: "default.policy", 96 Source: PolicyProviderTypeDir, 97 Version: "", 98 Rules: []*RuleDefinition{ 99 { 100 ID: "foo", 101 Expression: "open.file.path == \"/etc/local-default/shadow\"", 102 }, 103 { 104 ID: "bar", 105 Expression: "open.file.path == \"/etc/local-default/file\"", 106 }, 107 }, 108 Macros: nil, 109 }}, nil 110 }, 111 }, 112 dummyRCProvider{ 113 dummyLoadPoliciesFunc: func() ([]*Policy, *multierror.Error) { 114 return []*Policy{{ 115 Name: "myRC.policy", 116 Source: PolicyProviderTypeRC, 117 Rules: []*RuleDefinition{ 118 { 119 ID: "foo", 120 Expression: "open.file.path == \"/etc/rc-custom/shadow\"", 121 }, 122 }, 123 }}, nil 124 }, 125 }, 126 }, 127 }, 128 want: func(t assert.TestingT, fields fields, got *EvaluationSet, msgs ...interface{}) bool { 129 gotNumberOfRules := len(got.RuleSets[DefaultRuleSetTagValue].rules) 130 assert.Equal(t, 2, gotNumberOfRules) 131 132 expectedRules := map[eval.RuleID]*Rule{ 133 "foo": { 134 Rule: &eval.Rule{ 135 ID: "foo", 136 Expression: "open.file.path == \"/etc/local-default/shadow\"", 137 }, 138 Definition: &RuleDefinition{ 139 ID: "foo", 140 Expression: "open.file.path == \"/etc/local-default/shadow\"", 141 }}, 142 "bar": { 143 Rule: &eval.Rule{ 144 ID: "bar", 145 Expression: "open.file.path == \"/etc/local-default/file\"", 146 }, 147 Definition: &RuleDefinition{ 148 ID: "bar", 149 Expression: "open.file.path == \"/etc/local-default/file\"", 150 }}, 151 } 152 153 var r DiffReporter 154 if !cmp.Equal(expectedRules, got.RuleSets[DefaultRuleSetTagValue].rules, cmp.Reporter(&r), cmpopts.IgnoreFields(Rule{}, "Opts", "Model"), cmpopts.IgnoreFields(RuleDefinition{}, "Policy"), cmpopts.IgnoreUnexported(eval.Rule{})) { 155 assert.Fail(t, fmt.Sprintf("Diff: %s)", r.String())) 156 } 157 158 return true 159 }, 160 wantErr: func(t assert.TestingT, err *multierror.Error, msgs ...interface{}) bool { 161 return assert.ErrorContains(t, err, "rule `foo` error: multiple definition with the same ID", fmt.Sprintf("Errors are: %+v", err.Errors)) 162 }, 163 }, 164 { 165 name: "disabling a default rule via a different file", 166 fields: fields{ 167 Providers: []PolicyProvider{ 168 dummyDirProvider{ 169 dummyLoadPoliciesFunc: func() ([]*Policy, *multierror.Error) { 170 return []*Policy{{ 171 Name: "default.policy", 172 Source: PolicyProviderTypeDir, 173 Version: "", 174 Rules: []*RuleDefinition{ 175 { 176 ID: "foo", 177 Expression: "open.file.path == \"/etc/local-default/shadow\"", 178 }, 179 { 180 ID: "bar", 181 Expression: "open.file.path == \"/etc/local-default/file\"", 182 }, 183 }, 184 Macros: nil, 185 }}, nil 186 }, 187 }, 188 dummyRCProvider{ 189 dummyLoadPoliciesFunc: func() ([]*Policy, *multierror.Error) { 190 return []*Policy{{ 191 Name: "myRC.policy", 192 Source: PolicyProviderTypeRC, 193 Rules: []*RuleDefinition{ 194 { 195 ID: "foo", 196 Disabled: true, 197 }, 198 { 199 ID: "bar", 200 Expression: "open.file.path == \"/etc/rc-custom/file\"", 201 }, 202 }, 203 }}, nil 204 }, 205 }, 206 }, 207 }, 208 want: func(t assert.TestingT, fields fields, got *EvaluationSet, msgs ...interface{}) bool { 209 gotNumberOfRules := len(got.RuleSets[DefaultRuleSetTagValue].rules) 210 assert.Equal(t, 1, gotNumberOfRules) 211 212 expectedRules := map[eval.RuleID]*Rule{ 213 "bar": { 214 Rule: &eval.Rule{ 215 ID: "bar", 216 Expression: "open.file.path == \"/etc/local-default/file\"", 217 }, 218 Definition: &RuleDefinition{ 219 ID: "bar", 220 Expression: "open.file.path == \"/etc/local-default/file\"", 221 }}, 222 } 223 224 var r DiffReporter 225 if !cmp.Equal(expectedRules, got.RuleSets[DefaultRuleSetTagValue].rules, cmp.Reporter(&r), cmpopts.IgnoreFields(Rule{}, "Opts", "Model"), cmpopts.IgnoreFields(RuleDefinition{}, "Policy"), cmpopts.IgnoreUnexported(eval.Rule{})) { 226 assert.Fail(t, fmt.Sprintf("Diff: %s)", r.String())) 227 } 228 229 return true 230 }, 231 wantErr: func(t assert.TestingT, err *multierror.Error, msgs ...interface{}) bool { 232 return assert.ErrorContains(t, err, "rule `bar` error: multiple definition with the same ID", fmt.Sprintf("Errors are: %+v", err.Errors)) 233 }, 234 }, 235 { 236 name: "disabling a default rule including ignored expression", 237 fields: fields{ 238 Providers: []PolicyProvider{ 239 dummyDirProvider{ 240 dummyLoadPoliciesFunc: func() ([]*Policy, *multierror.Error) { 241 return []*Policy{{ 242 Name: "default.policy", 243 Source: PolicyProviderTypeDir, 244 Version: "", 245 Rules: []*RuleDefinition{ 246 { 247 ID: "foo", 248 Expression: "open.file.path == \"/etc/local-default/shadow\"", 249 }, 250 { 251 ID: "bar", 252 Expression: "open.file.path == \"/etc/local-default/file\"", 253 }, 254 }, 255 Macros: nil, 256 }}, nil 257 }, 258 }, 259 dummyRCProvider{ 260 dummyLoadPoliciesFunc: func() ([]*Policy, *multierror.Error) { 261 return []*Policy{{ 262 Name: "myRC.policy", 263 Source: PolicyProviderTypeRC, 264 Rules: []*RuleDefinition{ 265 { 266 ID: "foo", 267 Expression: "open.file.path == \"/etc/rc-custom/shadow\"", 268 Disabled: true, 269 }, 270 { 271 ID: "bar", 272 Expression: "open.file.path == \"/etc/rc-custom/file\"", 273 }, 274 }, 275 }}, nil 276 }, 277 }, 278 }, 279 }, 280 want: func(t assert.TestingT, fields fields, got *EvaluationSet, msgs ...interface{}) bool { 281 assert.Equal(t, 1, len(got.RuleSets)) 282 if _, ok := got.RuleSets[DefaultRuleSetTagValue]; !ok { 283 t.Errorf("Missing %s rule set", DefaultRuleSetTagValue) 284 } 285 286 gotNumberOfRules := len(got.RuleSets[DefaultRuleSetTagValue].rules) 287 assert.Equal(t, 1, gotNumberOfRules) 288 289 expectedRules := map[eval.RuleID]*Rule{ 290 "bar": { 291 Rule: &eval.Rule{ 292 ID: "bar", 293 Expression: "open.file.path == \"/etc/local-default/file\"", 294 }, 295 Definition: &RuleDefinition{ 296 ID: "bar", 297 Expression: "open.file.path == \"/etc/local-default/file\"", 298 }}, 299 } 300 301 var r DiffReporter 302 // TODO: Use custom cmp.Comparer instead of ignoring unexported fields 303 if !cmp.Equal(expectedRules, got.RuleSets[DefaultRuleSetTagValue].rules, cmp.Reporter(&r), 304 cmpopts.IgnoreFields(Rule{}, "Opts", "Model"), cmpopts.IgnoreFields(RuleDefinition{}, "Policy"), 305 cmpopts.IgnoreFields(RuleSet{}, "opts", "evalOpts", "eventRuleBuckets", "fieldEvaluators", "model", 306 "eventCtor", "listenersLock", "listeners", "globalVariables", "scopedVariables", "fields", "logger", "pool"), 307 cmpopts.IgnoreUnexported(eval.Rule{})) { 308 assert.Fail(t, fmt.Sprintf("Diff: %s)", r.String())) 309 } 310 311 return true 312 }, 313 wantErr: func(t assert.TestingT, err *multierror.Error, msgs ...interface{}) bool { 314 return assert.ErrorContains(t, err, "rule `bar` error: multiple definition with the same ID", fmt.Sprintf("Errors are: %+v", err.Errors)) 315 }, 316 }, 317 { 318 name: "disabling a default rule and creating a custom rule with same ID", 319 fields: fields{ 320 Providers: []PolicyProvider{ 321 dummyDirProvider{ 322 dummyLoadPoliciesFunc: func() ([]*Policy, *multierror.Error) { 323 return []*Policy{{ 324 Name: "default.policy", 325 Source: PolicyProviderTypeDir, 326 Version: "", 327 Rules: []*RuleDefinition{ 328 { 329 ID: "foo", 330 Expression: "open.file.path == \"/etc/local-default/shadow\"", 331 }, 332 { 333 ID: "bar", 334 Expression: "open.file.path == \"/etc/local-default/file\"", 335 }, 336 }, 337 Macros: nil, 338 }}, nil 339 }, 340 }, 341 dummyRCProvider{ 342 dummyLoadPoliciesFunc: func() ([]*Policy, *multierror.Error) { 343 return []*Policy{{ 344 Name: "myRC.policy", 345 Source: PolicyProviderTypeRC, 346 Rules: []*RuleDefinition{ 347 { 348 ID: "foo", 349 Expression: "", 350 Disabled: true, 351 }, 352 { 353 ID: "bar", 354 Expression: "open.file.path == \"/etc/rc-custom/file\"", 355 }, 356 }, 357 }}, nil 358 }, 359 }, 360 }, 361 }, 362 want: func(t assert.TestingT, fields fields, got *EvaluationSet, msgs ...interface{}) bool { 363 assert.Equal(t, 1, len(got.RuleSets)) 364 if _, ok := got.RuleSets[DefaultRuleSetTagValue]; !ok { 365 t.Errorf("Missing %s rule set", DefaultRuleSetTagValue) 366 } 367 368 gotNumberOfRules := len(got.RuleSets[DefaultRuleSetTagValue].rules) 369 assert.Equal(t, 1, gotNumberOfRules) 370 371 expectedRules := map[eval.RuleID]*Rule{ 372 "bar": { 373 Rule: &eval.Rule{ 374 ID: "bar", 375 Expression: "open.file.path == \"/etc/local-default/file\"", 376 }, 377 Definition: &RuleDefinition{ 378 ID: "bar", 379 Expression: "open.file.path == \"/etc/local-default/file\"", 380 }}, 381 } 382 383 var r DiffReporter 384 if !cmp.Equal(expectedRules, got.RuleSets[DefaultRuleSetTagValue].rules, cmp.Reporter(&r), 385 cmpopts.IgnoreFields(Rule{}, "Opts", "Model"), cmpopts.IgnoreFields(RuleDefinition{}, "Policy"), 386 cmpopts.IgnoreFields(RuleSet{}, "opts", "evalOpts", "eventRuleBuckets", "fieldEvaluators", "model", 387 "eventCtor", "listenersLock", "listeners", "globalVariables", "scopedVariables", "fields", "logger", "pool"), 388 cmpopts.IgnoreUnexported(eval.Rule{})) { 389 assert.Fail(t, fmt.Sprintf("Diff: %s)", r.String())) 390 } 391 392 return true 393 }, 394 wantErr: func(t assert.TestingT, err *multierror.Error, msgs ...interface{}) bool { 395 assert.Equal(t, 1, err.Len(), fmt.Sprintf("Errors are: %s", err.Errors)) 396 return assert.ErrorContains(t, err, "rule `bar` error: multiple definition with the same ID") 397 }, 398 }, 399 { 400 name: "combine:override", 401 fields: fields{ 402 Providers: []PolicyProvider{ 403 dummyDirProvider{ 404 dummyLoadPoliciesFunc: func() ([]*Policy, *multierror.Error) { 405 return []*Policy{{ 406 Name: "default.policy", 407 Source: PolicyProviderTypeDir, 408 Version: "", 409 Rules: []*RuleDefinition{ 410 { 411 ID: "foo", 412 Expression: "open.file.path == \"/etc/local-default/shadow\"", 413 }, 414 { 415 ID: "bar", 416 Expression: "open.file.path == \"/etc/local-default/file\"", 417 }, 418 { 419 ID: "foobar", 420 Expression: "open.file.path == \"/etc/local-default/foobar\"", 421 }, 422 { 423 ID: "foobar2", 424 Expression: "open.file.path == \"/etc/local-default/foobar2\"", 425 }, 426 }, 427 Macros: nil, 428 }}, nil 429 }, 430 }, 431 dummyRCProvider{ 432 dummyLoadPoliciesFunc: func() ([]*Policy, *multierror.Error) { 433 return []*Policy{{ 434 Name: "myRC.policy", 435 Source: PolicyProviderTypeRC, 436 Rules: []*RuleDefinition{ 437 { 438 ID: "foo", 439 Expression: "open.file.path == \"/etc/rc-custom/shadow\"", 440 Combine: OverridePolicy, 441 Tags: map[string]string{ 442 "tag1": "test1", 443 }, 444 Actions: []*ActionDefinition{ 445 { 446 Kill: &KillDefinition{ 447 Signal: "SIGKILL", 448 }, 449 }, 450 }, 451 }, 452 { 453 ID: "bar", 454 Expression: "open.file.path == \"/etc/rc-custom/file\"", 455 }, 456 { 457 ID: "foobar", 458 Expression: "open.file.path == \"/etc/local-custom/foobar\"", 459 Combine: OverridePolicy, 460 OverrideOptions: OverrideOptions{ 461 Fields: []OverrideField{ 462 "actions", 463 "tags", 464 }, 465 }, 466 Tags: map[string]string{ 467 "tag1": "test2", 468 }, 469 Actions: []*ActionDefinition{ 470 { 471 Kill: &KillDefinition{ 472 Signal: "SIGKILL", 473 }, 474 }, 475 }, 476 }, 477 { 478 ID: "foobar2", 479 Expression: "open.file.path == \"/etc/local-custom/foobar2\"", 480 Combine: OverridePolicy, 481 OverrideOptions: OverrideOptions{ 482 Fields: []OverrideField{ 483 "expression", 484 }, 485 }, 486 Tags: map[string]string{ 487 "tag1": "test2", 488 }, 489 Actions: []*ActionDefinition{ 490 { 491 Kill: &KillDefinition{ 492 Signal: "SIGKILL", 493 }, 494 }, 495 }, 496 }, 497 }, 498 }}, nil 499 }, 500 }, 501 }, 502 }, 503 want: func(t assert.TestingT, fields fields, got *EvaluationSet, msgs ...interface{}) bool { 504 assert.Equal(t, 1, len(got.RuleSets)) 505 if _, ok := got.RuleSets[DefaultRuleSetTagValue]; !ok { 506 t.Errorf("Missing %s rule set", DefaultRuleSetTagValue) 507 } 508 509 assert.Equal(t, 4, len(got.RuleSets[DefaultRuleSetTagValue].rules)) 510 511 expectedRules := map[eval.RuleID]*Rule{ 512 "foo": { 513 Rule: &eval.Rule{ 514 ID: "foo", 515 Expression: "open.file.path == \"/etc/rc-custom/shadow\"", 516 }, 517 Definition: &RuleDefinition{ 518 ID: "foo", 519 Expression: "open.file.path == \"/etc/rc-custom/shadow\"", 520 Combine: OverridePolicy, 521 }}, 522 "bar": { 523 Rule: &eval.Rule{ 524 ID: "bar", 525 Expression: "open.file.path == \"/etc/local-default/file\"", 526 }, 527 Definition: &RuleDefinition{ 528 ID: "bar", 529 Expression: "open.file.path == \"/etc/local-default/file\"", 530 }, 531 }, 532 "foobar": { 533 Rule: &eval.Rule{ 534 ID: "foobar", 535 Expression: "open.file.path == \"/etc/local-default/foobar\"", 536 Tags: []string{"tag1:test2"}, 537 }, 538 Definition: &RuleDefinition{ 539 ID: "foobar", 540 Expression: "open.file.path == \"/etc/local-default/foobar\"", 541 Combine: OverridePolicy, 542 Tags: map[string]string{ 543 "tag1": "test2", 544 }, 545 Actions: []*ActionDefinition{ 546 { 547 Kill: &KillDefinition{ 548 Signal: "SIGKILL", 549 }, 550 }, 551 }, 552 }}, 553 "foobar2": { 554 Rule: &eval.Rule{ 555 ID: "foobar2", 556 Expression: "open.file.path == \"/etc/local-custom/foobar2\"", 557 }, 558 Definition: &RuleDefinition{ 559 ID: "foobar2", 560 Expression: "open.file.path == \"/etc/local-custom/foobar2\"", 561 Combine: OverridePolicy, 562 }, 563 }, 564 } 565 566 var r DiffReporter 567 // TODO: Use custom cmp.Comparer instead of ignoring unexported fields 568 if !cmp.Equal(expectedRules, got.RuleSets[DefaultRuleSetTagValue].rules, cmp.Reporter(&r), 569 cmpopts.IgnoreFields(Rule{}, "Opts", "Model"), cmpopts.IgnoreFields(RuleDefinition{}, "Policy"), 570 cmpopts.IgnoreFields(RuleSet{}, "opts", "evalOpts", "eventRuleBuckets", "fieldEvaluators", "model", 571 "eventCtor", "listenersLock", "listeners", "globalVariables", "scopedVariables", "fields", "logger", "pool"), 572 cmpopts.IgnoreUnexported(eval.Rule{})) { 573 assert.Fail(t, fmt.Sprintf("Diff: %s)", r.String())) 574 } 575 576 return true 577 }, 578 wantErr: func(t assert.TestingT, err *multierror.Error, msgs ...interface{}) bool { 579 assert.Equal(t, 1, err.Len(), fmt.Sprintf("Errors are: %s", err.Errors)) 580 return assert.ErrorContains(t, err, "rule `bar` error: multiple definition with the same ID", fmt.Sprintf("Errors are: %+v", err.Errors)) 581 }, 582 }, 583 } 584 585 for _, tt := range tests { 586 t.Run(tt.name, func(t *testing.T) { 587 588 p := &PolicyLoader{ 589 Providers: tt.fields.Providers, 590 } 591 592 policyLoaderOpts := PolicyLoaderOpts{} 593 es, _ := newTestEvaluationSet(tt.fields.TagValues) 594 595 err := es.LoadPolicies(p, policyLoaderOpts) 596 597 tt.want(t, tt.fields, es) 598 tt.wantErr(t, err) 599 }) 600 } 601 } 602 603 func TestEvaluationSet_LoadPolicies_PolicyPrecedence(t *testing.T) { 604 type fields struct { 605 Providers []PolicyProvider 606 TagValues []eval.RuleSetTagValue 607 } 608 tests := []struct { 609 name string 610 fields fields 611 want func(t assert.TestingT, fields fields, got *EvaluationSet, msgs ...interface{}) bool 612 wantErr func(t assert.TestingT, err *multierror.Error, msgs ...interface{}) bool 613 }{ 614 { 615 name: "RC Default replaces Local Default and overrides all else, and RC Custom overrides Local Custom", 616 fields: fields{ 617 Providers: []PolicyProvider{ 618 dummyDirProvider{ 619 dummyLoadPoliciesFunc: func() ([]*Policy, *multierror.Error) { 620 return []*Policy{{ 621 Name: "myLocal.policy", 622 Source: PolicyProviderTypeDir, 623 Rules: []*RuleDefinition{ 624 { 625 ID: "foo", 626 Expression: "open.file.path == \"/etc/local-custom/foo\"", 627 }, 628 { 629 ID: "bar", 630 Expression: "open.file.path == \"/etc/local-custom/bar\"", 631 }, 632 { 633 ID: "baz", 634 Expression: "open.file.path == \"/etc/local-custom/baz\"", 635 }, 636 { 637 ID: "alpha", 638 Expression: "open.file.path == \"/etc/local-custom/alpha\"", 639 }, 640 }, 641 }, { 642 Name: DefaultPolicyName, 643 Source: PolicyProviderTypeDir, 644 Rules: []*RuleDefinition{ 645 { 646 ID: "foo", 647 Expression: "open.file.path == \"/etc/local-default/foo\"", 648 }, 649 { 650 ID: "bar", 651 Expression: "open.file.path == \"/etc/local-default/bar\"", 652 }, 653 { 654 ID: "baz", 655 Expression: "open.file.path == \"/etc/local-default/baz\"", 656 }, 657 { 658 ID: "alpha", 659 Expression: "open.file.path == \"/etc/local-default/alpha\"", 660 }, 661 }, 662 }}, nil 663 }, 664 }, 665 dummyRCProvider{ 666 dummyLoadPoliciesFunc: func() ([]*Policy, *multierror.Error) { 667 return []*Policy{{ 668 Name: "myRC.policy", 669 Source: PolicyProviderTypeRC, 670 Rules: []*RuleDefinition{ 671 { 672 ID: "foo", 673 Expression: "open.file.path == \"/etc/rc-custom/foo\"", 674 }, 675 { 676 ID: "bar", 677 Expression: "open.file.path == \"/etc/rc-custom/bar\"", 678 }, 679 { 680 ID: "baz", 681 Expression: "open.file.path == \"/etc/rc-custom/baz\"", 682 }, 683 }, 684 }, { 685 Name: DefaultPolicyName, 686 Source: PolicyProviderTypeRC, 687 Rules: []*RuleDefinition{ 688 { 689 ID: "foo", 690 Expression: "open.file.path == \"/etc/rc-default/foo\"", 691 }, 692 }, 693 }}, nil 694 }, 695 }, 696 }, 697 }, 698 want: func(t assert.TestingT, fields fields, got *EvaluationSet, msgs ...interface{}) bool { 699 gotNumberOfRules := len(got.RuleSets[DefaultRuleSetTagValue].rules) 700 assert.Equal(t, 4, gotNumberOfRules) 701 702 expectedRules := map[eval.RuleID]*Rule{ 703 "foo": { 704 Rule: &eval.Rule{ 705 ID: "foo", 706 Expression: "open.file.path == \"/etc/rc-default/foo\"", 707 }, 708 Definition: &RuleDefinition{ 709 ID: "foo", 710 Expression: "open.file.path == \"/etc/rc-default/foo\"", 711 }}, 712 "bar": { 713 Rule: &eval.Rule{ 714 ID: "bar", 715 Expression: "open.file.path == \"/etc/rc-custom/bar\"", 716 }, 717 Definition: &RuleDefinition{ 718 ID: "bar", 719 Expression: "open.file.path == \"/etc/rc-custom/bar\"", 720 }}, 721 "baz": { 722 Rule: &eval.Rule{ 723 ID: "baz", 724 Expression: "open.file.path == \"/etc/rc-custom/baz\"", 725 }, 726 Definition: &RuleDefinition{ 727 ID: "baz", 728 Expression: "open.file.path == \"/etc/rc-custom/baz\"", 729 }}, 730 "alpha": { 731 Rule: &eval.Rule{ 732 ID: "alpha", 733 Expression: "open.file.path == \"/etc/local-custom/alpha\"", 734 }, 735 Definition: &RuleDefinition{ 736 ID: "alpha", 737 Expression: "open.file.path == \"/etc/local-custom/alpha\"", 738 }}, 739 } 740 741 var r DiffReporter 742 743 // TODO: Use custom cmp.Comparer instead of ignoring unexported fields 744 if !cmp.Equal(expectedRules, got.RuleSets[DefaultRuleSetTagValue].rules, cmp.Reporter(&r), 745 cmpopts.IgnoreFields(Rule{}, "Opts", "Model"), cmpopts.IgnoreFields(RuleDefinition{}, "Policy"), 746 cmpopts.IgnoreFields(RuleSet{}, "opts", "evalOpts", "eventRuleBuckets", "fieldEvaluators", "model", 747 "eventCtor", "listenersLock", "listeners", "globalVariables", "scopedVariables", "fields", "logger", "pool"), 748 cmpopts.IgnoreUnexported(eval.Rule{})) { 749 assert.Fail(t, fmt.Sprintf("Diff: %s)", r.String())) 750 } 751 752 return true 753 }, 754 wantErr: func(t assert.TestingT, err *multierror.Error, msgs ...interface{}) bool { 755 assert.Equal(t, err.Len(), 4, "Expected %d errors, got %d: %+v", 1, err.Len(), err) 756 assert.ErrorContains(t, err, "rule `foo` error: multiple definition with the same ID", fmt.Sprintf("Errors are: %+v", err.Errors)) 757 assert.ErrorContains(t, err, "rule `bar` error: multiple definition with the same ID", fmt.Sprintf("Errors are: %+v", err.Errors)) 758 return assert.ErrorContains(t, err, "rule `baz` error: multiple definition with the same ID", fmt.Sprintf("Errors are: %+v", err.Errors)) 759 }, 760 }, 761 } 762 763 for _, tt := range tests { 764 t.Run(tt.name, func(t *testing.T) { 765 p := &PolicyLoader{ 766 Providers: tt.fields.Providers, 767 } 768 769 policyLoaderOpts := PolicyLoaderOpts{} 770 es, _ := newTestEvaluationSet(tt.fields.TagValues) 771 772 err := es.LoadPolicies(p, policyLoaderOpts) 773 774 tt.want(t, tt.fields, es) 775 tt.wantErr(t, err) 776 }) 777 } 778 } 779 780 func TestEvaluationSet_LoadPolicies_RuleSetTags(t *testing.T) { 781 type args struct { 782 policy *PolicyDef 783 tagValues []eval.RuleSetTagValue 784 } 785 tests := []struct { 786 name string 787 args args 788 want func(t assert.TestingT, args args, got *EvaluationSet, msgs ...interface{}) bool 789 wantErr func(t assert.TestingT, err *multierror.Error, msgs ...interface{}) bool 790 }{ 791 { 792 name: "just threat score", 793 args: args{ 794 policy: &PolicyDef{ 795 Rules: []*RuleDefinition{ 796 { 797 ID: "testA", 798 Expression: `open.file.path == "/tmp/test"`, 799 }, 800 { 801 ID: "testB", 802 Expression: `open.file.path == "/tmp/test"`, 803 Tags: map[string]string{"ruleset": "threat_score"}, 804 }, 805 { 806 ID: "testC", 807 Expression: `open.file.path == "/tmp/toto"`, 808 }, 809 }, 810 }, 811 tagValues: []eval.RuleSetTagValue{"threat_score"}, 812 }, 813 want: func(t assert.TestingT, args args, got *EvaluationSet, msgs ...interface{}) bool { 814 gotNumOfRules := len(got.RuleSets["threat_score"].rules) 815 expected := 1 816 assert.Equal(t, expected, gotNumOfRules) 817 818 return assert.Equal(t, 1, len(got.RuleSets)) 819 }, 820 wantErr: func(t assert.TestingT, err *multierror.Error, msgs ...interface{}) bool { 821 return assert.Nil(t, err, msgs) 822 }, 823 }, 824 { 825 name: "just probe evaluation", 826 args: args{ 827 policy: &PolicyDef{ 828 Rules: []*RuleDefinition{ 829 { 830 ID: "testA", 831 Expression: `open.file.path == "/tmp/test"`, 832 }, 833 { 834 ID: "testB", 835 Expression: `open.file.path == "/tmp/test"`, 836 Tags: map[string]string{"ruleset": DefaultRuleSetTagValue}, 837 }, 838 { 839 ID: "testC", 840 Expression: `open.file.path == "/tmp/toto"`, 841 }, 842 }, 843 }, 844 tagValues: []eval.RuleSetTagValue{DefaultRuleSetTagValue}, 845 }, 846 want: func(t assert.TestingT, args args, got *EvaluationSet, msgs ...interface{}) bool { 847 gotNumberOfRules := len(got.RuleSets[DefaultRuleSetTagValue].rules) 848 expected := 3 849 assert.Equal(t, expected, gotNumberOfRules) 850 851 return assert.Equal(t, 1, len(got.RuleSets)) 852 }, 853 wantErr: func(t assert.TestingT, err *multierror.Error, msgs ...interface{}) bool { 854 return assert.Nil(t, err, msgs) 855 }, 856 }, 857 { 858 name: "mix of tags", 859 args: args{ 860 policy: &PolicyDef{ 861 Rules: []*RuleDefinition{ 862 { 863 ID: "testA", 864 Expression: `open.file.path == "/tmp/test"`, 865 }, 866 { 867 ID: "testB", 868 Expression: `open.file.path == "/tmp/test"`, 869 Tags: map[string]string{"ruleset": "threat_score"}, 870 }, 871 { 872 ID: "testC", 873 Expression: `open.file.path == "/tmp/toto"`, 874 Tags: map[string]string{"ruleset": "special"}, 875 }, 876 { 877 ID: "testD", 878 Expression: `open.file.path == "/tmp/toto"`, 879 Tags: map[string]string{"threat_score": "4", "ruleset": "special"}, 880 }, 881 }, 882 }, 883 tagValues: []eval.RuleSetTagValue{DefaultRuleSetTagValue, "threat_score", "special"}, 884 }, 885 want: func(t assert.TestingT, args args, got *EvaluationSet, msgs ...interface{}) bool { 886 assert.Equal(t, len(args.tagValues), len(got.RuleSets)) 887 888 gotNumProbeEvalRules := len(got.RuleSets[DefaultRuleSetTagValue].rules) 889 expected := 1 890 assert.Equal(t, expected, gotNumProbeEvalRules) 891 892 gotNumThreatScoreRules := len(got.RuleSets["threat_score"].rules) 893 expectedNumThreatScoreRules := 1 894 assert.Equal(t, expectedNumThreatScoreRules, gotNumThreatScoreRules) 895 896 gotNumSpecialRules := len(got.RuleSets["special"].rules) 897 expectedNumSpecialRules := 2 898 assert.Equal(t, expectedNumSpecialRules, gotNumSpecialRules) 899 900 return assert.Equal(t, 3, len(got.RuleSets)) 901 }, 902 wantErr: func(t assert.TestingT, err *multierror.Error, msgs ...interface{}) bool { 903 return assert.Nil(t, err, msgs) 904 }, 905 }, 906 } 907 908 for _, tt := range tests { 909 t.Run(tt.name, func(t *testing.T) { 910 policyLoaderOpts := PolicyLoaderOpts{} 911 loader, es := loadPolicySetup(t, tt.args.policy, tt.args.tagValues) 912 913 err := es.LoadPolicies(loader, policyLoaderOpts) 914 tt.want(t, tt.args, es) 915 tt.wantErr(t, err) 916 }) 917 } 918 } 919 920 func TestEvaluationSet_LoadPolicies_DisableEnforcement(t *testing.T) { 921 type args struct { 922 policy *PolicyDef 923 tagValues []eval.RuleSetTagValue 924 } 925 tests := []struct { 926 name string 927 disableEnforcement bool 928 args args 929 want func(t assert.TestingT, args args, got *EvaluationSet, msgs ...interface{}) 930 wantErr func(t assert.TestingT, err *multierror.Error, msgs ...interface{}) 931 }{ 932 { 933 name: "enforcing policy", 934 args: args{ 935 policy: &PolicyDef{ 936 Rules: []*RuleDefinition{ 937 { 938 ID: "ruleA", 939 Expression: `exec.file.path == "/tmp/test"`, 940 Actions: []*ActionDefinition{ 941 { 942 Kill: &KillDefinition{ 943 Signal: "SIGKILL", 944 }, 945 }, { 946 Set: &SetDefinition{ 947 Name: "var1", 948 Value: "foo", 949 }, 950 }, 951 }, 952 }, 953 }, 954 }, 955 }, 956 want: func(t assert.TestingT, args args, got *EvaluationSet, msgs ...interface{}) { 957 assert.Equal(t, 1, len(got.RuleSets)) 958 959 gotNumProbeEvalRules := len(got.RuleSets[DefaultRuleSetTagValue].rules) 960 assert.Equal(t, 1, gotNumProbeEvalRules) 961 962 rule := got.RuleSets[DefaultRuleSetTagValue].rules["ruleA"] 963 assert.NotNil(t, rule) 964 965 assert.Equal(t, 2, len(rule.Definition.Actions)) 966 assert.NotNil(t, rule.Definition.Actions[0].Kill) 967 assert.Equal(t, "SIGKILL", rule.Definition.Actions[0].Kill.Signal) 968 }, 969 wantErr: func(t assert.TestingT, err *multierror.Error, msgs ...interface{}) { 970 assert.Nil(t, err, msgs) 971 }, 972 }, 973 { 974 name: "enforcing policy with enforcement disabled", 975 disableEnforcement: true, 976 args: args{ 977 policy: &PolicyDef{ 978 Rules: []*RuleDefinition{ 979 { 980 ID: "ruleA", 981 Expression: `exec.file.path == "/tmp/test"`, 982 Actions: []*ActionDefinition{ 983 { 984 Kill: &KillDefinition{ 985 Signal: "SIGKILL", 986 }, 987 }, 988 }, 989 }, 990 }, 991 }, 992 }, 993 want: func(t assert.TestingT, args args, got *EvaluationSet, msgs ...interface{}) { 994 assert.Equal(t, 1, len(got.RuleSets)) 995 996 gotNumProbeEvalRules := len(got.RuleSets[DefaultRuleSetTagValue].rules) 997 assert.Equal(t, 1, gotNumProbeEvalRules) 998 999 rule := got.RuleSets[DefaultRuleSetTagValue].rules["ruleA"] 1000 assert.NotNil(t, rule) 1001 1002 assert.Equal(t, 1, len(rule.Definition.Actions)) 1003 assert.Nil(t, rule.Definition.Actions[0].Kill) 1004 }, 1005 wantErr: func(t assert.TestingT, err *multierror.Error, msgs ...interface{}) { 1006 assert.ErrorContains(t, err, "action is disabled") 1007 }, 1008 }, 1009 } 1010 1011 for _, tt := range tests { 1012 t.Run(tt.name, func(t *testing.T) { 1013 policyLoaderOpts := PolicyLoaderOpts{DisableEnforcement: tt.disableEnforcement} 1014 loader, es := loadPolicySetup(t, tt.args.policy, tt.args.tagValues) 1015 1016 err := es.LoadPolicies(loader, policyLoaderOpts) 1017 tt.want(t, tt.args, es) 1018 tt.wantErr(t, err) 1019 }) 1020 } 1021 } 1022 1023 func TestNewEvaluationSet(t *testing.T) { 1024 ruleSet := newRuleSet() 1025 ruleSetWithThreatScoreTag := newRuleSet() 1026 ruleSetWithThreatScoreTag.setRuleSetTagValue("threat_score") 1027 1028 type args struct { 1029 ruleSetsToInclude []*RuleSet 1030 } 1031 tests := []struct { 1032 name string 1033 args args 1034 want func(t assert.TestingT, got *EvaluationSet, msgs ...interface{}) bool 1035 wantErr assert.ErrorAssertionFunc 1036 }{ 1037 { 1038 name: "no rule sets", 1039 args: args{ruleSetsToInclude: []*RuleSet{}}, 1040 want: func(t assert.TestingT, got *EvaluationSet, msgs ...interface{}) bool { 1041 return assert.Nil(t, got) 1042 }, 1043 wantErr: func(t assert.TestingT, err error, msgs ...interface{}) bool { 1044 return assert.ErrorIs(t, err, ErrNoRuleSetsInEvaluationSet, msgs) 1045 }, 1046 }, 1047 { 1048 name: "just probe evaluation ruleset", 1049 args: args{[]*RuleSet{ruleSet}}, 1050 want: func(t assert.TestingT, got *EvaluationSet, msgs ...interface{}) bool { 1051 expected := &EvaluationSet{RuleSets: map[eval.RuleSetTagValue]*RuleSet{"probe_evaluation": ruleSet}} 1052 return assert.Equal(t, expected, got, msgs) 1053 }, 1054 wantErr: func(t assert.TestingT, err error, msgs ...interface{}) bool { 1055 return assert.ErrorIs(t, err, nil, msgs) 1056 }, 1057 }, 1058 { 1059 name: "just non-probe evaluation ruleset", 1060 args: args{ruleSetsToInclude: []*RuleSet{ruleSetWithThreatScoreTag}}, 1061 want: func(t assert.TestingT, got *EvaluationSet, msgs ...interface{}) bool { 1062 expected := &EvaluationSet{RuleSets: map[eval.RuleSetTagValue]*RuleSet{"threat_score": ruleSetWithThreatScoreTag}} 1063 return assert.Equal(t, expected, got, msgs) 1064 }, 1065 wantErr: func(t assert.TestingT, err error, msgs ...interface{}) bool { 1066 return assert.ErrorIs(t, err, nil, msgs) 1067 }, 1068 }, 1069 { 1070 name: "multiple rule sets", 1071 args: args{ruleSetsToInclude: []*RuleSet{ruleSetWithThreatScoreTag, ruleSet}}, 1072 want: func(t assert.TestingT, got *EvaluationSet, msgs ...interface{}) bool { 1073 expected := &EvaluationSet{RuleSets: map[eval.RuleSetTagValue]*RuleSet{"threat_score": ruleSetWithThreatScoreTag, "probe_evaluation": ruleSet}} 1074 return assert.Equal(t, expected, got, msgs) 1075 }, 1076 wantErr: func(t assert.TestingT, err error, msgs ...interface{}) bool { 1077 return assert.ErrorIs(t, err, nil, msgs) 1078 }, 1079 }, 1080 } 1081 for _, tt := range tests { 1082 t.Run(tt.name, func(t *testing.T) { 1083 got, err := NewEvaluationSet(tt.args.ruleSetsToInclude) 1084 tt.wantErr(t, err, fmt.Sprintf("NewEvaluationSet(%v)", tt.args.ruleSetsToInclude)) 1085 tt.want(t, got, fmt.Sprintf("NewEvaluationSet(%v)", tt.args.ruleSetsToInclude)) 1086 }) 1087 } 1088 } 1089 1090 // Test Utilities 1091 func newTestEvaluationSet(tagValues []eval.RuleSetTagValue) (*EvaluationSet, error) { 1092 var ruleSetsToInclude []*RuleSet 1093 if len(tagValues) > 0 { 1094 for _, tagValue := range tagValues { 1095 rs := newRuleSet() 1096 rs.setRuleSetTagValue(tagValue) 1097 ruleSetsToInclude = append(ruleSetsToInclude, rs) 1098 } 1099 } else { 1100 rs := newRuleSet() 1101 ruleSetsToInclude = append(ruleSetsToInclude, rs) 1102 } 1103 1104 return NewEvaluationSet(ruleSetsToInclude) 1105 } 1106 1107 func loadPolicyIntoProbeEvaluationRuleSet(t *testing.T, testPolicy *PolicyDef, policyOpts PolicyLoaderOpts) (*EvaluationSet, *multierror.Error) { 1108 tmpDir := t.TempDir() 1109 1110 if err := savePolicy(filepath.Join(tmpDir, "test.policy"), testPolicy); err != nil { 1111 t.Fatal(err) 1112 } 1113 1114 provider, err := NewPoliciesDirProvider(tmpDir, false) 1115 if err != nil { 1116 t.Fatal(err) 1117 } 1118 1119 loader := NewPolicyLoader(provider) 1120 1121 evaluationSet, _ := newTestEvaluationSet([]eval.RuleSetTagValue{}) 1122 return evaluationSet, evaluationSet.LoadPolicies(loader, policyOpts) 1123 } 1124 1125 func loadPolicySetup(t *testing.T, testPolicy *PolicyDef, tagValues []eval.RuleSetTagValue) (*PolicyLoader, *EvaluationSet) { 1126 tmpDir := t.TempDir() 1127 1128 if err := savePolicy(filepath.Join(tmpDir, "test.policy"), testPolicy); err != nil { 1129 t.Fatal(err) 1130 } 1131 1132 provider, err := NewPoliciesDirProvider(tmpDir, false) 1133 if err != nil { 1134 t.Fatal(err) 1135 } 1136 1137 loader := NewPolicyLoader(provider) 1138 1139 evaluationSet, _ := newTestEvaluationSet(tagValues) 1140 return loader, evaluationSet 1141 } 1142 1143 // The following is from https://pkg.go.dev/github.com/google/go-cmp@v0.5.9/cmp#example-Reporter 1144 // DiffReporter is a simple custom reporter that only records differences 1145 // detected during comparison. 1146 type DiffReporter struct { 1147 path cmp.Path 1148 diffs []string 1149 } 1150 1151 func (r *DiffReporter) PushStep(ps cmp.PathStep) { 1152 r.path = append(r.path, ps) 1153 } 1154 1155 func (r *DiffReporter) Report(rs cmp.Result) { 1156 if !rs.Equal() { 1157 vx, vy := r.path.Last().Values() 1158 r.diffs = append(r.diffs, fmt.Sprintf("%#v:\n\t-: %+v\n\t+: %+v\n", r.path, vx, vy)) 1159 } 1160 } 1161 1162 func (r *DiffReporter) PopStep() { 1163 r.path = r.path[:len(r.path)-1] 1164 } 1165 1166 func (r *DiffReporter) String() string { 1167 return strings.Join(r.diffs, "\n") 1168 }