github.com/crowdsecurity/crowdsec@v1.6.1/pkg/setup/detect_test.go (about)

     1  package setup_test
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"os/exec"
     7  	"runtime"
     8  	"testing"
     9  
    10  	"github.com/lithammer/dedent"
    11  	"github.com/stretchr/testify/require"
    12  
    13  	"github.com/crowdsecurity/go-cs-lib/cstest"
    14  
    15  	"github.com/crowdsecurity/crowdsec/pkg/setup"
    16  )
    17  
    18  //nolint:dupword
    19  var fakeSystemctlOutput = `UNIT FILE                                 STATE    VENDOR PRESET
    20  crowdsec-setup-detect.service            enabled  enabled
    21  apache2.service                           enabled  enabled
    22  apparmor.service                          enabled  enabled
    23  apport.service                            enabled  enabled
    24  atop.service                              enabled  enabled
    25  atopacct.service                          enabled  enabled
    26  finalrd.service                           enabled  enabled
    27  fwupd-refresh.service                     enabled  enabled
    28  fwupd.service                             enabled  enabled
    29  
    30  9 unit files listed.`
    31  
    32  func fakeExecCommandNotFound(command string, args ...string) *exec.Cmd {
    33  	cs := []string{"-test.run=TestSetupHelperProcess", "--", command}
    34  	cs = append(cs, args...)
    35  	cmd := exec.Command("this-command-does-not-exist", cs...)
    36  	cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
    37  
    38  	return cmd
    39  }
    40  
    41  func fakeExecCommand(command string, args ...string) *exec.Cmd {
    42  	cs := []string{"-test.run=TestSetupHelperProcess", "--", command}
    43  	cs = append(cs, args...)
    44  	//nolint:gosec
    45  	cmd := exec.Command(os.Args[0], cs...)
    46  	cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
    47  
    48  	return cmd
    49  }
    50  
    51  func TestSetupHelperProcess(t *testing.T) {
    52  	if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
    53  		return
    54  	}
    55  
    56  	fmt.Fprint(os.Stdout, fakeSystemctlOutput)
    57  	os.Exit(0)
    58  }
    59  
    60  func tempYAML(t *testing.T, content string) os.File {
    61  	t.Helper()
    62  	require := require.New(t)
    63  	file, err := os.CreateTemp("", "")
    64  	require.NoError(err)
    65  
    66  	_, err = file.WriteString(dedent.Dedent(content))
    67  	require.NoError(err)
    68  
    69  	err = file.Close()
    70  	require.NoError(err)
    71  
    72  	file, err = os.Open(file.Name())
    73  	require.NoError(err)
    74  
    75  	return *file
    76  }
    77  
    78  func TestPathExists(t *testing.T) {
    79  	t.Parallel()
    80  
    81  	type test struct {
    82  		path     string
    83  		expected bool
    84  	}
    85  
    86  	tests := []test{
    87  		{"/this-should-not-exist", false},
    88  	}
    89  
    90  	if runtime.GOOS == "windows" {
    91  		tests = append(tests, test{`C:\`, true})
    92  	} else {
    93  		tests = append(tests, test{"/tmp", true})
    94  	}
    95  
    96  	for _, tc := range tests {
    97  		tc := tc
    98  		env := setup.NewExprEnvironment(setup.DetectOptions{}, setup.ExprOS{})
    99  
   100  		t.Run(tc.path, func(t *testing.T) {
   101  			t.Parallel()
   102  			actual := env.PathExists(tc.path)
   103  			require.Equal(t, tc.expected, actual)
   104  		})
   105  	}
   106  }
   107  
   108  func TestVersionCheck(t *testing.T) {
   109  	t.Parallel()
   110  
   111  	tests := []struct {
   112  		version     string
   113  		constraint  string
   114  		expected    bool
   115  		expectedErr string
   116  	}{
   117  		{"1", "=1", true, ""},
   118  		{"1", "!=1", false, ""},
   119  		{"1", "<=1", true, ""},
   120  		{"1", ">1", false, ""},
   121  		{"1", ">=1", true, ""},
   122  		{"1.0", "<1.0", false, ""},
   123  		{"1", "<1", false, ""},
   124  		{"1.3.5", "1.3", true, ""},
   125  		{"1.0", "<1.0", false, ""},
   126  		{"1.0", "<=1.0", true, ""},
   127  		{"2", ">1, <3", true, ""},
   128  		{"2", "<=2, >=2.2", false, ""},
   129  		{"2.3", "~2", true, ""},
   130  		{"2.3", "=2", true, ""},
   131  		{"1.1.1", "=1.1", true, ""},
   132  		{"1.1.1", "1.1", true, ""},
   133  		{"1.1", "!=1.1.1", true, ""},
   134  		{"1.1", "~1.1.1", false, ""},
   135  		{"1.1.1", "~1.1", true, ""},
   136  		{"1.1.3", "~1.1", true, ""},
   137  		{"19.04", "<19.10", true, ""},
   138  		{"19.04", ">=19.10", false, ""},
   139  		{"19.04", "=19.4", true, ""},
   140  		{"19.04", "~19.4", true, ""},
   141  		{"1.2.3", "~1.2", true, ""},
   142  		{"1.2.3", "!=1.2", false, ""},
   143  		{"1.2.3", "1.1.1 - 1.3.4", true, ""},
   144  		{"1.3.5", "1.1.1 - 1.3.4", false, ""},
   145  		{"1.3.5", "=1", true, ""},
   146  		{"1.3.5", "1", true, ""},
   147  	}
   148  
   149  	for _, tc := range tests {
   150  		tc := tc
   151  		e := setup.ExprOS{RawVersion: tc.version}
   152  
   153  		t.Run(fmt.Sprintf("Check(%s,%s)", tc.version, tc.constraint), func(t *testing.T) {
   154  			t.Parallel()
   155  			actual, err := e.VersionCheck(tc.constraint)
   156  			cstest.RequireErrorContains(t, err, tc.expectedErr)
   157  			require.Equal(t, tc.expected, actual)
   158  		})
   159  	}
   160  }
   161  
   162  // This is not required for Masterminds/semver
   163  /*
   164  func TestNormalizeVersion(t *testing.T) {
   165  	t.Parallel()
   166  
   167  	tests := []struct {
   168  		version  string
   169  		expected string
   170  	}{
   171  		{"0", "0"},
   172  		{"2", "2"},
   173  		{"3.14", "3.14"},
   174  		{"1.0", "1.0"},
   175  		{"18.04", "18.4"},
   176  		{"0.0.0", "0.0.0"},
   177  		{"18.04.0", "18.4.0"},
   178  		{"18.0004.0", "18.4.0"},
   179  		{"21.04.2", "21.4.2"},
   180  		{"050", "50"},
   181  		{"trololo", "trololo"},
   182  		{"0001.002.03", "1.2.3"},
   183  		{"0001.002.03-trololo", "0001.002.03-trololo"},
   184  	}
   185  
   186  	for _, tc := range tests {
   187  		tc := tc
   188  		t.Run(tc.version, func(t *testing.T) {
   189  			t.Parallel()
   190  			actual := setup.NormalizeVersion(tc.version)
   191  			require.Equal(t, tc.expected, actual)
   192  		})
   193  	}
   194  }
   195  */
   196  
   197  func TestListSupported(t *testing.T) {
   198  	t.Parallel()
   199  
   200  	tests := []struct {
   201  		name        string
   202  		yml         string
   203  		expected    []string
   204  		expectedErr string
   205  	}{
   206  		{
   207  			"list configured services",
   208  			`
   209  			version: 1.0
   210  			detect:
   211  			  foo:
   212  			  bar:
   213  			  baz:
   214  			`,
   215  			[]string{"foo", "bar", "baz"},
   216  			"",
   217  		},
   218  		{
   219  			"invalid yaml: blahblah",
   220  			"blahblah",
   221  			nil,
   222  			"yaml: unmarshal errors:",
   223  		},
   224  		{
   225  			"invalid yaml: tabs are not allowed",
   226  			`
   227  			version: 1.0
   228  			detect:
   229  				foos:
   230  			`,
   231  			nil,
   232  			"yaml: line 4: found character that cannot start any token",
   233  		},
   234  		{
   235  			"invalid yaml: no version",
   236  			"{}",
   237  			nil,
   238  			"missing version tag (must be 1.0)",
   239  		},
   240  		{
   241  			"invalid yaml: bad version",
   242  			"version: 2.0",
   243  			nil,
   244  			"invalid version tag '2.0' (must be 1.0)",
   245  		},
   246  	}
   247  
   248  	for _, tc := range tests {
   249  		tc := tc
   250  		t.Run(tc.name, func(t *testing.T) {
   251  			t.Parallel()
   252  			f := tempYAML(t, tc.yml)
   253  			defer os.Remove(f.Name())
   254  			supported, err := setup.ListSupported(&f)
   255  			cstest.RequireErrorContains(t, err, tc.expectedErr)
   256  			require.ElementsMatch(t, tc.expected, supported)
   257  		})
   258  	}
   259  }
   260  
   261  func TestApplyRules(t *testing.T) {
   262  	t.Parallel()
   263  	require := require.New(t)
   264  
   265  	tests := []struct {
   266  		name        string
   267  		rules       []string
   268  		expectedOk  bool
   269  		expectedErr string
   270  	}{
   271  		{
   272  			"empty list is always true", // XXX or false?
   273  			[]string{},
   274  			true,
   275  			"",
   276  		},
   277  		{
   278  			"simple true expression",
   279  			[]string{"1+1==2"},
   280  			true,
   281  			"",
   282  		},
   283  		{
   284  			"simple false expression",
   285  			[]string{"2+2==5"},
   286  			false,
   287  			"",
   288  		},
   289  		{
   290  			"all expressions are true",
   291  			[]string{"1+2==3", "1!=2"},
   292  			true,
   293  			"",
   294  		},
   295  		{
   296  			"all expressions must be true",
   297  			[]string{"true", "1==3", "1!=2"},
   298  			false,
   299  			"",
   300  		},
   301  		{
   302  			"each expression must be a boolan",
   303  			[]string{"true", "\"notabool\""},
   304  			false,
   305  			"rule '\"notabool\"': type must be a boolean",
   306  		},
   307  		{
   308  			// we keep evaluating expressions to ensure that the
   309  			// file is formally correct, even if it can some time.
   310  			"each expression must be a boolan (no short circuit)",
   311  			[]string{"false", "3"},
   312  			false,
   313  			"rule '3': type must be a boolean",
   314  		},
   315  		{
   316  			"unknown variable",
   317  			[]string{"false", "doesnotexist"},
   318  			false,
   319  			"rule 'doesnotexist': cannot fetch doesnotexist from",
   320  		},
   321  		{
   322  			"unknown expression",
   323  			[]string{"false", "doesnotexist()"},
   324  			false,
   325  			"rule 'doesnotexist()': cannot fetch doesnotexist from",
   326  		},
   327  	}
   328  
   329  	env := setup.ExprEnvironment{}
   330  
   331  	for _, tc := range tests {
   332  		tc := tc
   333  		t.Run(tc.name, func(t *testing.T) {
   334  			t.Parallel()
   335  			svc := setup.Service{When: tc.rules}
   336  			_, actualOk, err := setup.ApplyRules(svc, env) //nolint:typecheck,nolintlint  // exported only for tests
   337  			cstest.RequireErrorContains(t, err, tc.expectedErr)
   338  			require.Equal(tc.expectedOk, actualOk)
   339  		})
   340  	}
   341  }
   342  
   343  // XXX TODO: TestApplyRules with journalctl default
   344  
   345  func TestUnitFound(t *testing.T) {
   346  	require := require.New(t)
   347  	setup.ExecCommand = fakeExecCommand
   348  
   349  	defer func() { setup.ExecCommand = exec.Command }()
   350  
   351  	env := setup.NewExprEnvironment(setup.DetectOptions{}, setup.ExprOS{})
   352  
   353  	installed, err := env.UnitFound("crowdsec-setup-detect.service")
   354  	require.NoError(err)
   355  
   356  	require.True(installed)
   357  }
   358  
   359  // TODO apply rules to filter a list of Service structs
   360  // func testFilterWithRules(t *testing.T) {
   361  // }
   362  
   363  func TestDetectSimpleRule(t *testing.T) {
   364  	require := require.New(t)
   365  	setup.ExecCommand = fakeExecCommand
   366  
   367  	f := tempYAML(t, `
   368  	version: 1.0
   369  	detect:
   370  	  good:
   371  	    when:
   372  	      - true
   373  	  bad:
   374  	    when:
   375  	      - false
   376  	  ugly:
   377  	`)
   378  	defer os.Remove(f.Name())
   379  
   380  	detected, err := setup.Detect(&f, setup.DetectOptions{})
   381  	require.NoError(err)
   382  
   383  	expected := []setup.ServiceSetup{
   384  		{DetectedService: "good"},
   385  		{DetectedService: "ugly"},
   386  	}
   387  
   388  	require.ElementsMatch(expected, detected.Setup)
   389  }
   390  
   391  func TestDetectUnitError(t *testing.T) {
   392  	if runtime.GOOS == "windows" {
   393  		t.Skip("skipping on windows")
   394  	}
   395  
   396  	require := require.New(t)
   397  	setup.ExecCommand = fakeExecCommandNotFound
   398  
   399  	defer func() { setup.ExecCommand = exec.Command }()
   400  
   401  	tests := []struct {
   402  		name        string
   403  		config      string
   404  		expected    setup.Setup
   405  		expectedErr string
   406  	}{
   407  		{
   408  			"error is reported if systemctl does not exist",
   409  			`
   410  version: 1.0
   411  detect:
   412    wizard:
   413      when:
   414        - UnitFound("crowdsec-setup-detect.service")`,
   415  			setup.Setup{[]setup.ServiceSetup{}},
   416  			`while looking for service wizard: rule 'UnitFound("crowdsec-setup-detect.service")': ` +
   417  				`running systemctl: exec: "this-command-does-not-exist": executable file not found in $PATH`,
   418  		},
   419  	}
   420  
   421  	for _, tc := range tests {
   422  		tc := tc
   423  		t.Run(tc.name, func(t *testing.T) {
   424  			f := tempYAML(t, tc.config)
   425  			defer os.Remove(f.Name())
   426  
   427  			detected, err := setup.Detect(&f, setup.DetectOptions{})
   428  			cstest.RequireErrorContains(t, err, tc.expectedErr)
   429  			require.Equal(tc.expected, detected)
   430  		})
   431  	}
   432  }
   433  
   434  func TestDetectUnit(t *testing.T) {
   435  	require := require.New(t)
   436  	setup.ExecCommand = fakeExecCommand
   437  
   438  	defer func() { setup.ExecCommand = exec.Command }()
   439  
   440  	tests := []struct {
   441  		name        string
   442  		config      string
   443  		expected    setup.Setup
   444  		expectedErr string
   445  	}{
   446  		//		{
   447  		//			"detect a single unit, with default log filter",
   448  		//			`
   449  		// version: 1.0
   450  		// detect:
   451  		//  wizard:
   452  		//    when:
   453  		//      - UnitFound("crowdsec-setup-detect.service")
   454  		//    datasource:
   455  		//      labels:
   456  		//        type: syslog
   457  		//  sorcerer:
   458  		//    when:
   459  		//      - UnitFound("sorcerer.service")`,
   460  		//			setup.Setup{
   461  		//				Setup: []setup.ServiceSetup{
   462  		//					{
   463  		//						DetectedService: "wizard",
   464  		//						DataSource: setup.DataSourceItem{
   465  		//							"Labels":           map[string]string{"type": "syslog"},
   466  		//							"JournalCTLFilter": []string{"_SYSTEMD_UNIT=crowdsec-setup-detect.service"},
   467  		//						},
   468  		//					},
   469  		//				},
   470  		//			},
   471  		//			"",
   472  		//		},
   473  		//		{
   474  		//			"detect a single unit, but type label is missing",
   475  		//			`
   476  		// version: 1.0
   477  		// detect:
   478  		//  wizard:
   479  		//    when:
   480  		//      - UnitFound("crowdsec-setup-detect.service")`,
   481  		//			setup.Setup{},
   482  		//			"missing type label for service wizard",
   483  		//		},
   484  		{
   485  			"detect unit and pick up acquisistion filter",
   486  			`
   487  version: 1.0
   488  detect:
   489    wizard:
   490      when:
   491        - UnitFound("crowdsec-setup-detect.service")
   492      datasource:
   493        source: journalctl
   494        labels:
   495          type: syslog
   496        journalctl_filter:
   497          - _MY_CUSTOM_FILTER=something`,
   498  			setup.Setup{
   499  				Setup: []setup.ServiceSetup{
   500  					{
   501  						DetectedService: "wizard",
   502  						DataSource: setup.DataSourceItem{
   503  							// XXX this should not be DataSourceItem ??
   504  							"source":            "journalctl",
   505  							"labels":            setup.DataSourceItem{"type": "syslog"},
   506  							"journalctl_filter": []interface{}{"_MY_CUSTOM_FILTER=something"},
   507  						},
   508  					},
   509  				},
   510  			},
   511  			"",
   512  		},
   513  	}
   514  
   515  	for _, tc := range tests {
   516  		tc := tc
   517  		t.Run(tc.name, func(t *testing.T) {
   518  			f := tempYAML(t, tc.config)
   519  			defer os.Remove(f.Name())
   520  
   521  			detected, err := setup.Detect(&f, setup.DetectOptions{})
   522  			cstest.RequireErrorContains(t, err, tc.expectedErr)
   523  			require.Equal(tc.expected, detected)
   524  		})
   525  	}
   526  }
   527  
   528  func TestDetectForcedUnit(t *testing.T) {
   529  	require := require.New(t)
   530  	setup.ExecCommand = fakeExecCommand
   531  
   532  	defer func() { setup.ExecCommand = exec.Command }()
   533  
   534  	f := tempYAML(t, `
   535  	version: 1.0
   536  	detect:
   537  	  wizard:
   538  	    when:
   539  	      - UnitFound("crowdsec-setup-forced.service")
   540  	    datasource:
   541  	      source: journalctl
   542  	      labels:
   543  	        type: syslog
   544  	      journalctl_filter:
   545  	        - _SYSTEMD_UNIT=crowdsec-setup-forced.service
   546  	`)
   547  	defer os.Remove(f.Name())
   548  
   549  	detected, err := setup.Detect(&f, setup.DetectOptions{ForcedUnits: []string{"crowdsec-setup-forced.service"}})
   550  	require.NoError(err)
   551  
   552  	expected := setup.Setup{
   553  		Setup: []setup.ServiceSetup{
   554  			{
   555  				DetectedService: "wizard",
   556  				DataSource: setup.DataSourceItem{
   557  					"source":            "journalctl",
   558  					"labels":            setup.DataSourceItem{"type": "syslog"},
   559  					"journalctl_filter": []interface{}{"_SYSTEMD_UNIT=crowdsec-setup-forced.service"},
   560  				},
   561  			},
   562  		},
   563  	}
   564  	require.Equal(expected, detected)
   565  }
   566  
   567  func TestDetectForcedProcess(t *testing.T) {
   568  	if runtime.GOOS == "windows" {
   569  		// while looking for service wizard: rule 'ProcessRunning("foobar")': while looking up running processes: could not get Name: A device attached to the system is not functioning.
   570  		t.Skip("skipping on windows")
   571  	}
   572  
   573  	require := require.New(t)
   574  	setup.ExecCommand = fakeExecCommand
   575  
   576  	defer func() { setup.ExecCommand = exec.Command }()
   577  
   578  	f := tempYAML(t, `
   579  	version: 1.0
   580  	detect:
   581  	  wizard:
   582  	    when:
   583  	      - ProcessRunning("foobar")
   584  	`)
   585  	defer os.Remove(f.Name())
   586  
   587  	detected, err := setup.Detect(&f, setup.DetectOptions{ForcedProcesses: []string{"foobar"}})
   588  	require.NoError(err)
   589  
   590  	expected := setup.Setup{
   591  		Setup: []setup.ServiceSetup{
   592  			{DetectedService: "wizard"},
   593  		},
   594  	}
   595  	require.Equal(expected, detected)
   596  }
   597  
   598  func TestDetectSkipService(t *testing.T) {
   599  	if runtime.GOOS == "windows" {
   600  		t.Skip("skipping on windows")
   601  	}
   602  
   603  	require := require.New(t)
   604  	setup.ExecCommand = fakeExecCommand
   605  
   606  	defer func() { setup.ExecCommand = exec.Command }()
   607  
   608  	f := tempYAML(t, `
   609  	version: 1.0
   610  	detect:
   611  	  wizard:
   612  	    when:
   613  	      - ProcessRunning("foobar")
   614  	`)
   615  	defer os.Remove(f.Name())
   616  
   617  	detected, err := setup.Detect(&f, setup.DetectOptions{ForcedProcesses: []string{"foobar"}, SkipServices: []string{"wizard"}})
   618  	require.NoError(err)
   619  
   620  	expected := setup.Setup{[]setup.ServiceSetup{}}
   621  	require.Equal(expected, detected)
   622  }
   623  
   624  func TestDetectForcedOS(t *testing.T) {
   625  	require := require.New(t)
   626  	setup.ExecCommand = fakeExecCommand
   627  
   628  	defer func() { setup.ExecCommand = exec.Command }()
   629  
   630  	type test struct {
   631  		name        string
   632  		config      string
   633  		forced      setup.ExprOS
   634  		expected    setup.Setup
   635  		expectedErr string
   636  	}
   637  
   638  	tests := []test{
   639  		{
   640  			"detect OS - force linux",
   641  			`
   642  	version: 1.0
   643  	detect:
   644  	  linux:
   645  	    when:
   646  	      - OS.Family == "linux"`,
   647  			setup.ExprOS{Family: "linux"},
   648  			setup.Setup{
   649  				Setup: []setup.ServiceSetup{
   650  					{DetectedService: "linux"},
   651  				},
   652  			},
   653  			"",
   654  		},
   655  		{
   656  			"detect OS - force windows",
   657  			`
   658  	version: 1.0
   659  	detect:
   660  	  windows:
   661  	    when:
   662  	      - OS.Family == "windows"`,
   663  			setup.ExprOS{Family: "windows"},
   664  			setup.Setup{
   665  				Setup: []setup.ServiceSetup{
   666  					{DetectedService: "windows"},
   667  				},
   668  			},
   669  			"",
   670  		},
   671  		{
   672  			"detect OS - ubuntu (no match)",
   673  			`
   674  	version: 1.0
   675  	detect:
   676  	  linux:
   677  	    when:
   678  	      - OS.Family == "linux" && OS.ID == "ubuntu"`,
   679  			setup.ExprOS{Family: "linux"},
   680  			setup.Setup{[]setup.ServiceSetup{}},
   681  			"",
   682  		},
   683  		{
   684  			"detect OS - ubuntu (match)",
   685  			`
   686  	version: 1.0
   687  	detect:
   688  	  linux:
   689  	    when:
   690  	      - OS.Family == "linux" && OS.ID == "ubuntu"`,
   691  			setup.ExprOS{Family: "linux", ID: "ubuntu"},
   692  			setup.Setup{
   693  				Setup: []setup.ServiceSetup{
   694  					{DetectedService: "linux"},
   695  				},
   696  			},
   697  			"",
   698  		},
   699  		{
   700  			"detect OS - ubuntu (match with version)",
   701  			`
   702  	version: 1.0
   703  	detect:
   704  	  linux:
   705  	    when:
   706  	      - OS.Family == "linux" && OS.ID == "ubuntu" && OS.VersionCheck("19.04")`,
   707  			setup.ExprOS{Family: "linux", ID: "ubuntu", RawVersion: "19.04"},
   708  			setup.Setup{
   709  				Setup: []setup.ServiceSetup{
   710  					{DetectedService: "linux"},
   711  				},
   712  			},
   713  			"",
   714  		},
   715  		{
   716  			"detect OS - ubuntu >= 20.04 (no match: no version detected)",
   717  			`
   718  	version: 1.0
   719  	detect:
   720  	  linux:
   721  	    when:
   722  	      - OS.ID == "ubuntu" && OS.VersionCheck(">=20.04")`,
   723  			setup.ExprOS{Family: "linux"},
   724  			setup.Setup{[]setup.ServiceSetup{}},
   725  			"",
   726  		},
   727  		{
   728  			"detect OS - ubuntu >= 20.04 (no match: version is lower)",
   729  			`
   730  	version: 1.0
   731  	detect:
   732  	  linux:
   733  	    when:
   734  	      - OS.ID == "ubuntu" && OS.VersionCheck(">=20.04")`,
   735  			setup.ExprOS{Family: "linux", ID: "ubuntu", RawVersion: "19.10"},
   736  			setup.Setup{[]setup.ServiceSetup{}},
   737  			"",
   738  		},
   739  		{
   740  			"detect OS - ubuntu >= 20.04 (match: same version)",
   741  			`
   742  	version: 1.0
   743  	detect:
   744  	  linux:
   745  	    when:
   746  	      - OS.ID == "ubuntu" && OS.VersionCheck(">=20.04")`,
   747  			setup.ExprOS{Family: "linux", ID: "ubuntu", RawVersion: "20.04"},
   748  			setup.Setup{
   749  				Setup: []setup.ServiceSetup{
   750  					{DetectedService: "linux"},
   751  				},
   752  			},
   753  			"",
   754  		},
   755  		{
   756  			"detect OS - ubuntu >= 20.04 (match: version is higher)",
   757  			`
   758  	version: 1.0
   759  	detect:
   760  	  linux:
   761  	    when:
   762  	      - OS.ID == "ubuntu" && OS.VersionCheck(">=20.04")`,
   763  			setup.ExprOS{Family: "linux", ID: "ubuntu", RawVersion: "22.04"},
   764  			setup.Setup{
   765  				Setup: []setup.ServiceSetup{
   766  					{DetectedService: "linux"},
   767  				},
   768  			},
   769  			"",
   770  		},
   771  
   772  		{
   773  			"detect OS - ubuntu < 20.04 (no match: no version detected)",
   774  			`
   775  	version: 1.0
   776  	detect:
   777  	  linux:
   778  	    when:
   779  	      - OS.ID == "ubuntu" && OS.VersionCheck("<20.04")`,
   780  			setup.ExprOS{Family: "linux"},
   781  			setup.Setup{[]setup.ServiceSetup{}},
   782  			"",
   783  		},
   784  		{
   785  			"detect OS - ubuntu < 20.04 (no match: version is higher)",
   786  			`
   787  	version: 1.0
   788  	detect:
   789  	  linux:
   790  	    when:
   791  	      - OS.ID == "ubuntu" && OS.VersionCheck("<20.04")`,
   792  			setup.ExprOS{Family: "linux", ID: "ubuntu", RawVersion: "20.10"},
   793  			setup.Setup{[]setup.ServiceSetup{}},
   794  			"",
   795  		},
   796  		{
   797  			"detect OS - ubuntu < 20.04 (no match: same version)",
   798  			`
   799  	version: 1.0
   800  	detect:
   801  	  linux:
   802  	    when:
   803  	      - OS.ID == "ubuntu" && OS.VersionCheck("<20.04")`,
   804  			setup.ExprOS{Family: "linux", ID: "ubuntu", RawVersion: "20.04"},
   805  			setup.Setup{[]setup.ServiceSetup{}},
   806  			"",
   807  		},
   808  		{
   809  			"detect OS - ubuntu < 20.04 (match: version is lower)",
   810  			`
   811  	version: 1.0
   812  	detect:
   813  	  linux:
   814  	    when:
   815  	      - OS.ID == "ubuntu"
   816  	      - OS.VersionCheck("<20.04")`,
   817  			setup.ExprOS{Family: "linux", ID: "ubuntu", RawVersion: "19.10"},
   818  			setup.Setup{
   819  				Setup: []setup.ServiceSetup{
   820  					{DetectedService: "linux"},
   821  				},
   822  			},
   823  			"",
   824  		},
   825  	}
   826  
   827  	for _, tc := range tests {
   828  		tc := tc
   829  		t.Run(tc.name, func(t *testing.T) {
   830  			f := tempYAML(t, tc.config)
   831  			defer os.Remove(f.Name())
   832  
   833  			detected, err := setup.Detect(&f, setup.DetectOptions{ForcedOS: tc.forced})
   834  			cstest.RequireErrorContains(t, err, tc.expectedErr)
   835  			require.Equal(tc.expected, detected)
   836  		})
   837  	}
   838  }
   839  
   840  func TestDetectDatasourceValidation(t *testing.T) {
   841  	// It could be a good idea to test UnmarshalConfig() separately in addition
   842  	// to Configure(), in each datasource. For now, we test these here.
   843  
   844  	require := require.New(t)
   845  	setup.ExecCommand = fakeExecCommand
   846  
   847  	defer func() { setup.ExecCommand = exec.Command }()
   848  
   849  	type test struct {
   850  		name        string
   851  		config      string
   852  		expected    setup.Setup
   853  		expectedErr string
   854  	}
   855  
   856  	tests := []test{
   857  		{
   858  			name: "source is empty",
   859  			config: `
   860  				version: 1.0
   861  				detect:
   862  				  wizard:
   863  				    datasource:
   864  				      labels:
   865  				        type: something`,
   866  			expected:    setup.Setup{Setup: []setup.ServiceSetup{}},
   867  			expectedErr: "invalid datasource for wizard: source is empty",
   868  		}, {
   869  			name: "source is unknown",
   870  			config: `
   871  				version: 1.0
   872  				detect:
   873  				  foobar:
   874  				    datasource:
   875  				      source: wombat`,
   876  			expected:    setup.Setup{Setup: []setup.ServiceSetup{}},
   877  			expectedErr: "invalid datasource for foobar: unknown source 'wombat'",
   878  		}, {
   879  			name: "source is misplaced",
   880  			config: `
   881  				version: 1.0
   882  				detect:
   883  				  foobar:
   884  				    datasource:
   885  				    source: file`,
   886  			expected:    setup.Setup{Setup: []setup.ServiceSetup{}},
   887  			expectedErr: "yaml: unmarshal errors:\n  line 6: field source not found in type setup.Service",
   888  		}, {
   889  			name: "source is mismatched",
   890  			config: `
   891  				version: 1.0
   892  				detect:
   893  				  foobar:
   894  				    datasource:
   895  				      source: journalctl
   896  				      filename: /path/to/file.log`,
   897  			expected:    setup.Setup{Setup: []setup.ServiceSetup{}},
   898  			expectedErr: "invalid datasource for foobar: cannot parse JournalCtlSource configuration: yaml: unmarshal errors:\n  line 1: field filename not found in type journalctlacquisition.JournalCtlConfiguration",
   899  		}, {
   900  			name: "source file: required fields",
   901  			config: `
   902  				version: 1.0
   903  				detect:
   904  				  foobar:
   905  				    datasource:
   906  				      source: file`,
   907  			expected:    setup.Setup{Setup: []setup.ServiceSetup{}},
   908  			expectedErr: "invalid datasource for foobar: no filename or filenames configuration provided",
   909  		}, {
   910  			name: "source journalctl: required fields",
   911  			config: `
   912  				version: 1.0
   913  				detect:
   914  				  foobar:
   915  				    datasource:
   916  				      source: journalctl`,
   917  			expected:    setup.Setup{Setup: []setup.ServiceSetup{}},
   918  			expectedErr: "invalid datasource for foobar: journalctl_filter is required",
   919  		}, {
   920  			name: "source cloudwatch: required fields",
   921  			config: `
   922  				version: 1.0
   923  				detect:
   924  				  foobar:
   925  				    datasource:
   926  				      source: cloudwatch`,
   927  			expected:    setup.Setup{Setup: []setup.ServiceSetup{}},
   928  			expectedErr: "invalid datasource for foobar: group_name is mandatory for CloudwatchSource",
   929  		}, {
   930  			name: "source syslog: all fields are optional",
   931  			config: `
   932  				version: 1.0
   933  				detect:
   934  				  foobar:
   935  				    datasource:
   936  				      source: syslog`,
   937  			expected: setup.Setup{
   938  				Setup: []setup.ServiceSetup{
   939  					{
   940  						DetectedService: "foobar",
   941  						DataSource:      setup.DataSourceItem{"source": "syslog"},
   942  					},
   943  				},
   944  			},
   945  		}, {
   946  			name: "source docker: required fields",
   947  			config: `
   948  				version: 1.0
   949  				detect:
   950  				  foobar:
   951  				    datasource:
   952  				      source: docker`,
   953  			expected:    setup.Setup{Setup: []setup.ServiceSetup{}},
   954  			expectedErr: "invalid datasource for foobar: no containers names or containers ID configuration provided",
   955  		}, {
   956  			name: "source kinesis: required fields (enhanced fanout=false)",
   957  			config: `
   958  				version: 1.0
   959  				detect:
   960  				  foobar:
   961  				    datasource:
   962  				      source: kinesis`,
   963  			expected:    setup.Setup{Setup: []setup.ServiceSetup{}},
   964  			expectedErr: "invalid datasource for foobar: stream_name is mandatory when use_enhanced_fanout is false",
   965  		}, {
   966  			name: "source kinesis: required fields (enhanced fanout=true)",
   967  			config: `
   968  				version: 1.0
   969  				detect:
   970  				  foobar:
   971  				    datasource:
   972  				      source: kinesis
   973  				      use_enhanced_fanout: true`,
   974  			expected:    setup.Setup{Setup: []setup.ServiceSetup{}},
   975  			expectedErr: "invalid datasource for foobar: stream_arn is mandatory when use_enhanced_fanout is true",
   976  		}, {
   977  			name: "source kafka: required fields",
   978  			config: `
   979  				version: 1.0
   980  				detect:
   981  				  foobar:
   982  				    datasource:
   983  				      source: kafka`,
   984  			expected:    setup.Setup{Setup: []setup.ServiceSetup{}},
   985  			expectedErr: "invalid datasource for foobar: cannot create a kafka reader with an empty list of broker addresses",
   986  		}, {
   987  			name: "source loki: required fields",
   988  			config: `
   989  				version: 1.0
   990  				detect:
   991  				  foobar:
   992  				    datasource:
   993  				      source: loki`,
   994  			expected:    setup.Setup{Setup: []setup.ServiceSetup{}},
   995  			expectedErr: "invalid datasource for foobar: loki query is mandatory",
   996  		},
   997  	}
   998  
   999  	if runtime.GOOS == "windows" {
  1000  		tests = append(tests, test{
  1001  			name: "source wineventlog: required fields",
  1002  			config: `
  1003  				version: 1.0
  1004  				detect:
  1005  				  foobar:
  1006  				    datasource:
  1007  				      source: wineventlog`,
  1008  			expected:    setup.Setup{Setup: []setup.ServiceSetup{}},
  1009  			expectedErr: "invalid datasource for foobar: event_channel or xpath_query must be set",
  1010  		})
  1011  	}
  1012  
  1013  	for _, tc := range tests {
  1014  		tc := tc
  1015  		t.Run(tc.name, func(t *testing.T) {
  1016  			f := tempYAML(t, tc.config)
  1017  			defer os.Remove(f.Name())
  1018  			detected, err := setup.Detect(&f, setup.DetectOptions{})
  1019  			cstest.RequireErrorContains(t, err, tc.expectedErr)
  1020  			require.Equal(tc.expected, detected)
  1021  		})
  1022  	}
  1023  }