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 }