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  }