detect.go 14 KB

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