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

     1  package setup
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"os/exec"
     9  	"sort"
    10  
    11  	"github.com/Masterminds/semver/v3"
    12  	"github.com/antonmedv/expr"
    13  	"github.com/blackfireio/osinfo"
    14  	"github.com/shirou/gopsutil/v3/process"
    15  	log "github.com/sirupsen/logrus"
    16  	"gopkg.in/yaml.v3"
    17  
    18  	"github.com/crowdsecurity/crowdsec/pkg/acquisition"
    19  	"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
    20  )
    21  
    22  // ExecCommand can be replaced with a mock during tests.
    23  var ExecCommand = exec.Command
    24  
    25  // HubItems contains the objects that are recommended to support a service.
    26  type HubItems struct {
    27  	Collections   []string `yaml:"collections,omitempty"`
    28  	Parsers       []string `yaml:"parsers,omitempty"`
    29  	Scenarios     []string `yaml:"scenarios,omitempty"`
    30  	PostOverflows []string `yaml:"postoverflows,omitempty"`
    31  }
    32  
    33  type DataSourceItem map[string]interface{}
    34  
    35  // ServiceSetup describes the recommendations (hub objects and datasources) for a detected service.
    36  type ServiceSetup struct {
    37  	DetectedService string         `yaml:"detected_service"`
    38  	Install         *HubItems      `yaml:"install,omitempty"`
    39  	DataSource      DataSourceItem `yaml:"datasource,omitempty"`
    40  }
    41  
    42  // Setup is a container for a list of ServiceSetup objects, allowing for future extensions.
    43  type Setup struct {
    44  	Setup []ServiceSetup `yaml:"setup"`
    45  }
    46  
    47  func validateDataSource(opaqueDS DataSourceItem) error {
    48  	if len(opaqueDS) == 0 {
    49  		// empty datasource is valid
    50  		return nil
    51  	}
    52  
    53  	// formally validate YAML
    54  
    55  	commonDS := configuration.DataSourceCommonCfg{}
    56  	body, err := yaml.Marshal(opaqueDS)
    57  	if err != nil {
    58  		return err
    59  	}
    60  
    61  	err = yaml.Unmarshal(body, &commonDS)
    62  	if err != nil {
    63  		return err
    64  	}
    65  
    66  	// source is mandatory // XXX unless it's not?
    67  
    68  	if commonDS.Source == "" {
    69  		return fmt.Errorf("source is empty")
    70  	}
    71  
    72  	// source must be known
    73  
    74  	ds := acquisition.GetDataSourceIface(commonDS.Source)
    75  	if ds == nil {
    76  		return fmt.Errorf("unknown source '%s'", commonDS.Source)
    77  	}
    78  
    79  	// unmarshal and validate the rest with the specific implementation
    80  
    81  	err = ds.UnmarshalConfig(body)
    82  	if err != nil {
    83  		return err
    84  	}
    85  
    86  	// pp.Println(ds)
    87  	return nil
    88  }
    89  
    90  func readDetectConfig(fin io.Reader) (DetectConfig, error) {
    91  	var dc DetectConfig
    92  
    93  	yamlBytes, err := io.ReadAll(fin)
    94  	if err != nil {
    95  		return DetectConfig{}, err
    96  	}
    97  
    98  	dec := yaml.NewDecoder(bytes.NewBuffer(yamlBytes))
    99  	dec.KnownFields(true)
   100  
   101  	if err = dec.Decode(&dc); err != nil {
   102  		return DetectConfig{}, err
   103  	}
   104  
   105  	switch dc.Version {
   106  	case "":
   107  		return DetectConfig{}, fmt.Errorf("missing version tag (must be 1.0)")
   108  	case "1.0":
   109  		// all is well
   110  	default:
   111  		return DetectConfig{}, fmt.Errorf("invalid version tag '%s' (must be 1.0)", dc.Version)
   112  	}
   113  
   114  	for name, svc := range dc.Detect {
   115  		err = validateDataSource(svc.DataSource)
   116  		if err != nil {
   117  			return DetectConfig{}, fmt.Errorf("invalid datasource for %s: %w", name, err)
   118  		}
   119  	}
   120  
   121  	return dc, nil
   122  }
   123  
   124  // Service describes the rules for detecting a service and its recommended items.
   125  type Service struct {
   126  	When       []string       `yaml:"when"`
   127  	Install    *HubItems      `yaml:"install,omitempty"`
   128  	DataSource DataSourceItem `yaml:"datasource,omitempty"`
   129  	// AcquisYAML []byte
   130  }
   131  
   132  // DetectConfig is the container of all detection rules (detect.yaml).
   133  type DetectConfig struct {
   134  	Version string             `yaml:"version"`
   135  	Detect  map[string]Service `yaml:"detect"`
   136  }
   137  
   138  // ExprState keeps a global state for the duration of the service detection (cache etc.)
   139  type ExprState struct {
   140  	unitsSearched map[string]bool
   141  	detectOptions DetectOptions
   142  
   143  	// cache
   144  	installedUnits map[string]bool
   145  	// true if the list of running processes has already been retrieved, we can
   146  	// avoid getting it a second time.
   147  	processesSearched map[string]bool
   148  	// cache
   149  	runningProcesses map[string]bool
   150  }
   151  
   152  // ExprServiceState keep a local state during the detection of a single service. It is reset before each service rules' evaluation.
   153  type ExprServiceState struct {
   154  	detectedUnits []string
   155  }
   156  
   157  // ExprOS contains the detected (or forced) OS fields available to the rule engine.
   158  type ExprOS struct {
   159  	Family     string
   160  	ID         string
   161  	RawVersion string
   162  }
   163  
   164  // This is not required with Masterminds/semver
   165  /*
   166  // normalizeVersion strips leading zeroes from each part, to allow comparison of ubuntu-like versions.
   167  func normalizeVersion(version string) string {
   168  	// if it doesn't match a version string, return unchanged
   169  	if ok := regexp.MustCompile(`^(\d+)(\.\d+)?(\.\d+)?$`).MatchString(version); !ok {
   170  		// definitely not an ubuntu-like version, return unchanged
   171  		return version
   172  	}
   173  
   174  	ret := []rune{}
   175  
   176  	var cur rune
   177  
   178  	trim := true
   179  	for _, next := range version + "." {
   180  		if trim && cur == '0' && next != '.' {
   181  			cur = next
   182  
   183  			continue
   184  		}
   185  
   186  		if cur != 0 {
   187  			ret = append(ret, cur)
   188  		}
   189  
   190  		trim = (cur == '.' || cur == 0)
   191  		cur = next
   192  	}
   193  
   194  	return string(ret)
   195  }
   196  */
   197  
   198  // VersionCheck returns true if the version of the OS matches the given constraint
   199  func (os ExprOS) VersionCheck(constraint string) (bool, error) {
   200  	v, err := semver.NewVersion(os.RawVersion)
   201  	if err != nil {
   202  		return false, err
   203  	}
   204  
   205  	c, err := semver.NewConstraint(constraint)
   206  	if err != nil {
   207  		return false, err
   208  	}
   209  
   210  	return c.Check(v), nil
   211  }
   212  
   213  // VersionAtLeast returns true if the version of the OS is at least the given version.
   214  func (os ExprOS) VersionAtLeast(constraint string) (bool, error) {
   215  	return os.VersionCheck(">=" + constraint)
   216  }
   217  
   218  // VersionIsLower returns true if the version of the OS is lower than the given version.
   219  func (os ExprOS) VersionIsLower(version string) (bool, error) {
   220  	result, err := os.VersionAtLeast(version)
   221  	if err != nil {
   222  		return false, err
   223  	}
   224  
   225  	return !result, nil
   226  }
   227  
   228  // ExprEnvironment is used to expose functions and values to the rule engine.
   229  // It can cache the results of service detection commands, like systemctl etc.
   230  type ExprEnvironment struct {
   231  	OS ExprOS
   232  
   233  	_serviceState *ExprServiceState
   234  	_state        *ExprState
   235  }
   236  
   237  // NewExprEnvironment creates an environment object for the rule engine.
   238  func NewExprEnvironment(opts DetectOptions, os ExprOS) ExprEnvironment {
   239  	return ExprEnvironment{
   240  		_state: &ExprState{
   241  			detectOptions: opts,
   242  
   243  			unitsSearched:  make(map[string]bool),
   244  			installedUnits: make(map[string]bool),
   245  
   246  			processesSearched: make(map[string]bool),
   247  			runningProcesses:  make(map[string]bool),
   248  		},
   249  		_serviceState: &ExprServiceState{},
   250  		OS:            os,
   251  	}
   252  }
   253  
   254  // PathExists returns true if the given path exists.
   255  func (e ExprEnvironment) PathExists(path string) bool {
   256  	_, err := os.Stat(path)
   257  
   258  	return err == nil
   259  }
   260  
   261  // UnitFound returns true if the unit is listed in the systemctl output.
   262  // Whether a disabled or failed unit is considered found or not, depends on the
   263  // systemctl parameters used.
   264  func (e ExprEnvironment) UnitFound(unitName string) (bool, error) {
   265  	// fill initial caches
   266  	if len(e._state.unitsSearched) == 0 {
   267  		if !e._state.detectOptions.SnubSystemd {
   268  			units, err := systemdUnitList()
   269  			if err != nil {
   270  				return false, err
   271  			}
   272  
   273  			for _, name := range units {
   274  				e._state.installedUnits[name] = true
   275  			}
   276  		}
   277  
   278  		for _, name := range e._state.detectOptions.ForcedUnits {
   279  			e._state.installedUnits[name] = true
   280  		}
   281  	}
   282  
   283  	e._state.unitsSearched[unitName] = true
   284  	if e._state.installedUnits[unitName] {
   285  		e._serviceState.detectedUnits = append(e._serviceState.detectedUnits, unitName)
   286  
   287  		return true, nil
   288  	}
   289  
   290  	return false, nil
   291  }
   292  
   293  // ProcessRunning returns true if there is a running process with the given name.
   294  func (e ExprEnvironment) ProcessRunning(processName string) (bool, error) {
   295  	if len(e._state.processesSearched) == 0 {
   296  		procs, err := process.Processes()
   297  		if err != nil {
   298  			return false, fmt.Errorf("while looking up running processes: %w", err)
   299  		}
   300  
   301  		for _, p := range procs {
   302  			name, err := p.Name()
   303  			if err != nil {
   304  				return false, fmt.Errorf("while looking up running processes: %w", err)
   305  			}
   306  
   307  			e._state.runningProcesses[name] = true
   308  		}
   309  
   310  		for _, name := range e._state.detectOptions.ForcedProcesses {
   311  			e._state.runningProcesses[name] = true
   312  		}
   313  	}
   314  
   315  	e._state.processesSearched[processName] = true
   316  
   317  	return e._state.runningProcesses[processName], nil
   318  }
   319  
   320  // applyRules checks if the 'when' expressions are true and returns a Service struct,
   321  // augmented with default values and anything that might be useful later on
   322  //
   323  // All expressions are evaluated (no short-circuit) because we want to know if there are errors.
   324  func applyRules(svc Service, env ExprEnvironment) (Service, bool, error) {
   325  	newsvc := svc
   326  	svcok := true
   327  	env._serviceState = &ExprServiceState{}
   328  
   329  	for _, rule := range svc.When {
   330  		out, err := expr.Eval(rule, env)
   331  		log.Tracef("  Rule '%s' -> %t, %v", rule, out, err)
   332  
   333  		if err != nil {
   334  			return Service{}, false, fmt.Errorf("rule '%s': %w", rule, err)
   335  		}
   336  
   337  		outbool, ok := out.(bool)
   338  		if !ok {
   339  			return Service{}, false, fmt.Errorf("rule '%s': type must be a boolean", rule)
   340  		}
   341  
   342  		svcok = svcok && outbool
   343  	}
   344  
   345  	//	if newsvc.Acquis == nil || (newsvc.Acquis.LogFiles == nil && newsvc.Acquis.JournalCTLFilter == nil) {
   346  	//		for _, unitName := range env._serviceState.detectedUnits {
   347  	//			if newsvc.Acquis == nil {
   348  	//				newsvc.Acquis = &AcquisItem{}
   349  	//			}
   350  	//			// if there is reference to more than one unit in the rules, we use the first one
   351  	//			newsvc.Acquis.JournalCTLFilter = []string{fmt.Sprintf(`_SYSTEMD_UNIT=%s`, unitName)}
   352  	//			break //nolint  // we want to exit after one iteration
   353  	//		}
   354  	//	}
   355  
   356  	return newsvc, svcok, nil
   357  }
   358  
   359  // filterWithRules decorates a DetectConfig map by filtering according to the when: clauses,
   360  // and applying default values or whatever useful to the Service items.
   361  func filterWithRules(dc DetectConfig, env ExprEnvironment) (map[string]Service, error) {
   362  	ret := make(map[string]Service)
   363  
   364  	for name := range dc.Detect {
   365  		//
   366  		// an empty list of when: clauses defaults to true, if we want
   367  		// to change this behavior, the place is here.
   368  		// if len(svc.When) == 0 {
   369  		// 	log.Warningf("empty 'when' clause: %+v", svc)
   370  		// }
   371  		//
   372  		log.Trace("Evaluating rules for: ", name)
   373  
   374  		svc, ok, err := applyRules(dc.Detect[name], env)
   375  		if err != nil {
   376  			return nil, fmt.Errorf("while looking for service %s: %w", name, err)
   377  		}
   378  
   379  		if !ok {
   380  			log.Tracef("  Skipping %s", name)
   381  
   382  			continue
   383  		}
   384  
   385  		log.Tracef("  Detected %s", name)
   386  
   387  		ret[name] = svc
   388  	}
   389  
   390  	return ret, nil
   391  }
   392  
   393  // return units that have been forced but not searched yet.
   394  func (e ExprEnvironment) unsearchedUnits() []string {
   395  	ret := []string{}
   396  
   397  	for _, unit := range e._state.detectOptions.ForcedUnits {
   398  		if !e._state.unitsSearched[unit] {
   399  			ret = append(ret, unit)
   400  		}
   401  	}
   402  
   403  	return ret
   404  }
   405  
   406  // return processes that have been forced but not searched yet.
   407  func (e ExprEnvironment) unsearchedProcesses() []string {
   408  	ret := []string{}
   409  
   410  	for _, proc := range e._state.detectOptions.ForcedProcesses {
   411  		if !e._state.processesSearched[proc] {
   412  			ret = append(ret, proc)
   413  		}
   414  	}
   415  
   416  	return ret
   417  }
   418  
   419  // checkConsumedForcedItems checks if all the "forced" options (units or processes) have been evaluated during the service detection.
   420  func checkConsumedForcedItems(e ExprEnvironment) error {
   421  	unconsumed := e.unsearchedUnits()
   422  
   423  	unitMsg := ""
   424  	if len(unconsumed) > 0 {
   425  		unitMsg = fmt.Sprintf("unit(s) forced but not supported: %v", unconsumed)
   426  	}
   427  
   428  	unconsumed = e.unsearchedProcesses()
   429  
   430  	procsMsg := ""
   431  	if len(unconsumed) > 0 {
   432  		procsMsg = fmt.Sprintf("process(es) forced but not supported: %v", unconsumed)
   433  	}
   434  
   435  	join := ""
   436  	if unitMsg != "" && procsMsg != "" {
   437  		join = "; "
   438  	}
   439  
   440  	if unitMsg != "" || procsMsg != "" {
   441  		return fmt.Errorf("%s%s%s", unitMsg, join, procsMsg)
   442  	}
   443  
   444  	return nil
   445  }
   446  
   447  // DetectOptions contains parameters for the Detect function.
   448  type DetectOptions struct {
   449  	// slice of unit names that we want to force-detect
   450  	ForcedUnits []string
   451  	// slice of process names that we want to force-detect
   452  	ForcedProcesses []string
   453  	ForcedOS        ExprOS
   454  	SkipServices    []string
   455  	SnubSystemd     bool
   456  }
   457  
   458  // Detect performs the service detection from a given configuration.
   459  // It outputs a setup file that can be used as input to "cscli setup install-hub"
   460  // or "cscli setup datasources".
   461  func Detect(detectReader io.Reader, opts DetectOptions) (Setup, error) {
   462  	ret := Setup{}
   463  
   464  	// explicitly initialize to avoid json mashaling an empty slice as "null"
   465  	ret.Setup = make([]ServiceSetup, 0)
   466  
   467  	sc, err := readDetectConfig(detectReader)
   468  	if err != nil {
   469  		return ret, err
   470  	}
   471  
   472  	//	// generate acquis.yaml snippet for this service
   473  	//	for key := range sc.Detect {
   474  	//		svc := sc.Detect[key]
   475  	//		if svc.Acquis != nil {
   476  	//			svc.AcquisYAML, err = yaml.Marshal(svc.Acquis)
   477  	//			if err != nil {
   478  	//				return ret, err
   479  	//			}
   480  	//			sc.Detect[key] = svc
   481  	//		}
   482  	//	}
   483  
   484  	var osfull *osinfo.OSInfo
   485  
   486  	os := opts.ForcedOS
   487  	if os == (ExprOS{}) {
   488  		osfull, err = osinfo.GetOSInfo()
   489  		if err != nil {
   490  			return ret, fmt.Errorf("detecting OS: %w", err)
   491  		}
   492  
   493  		log.Tracef("Detected OS - %+v", *osfull)
   494  
   495  		os = ExprOS{
   496  			Family:     osfull.Family,
   497  			ID:         osfull.ID,
   498  			RawVersion: osfull.Version,
   499  		}
   500  	} else {
   501  		log.Tracef("Forced OS - %+v", os)
   502  	}
   503  
   504  	if len(opts.ForcedUnits) > 0 {
   505  		log.Tracef("Forced units - %v", opts.ForcedUnits)
   506  	}
   507  
   508  	if len(opts.ForcedProcesses) > 0 {
   509  		log.Tracef("Forced processes - %v", opts.ForcedProcesses)
   510  	}
   511  
   512  	env := NewExprEnvironment(opts, os)
   513  
   514  	detected, err := filterWithRules(sc, env)
   515  	if err != nil {
   516  		return ret, err
   517  	}
   518  
   519  	if err = checkConsumedForcedItems(env); err != nil {
   520  		return ret, err
   521  	}
   522  
   523  	// remove services the user asked to ignore
   524  	for _, name := range opts.SkipServices {
   525  		delete(detected, name)
   526  	}
   527  
   528  	// sort the keys (service names) to have them in a predictable
   529  	// order in the final output
   530  
   531  	keys := make([]string, 0)
   532  	for k := range detected {
   533  		keys = append(keys, k)
   534  	}
   535  
   536  	sort.Strings(keys)
   537  
   538  	for _, name := range keys {
   539  		svc := detected[name]
   540  		//		if svc.DataSource != nil {
   541  		//			if svc.DataSource.Labels["type"] == "" {
   542  		//				return Setup{}, fmt.Errorf("missing type label for service %s", name)
   543  		//			}
   544  		//			err = yaml.Unmarshal(svc.AcquisYAML, svc.DataSource)
   545  		//			if err != nil {
   546  		//				return Setup{}, fmt.Errorf("while unmarshaling datasource for service %s: %w", name, err)
   547  		//			}
   548  		//		}
   549  
   550  		ret.Setup = append(ret.Setup, ServiceSetup{
   551  			DetectedService: name,
   552  			Install:         svc.Install,
   553  			DataSource:      svc.DataSource,
   554  		})
   555  	}
   556  
   557  	return ret, nil
   558  }
   559  
   560  // ListSupported parses the configuration file and outputs a list of the supported services.
   561  func ListSupported(detectConfig io.Reader) ([]string, error) {
   562  	dc, err := readDetectConfig(detectConfig)
   563  	if err != nil {
   564  		return nil, err
   565  	}
   566  
   567  	keys := make([]string, 0)
   568  	for k := range dc.Detect {
   569  		keys = append(keys, k)
   570  	}
   571  
   572  	sort.Strings(keys)
   573  
   574  	return keys, nil
   575  }