hubtest.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "math"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "github.com/AlecAivazis/survey/v2"
  10. "github.com/enescakir/emoji"
  11. "github.com/fatih/color"
  12. log "github.com/sirupsen/logrus"
  13. "github.com/spf13/cobra"
  14. "gopkg.in/yaml.v2"
  15. "github.com/crowdsecurity/crowdsec/pkg/hubtest"
  16. )
  17. var (
  18. HubTest hubtest.HubTest
  19. )
  20. func NewHubTestCmd() *cobra.Command {
  21. var hubPath string
  22. var crowdsecPath string
  23. var cscliPath string
  24. var cmdHubTest = &cobra.Command{
  25. Use: "hubtest",
  26. Short: "Run functional tests on hub configurations",
  27. Long: "Run functional tests on hub configurations (parsers, scenarios, collections...)",
  28. Args: cobra.ExactArgs(0),
  29. DisableAutoGenTag: true,
  30. PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
  31. var err error
  32. HubTest, err = hubtest.NewHubTest(hubPath, crowdsecPath, cscliPath)
  33. if err != nil {
  34. return fmt.Errorf("unable to load hubtest: %+v", err)
  35. }
  36. return nil
  37. },
  38. }
  39. cmdHubTest.PersistentFlags().StringVar(&hubPath, "hub", ".", "Path to hub folder")
  40. cmdHubTest.PersistentFlags().StringVar(&crowdsecPath, "crowdsec", "crowdsec", "Path to crowdsec")
  41. cmdHubTest.PersistentFlags().StringVar(&cscliPath, "cscli", "cscli", "Path to cscli")
  42. cmdHubTest.AddCommand(NewHubTestCreateCmd())
  43. cmdHubTest.AddCommand(NewHubTestRunCmd())
  44. cmdHubTest.AddCommand(NewHubTestCleanCmd())
  45. cmdHubTest.AddCommand(NewHubTestInfoCmd())
  46. cmdHubTest.AddCommand(NewHubTestListCmd())
  47. cmdHubTest.AddCommand(NewHubTestCoverageCmd())
  48. cmdHubTest.AddCommand(NewHubTestEvalCmd())
  49. cmdHubTest.AddCommand(NewHubTestExplainCmd())
  50. return cmdHubTest
  51. }
  52. func NewHubTestCreateCmd() *cobra.Command {
  53. parsers := []string{}
  54. postoverflows := []string{}
  55. scenarios := []string{}
  56. var ignoreParsers bool
  57. var labels map[string]string
  58. var logType string
  59. var cmdHubTestCreate = &cobra.Command{
  60. Use: "create",
  61. Short: "create [test_name]",
  62. Example: `cscli hubtest create my-awesome-test --type syslog
  63. cscli hubtest create my-nginx-custom-test --type nginx
  64. cscli hubtest create my-scenario-test --parsers crowdsecurity/nginx --scenarios crowdsecurity/http-probing`,
  65. Args: cobra.ExactArgs(1),
  66. DisableAutoGenTag: true,
  67. RunE: func(cmd *cobra.Command, args []string) error {
  68. testName := args[0]
  69. testPath := filepath.Join(HubTest.HubTestPath, testName)
  70. if _, err := os.Stat(testPath); os.IsExist(err) {
  71. return fmt.Errorf("test '%s' already exists in '%s', exiting", testName, testPath)
  72. }
  73. if logType == "" {
  74. return fmt.Errorf("please provide a type (--type) for the test")
  75. }
  76. if err := os.MkdirAll(testPath, os.ModePerm); err != nil {
  77. return fmt.Errorf("unable to create folder '%s': %+v", testPath, err)
  78. }
  79. // create empty log file
  80. logFileName := fmt.Sprintf("%s.log", testName)
  81. logFilePath := filepath.Join(testPath, logFileName)
  82. logFile, err := os.Create(logFilePath)
  83. if err != nil {
  84. return err
  85. }
  86. logFile.Close()
  87. // create empty parser assertion file
  88. parserAssertFilePath := filepath.Join(testPath, hubtest.ParserAssertFileName)
  89. parserAssertFile, err := os.Create(parserAssertFilePath)
  90. if err != nil {
  91. return err
  92. }
  93. parserAssertFile.Close()
  94. // create empty scenario assertion file
  95. scenarioAssertFilePath := filepath.Join(testPath, hubtest.ScenarioAssertFileName)
  96. scenarioAssertFile, err := os.Create(scenarioAssertFilePath)
  97. if err != nil {
  98. return err
  99. }
  100. scenarioAssertFile.Close()
  101. parsers = append(parsers, "crowdsecurity/syslog-logs")
  102. parsers = append(parsers, "crowdsecurity/dateparse-enrich")
  103. if len(scenarios) == 0 {
  104. scenarios = append(scenarios, "")
  105. }
  106. if len(postoverflows) == 0 {
  107. postoverflows = append(postoverflows, "")
  108. }
  109. configFileData := &hubtest.HubTestItemConfig{
  110. Parsers: parsers,
  111. Scenarios: scenarios,
  112. PostOVerflows: postoverflows,
  113. LogFile: logFileName,
  114. LogType: logType,
  115. IgnoreParsers: ignoreParsers,
  116. Labels: labels,
  117. }
  118. configFilePath := filepath.Join(testPath, "config.yaml")
  119. fd, err := os.OpenFile(configFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
  120. if err != nil {
  121. return fmt.Errorf("open: %s", err)
  122. }
  123. data, err := yaml.Marshal(configFileData)
  124. if err != nil {
  125. return fmt.Errorf("marshal: %s", err)
  126. }
  127. _, err = fd.Write(data)
  128. if err != nil {
  129. return fmt.Errorf("write: %s", err)
  130. }
  131. if err := fd.Close(); err != nil {
  132. return fmt.Errorf("close: %s", err)
  133. }
  134. fmt.Println()
  135. fmt.Printf(" Test name : %s\n", testName)
  136. fmt.Printf(" Test path : %s\n", testPath)
  137. fmt.Printf(" Log file : %s (please fill it with logs)\n", logFilePath)
  138. fmt.Printf(" Parser assertion file : %s (please fill it with assertion)\n", parserAssertFilePath)
  139. fmt.Printf(" Scenario assertion file : %s (please fill it with assertion)\n", scenarioAssertFilePath)
  140. fmt.Printf(" Configuration File : %s (please fill it with parsers, scenarios...)\n", configFilePath)
  141. return nil
  142. },
  143. }
  144. cmdHubTestCreate.PersistentFlags().StringVarP(&logType, "type", "t", "", "Log type of the test")
  145. cmdHubTestCreate.Flags().StringSliceVarP(&parsers, "parsers", "p", parsers, "Parsers to add to test")
  146. cmdHubTestCreate.Flags().StringSliceVar(&postoverflows, "postoverflows", postoverflows, "Postoverflows to add to test")
  147. cmdHubTestCreate.Flags().StringSliceVarP(&scenarios, "scenarios", "s", scenarios, "Scenarios to add to test")
  148. cmdHubTestCreate.PersistentFlags().BoolVar(&ignoreParsers, "ignore-parsers", false, "Don't run test on parsers")
  149. return cmdHubTestCreate
  150. }
  151. func NewHubTestRunCmd() *cobra.Command {
  152. var noClean bool
  153. var runAll bool
  154. var forceClean bool
  155. var cmdHubTestRun = &cobra.Command{
  156. Use: "run",
  157. Short: "run [test_name]",
  158. DisableAutoGenTag: true,
  159. RunE: func(cmd *cobra.Command, args []string) error {
  160. if !runAll && len(args) == 0 {
  161. printHelp(cmd)
  162. return fmt.Errorf("Please provide test to run or --all flag")
  163. }
  164. if runAll {
  165. if err := HubTest.LoadAllTests(); err != nil {
  166. return fmt.Errorf("unable to load all tests: %+v", err)
  167. }
  168. } else {
  169. for _, testName := range args {
  170. _, err := HubTest.LoadTestItem(testName)
  171. if err != nil {
  172. return fmt.Errorf("unable to load test '%s': %s", testName, err)
  173. }
  174. }
  175. }
  176. for _, test := range HubTest.Tests {
  177. if csConfig.Cscli.Output == "human" {
  178. log.Infof("Running test '%s'", test.Name)
  179. }
  180. err := test.Run()
  181. if err != nil {
  182. log.Errorf("running test '%s' failed: %+v", test.Name, err)
  183. }
  184. }
  185. return nil
  186. },
  187. PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
  188. success := true
  189. testResult := make(map[string]bool)
  190. for _, test := range HubTest.Tests {
  191. if test.AutoGen {
  192. if test.ParserAssert.AutoGenAssert {
  193. log.Warningf("Assert file '%s' is empty, generating assertion:", test.ParserAssert.File)
  194. fmt.Println()
  195. fmt.Println(test.ParserAssert.AutoGenAssertData)
  196. }
  197. if test.ScenarioAssert.AutoGenAssert {
  198. log.Warningf("Assert file '%s' is empty, generating assertion:", test.ScenarioAssert.File)
  199. fmt.Println()
  200. fmt.Println(test.ScenarioAssert.AutoGenAssertData)
  201. }
  202. if !noClean {
  203. if err := test.Clean(); err != nil {
  204. return fmt.Errorf("unable to clean test '%s' env: %s", test.Name, err)
  205. }
  206. }
  207. fmt.Printf("\nPlease fill your assert file(s) for test '%s', exiting\n", test.Name)
  208. os.Exit(1)
  209. }
  210. testResult[test.Name] = test.Success
  211. if test.Success {
  212. if csConfig.Cscli.Output == "human" {
  213. log.Infof("Test '%s' passed successfully (%d assertions)\n", test.Name, test.ParserAssert.NbAssert+test.ScenarioAssert.NbAssert)
  214. }
  215. if !noClean {
  216. if err := test.Clean(); err != nil {
  217. return fmt.Errorf("unable to clean test '%s' env: %s", test.Name, err)
  218. }
  219. }
  220. } else {
  221. success = false
  222. cleanTestEnv := false
  223. if csConfig.Cscli.Output == "human" {
  224. if len(test.ParserAssert.Fails) > 0 {
  225. fmt.Println()
  226. log.Errorf("Parser test '%s' failed (%d errors)\n", test.Name, len(test.ParserAssert.Fails))
  227. for _, fail := range test.ParserAssert.Fails {
  228. fmt.Printf("(L.%d) %s => %s\n", fail.Line, emoji.RedCircle, fail.Expression)
  229. fmt.Printf(" Actual expression values:\n")
  230. for key, value := range fail.Debug {
  231. fmt.Printf(" %s = '%s'\n", key, strings.TrimSuffix(value, "\n"))
  232. }
  233. fmt.Println()
  234. }
  235. }
  236. if len(test.ScenarioAssert.Fails) > 0 {
  237. fmt.Println()
  238. log.Errorf("Scenario test '%s' failed (%d errors)\n", test.Name, len(test.ScenarioAssert.Fails))
  239. for _, fail := range test.ScenarioAssert.Fails {
  240. fmt.Printf("(L.%d) %s => %s\n", fail.Line, emoji.RedCircle, fail.Expression)
  241. fmt.Printf(" Actual expression values:\n")
  242. for key, value := range fail.Debug {
  243. fmt.Printf(" %s = '%s'\n", key, strings.TrimSuffix(value, "\n"))
  244. }
  245. fmt.Println()
  246. }
  247. }
  248. if !forceClean && !noClean {
  249. prompt := &survey.Confirm{
  250. Message: fmt.Sprintf("\nDo you want to remove runtime folder for test '%s'? (default: Yes)", test.Name),
  251. Default: true,
  252. }
  253. if err := survey.AskOne(prompt, &cleanTestEnv); err != nil {
  254. return fmt.Errorf("unable to ask to remove runtime folder: %s", err)
  255. }
  256. }
  257. }
  258. if cleanTestEnv || forceClean {
  259. if err := test.Clean(); err != nil {
  260. return fmt.Errorf("unable to clean test '%s' env: %s", test.Name, err)
  261. }
  262. }
  263. }
  264. }
  265. if csConfig.Cscli.Output == "human" {
  266. hubTestResultTable(color.Output, testResult)
  267. } else if csConfig.Cscli.Output == "json" {
  268. jsonResult := make(map[string][]string, 0)
  269. jsonResult["success"] = make([]string, 0)
  270. jsonResult["fail"] = make([]string, 0)
  271. for testName, success := range testResult {
  272. if success {
  273. jsonResult["success"] = append(jsonResult["success"], testName)
  274. } else {
  275. jsonResult["fail"] = append(jsonResult["fail"], testName)
  276. }
  277. }
  278. jsonStr, err := json.Marshal(jsonResult)
  279. if err != nil {
  280. return fmt.Errorf("unable to json test result: %s", err)
  281. }
  282. fmt.Println(string(jsonStr))
  283. }
  284. if !success {
  285. os.Exit(1)
  286. }
  287. return nil
  288. },
  289. }
  290. cmdHubTestRun.Flags().BoolVar(&noClean, "no-clean", false, "Don't clean runtime environment if test succeed")
  291. cmdHubTestRun.Flags().BoolVar(&forceClean, "clean", false, "Clean runtime environment if test fail")
  292. cmdHubTestRun.Flags().BoolVar(&runAll, "all", false, "Run all tests")
  293. return cmdHubTestRun
  294. }
  295. func NewHubTestCleanCmd() *cobra.Command {
  296. var cmdHubTestClean = &cobra.Command{
  297. Use: "clean",
  298. Short: "clean [test_name]",
  299. Args: cobra.MinimumNArgs(1),
  300. DisableAutoGenTag: true,
  301. RunE: func(cmd *cobra.Command, args []string) error {
  302. for _, testName := range args {
  303. test, err := HubTest.LoadTestItem(testName)
  304. if err != nil {
  305. return fmt.Errorf("unable to load test '%s': %s", testName, err)
  306. }
  307. if err := test.Clean(); err != nil {
  308. return fmt.Errorf("unable to clean test '%s' env: %s", test.Name, err)
  309. }
  310. }
  311. return nil
  312. },
  313. }
  314. return cmdHubTestClean
  315. }
  316. func NewHubTestInfoCmd() *cobra.Command {
  317. var cmdHubTestInfo = &cobra.Command{
  318. Use: "info",
  319. Short: "info [test_name]",
  320. Args: cobra.MinimumNArgs(1),
  321. DisableAutoGenTag: true,
  322. RunE: func(cmd *cobra.Command, args []string) error {
  323. for _, testName := range args {
  324. test, err := HubTest.LoadTestItem(testName)
  325. if err != nil {
  326. return fmt.Errorf("unable to load test '%s': %s", testName, err)
  327. }
  328. fmt.Println()
  329. fmt.Printf(" Test name : %s\n", test.Name)
  330. fmt.Printf(" Test path : %s\n", test.Path)
  331. fmt.Printf(" Log file : %s\n", filepath.Join(test.Path, test.Config.LogFile))
  332. fmt.Printf(" Parser assertion file : %s\n", filepath.Join(test.Path, hubtest.ParserAssertFileName))
  333. fmt.Printf(" Scenario assertion file : %s\n", filepath.Join(test.Path, hubtest.ScenarioAssertFileName))
  334. fmt.Printf(" Configuration File : %s\n", filepath.Join(test.Path, "config.yaml"))
  335. }
  336. return nil
  337. },
  338. }
  339. return cmdHubTestInfo
  340. }
  341. func NewHubTestListCmd() *cobra.Command {
  342. var cmdHubTestList = &cobra.Command{
  343. Use: "list",
  344. Short: "list",
  345. DisableAutoGenTag: true,
  346. RunE: func(cmd *cobra.Command, args []string) error {
  347. if err := HubTest.LoadAllTests(); err != nil {
  348. return fmt.Errorf("unable to load all tests: %s", err)
  349. }
  350. switch csConfig.Cscli.Output {
  351. case "human":
  352. hubTestListTable(color.Output, HubTest.Tests)
  353. case "json":
  354. j, err := json.MarshalIndent(HubTest.Tests, " ", " ")
  355. if err != nil {
  356. return err
  357. }
  358. fmt.Println(string(j))
  359. default:
  360. return fmt.Errorf("only human/json output modes are supported")
  361. }
  362. return nil
  363. },
  364. }
  365. return cmdHubTestList
  366. }
  367. func NewHubTestCoverageCmd() *cobra.Command {
  368. var showParserCov bool
  369. var showScenarioCov bool
  370. var showOnlyPercent bool
  371. var cmdHubTestCoverage = &cobra.Command{
  372. Use: "coverage",
  373. Short: "coverage",
  374. DisableAutoGenTag: true,
  375. RunE: func(cmd *cobra.Command, args []string) error {
  376. if err := HubTest.LoadAllTests(); err != nil {
  377. return fmt.Errorf("unable to load all tests: %+v", err)
  378. }
  379. var err error
  380. scenarioCoverage := []hubtest.ScenarioCoverage{}
  381. parserCoverage := []hubtest.ParserCoverage{}
  382. scenarioCoveragePercent := 0
  383. parserCoveragePercent := 0
  384. // if both are false (flag by default), show both
  385. showAll := !showScenarioCov && !showParserCov
  386. if showParserCov || showAll {
  387. parserCoverage, err = HubTest.GetParsersCoverage()
  388. if err != nil {
  389. return fmt.Errorf("while getting parser coverage: %s", err)
  390. }
  391. parserTested := 0
  392. for _, test := range parserCoverage {
  393. if test.TestsCount > 0 {
  394. parserTested += 1
  395. }
  396. }
  397. parserCoveragePercent = int(math.Round((float64(parserTested) / float64(len(parserCoverage)) * 100)))
  398. }
  399. if showScenarioCov || showAll {
  400. scenarioCoverage, err = HubTest.GetScenariosCoverage()
  401. if err != nil {
  402. return fmt.Errorf("while getting scenario coverage: %s", err)
  403. }
  404. scenarioTested := 0
  405. for _, test := range scenarioCoverage {
  406. if test.TestsCount > 0 {
  407. scenarioTested += 1
  408. }
  409. }
  410. scenarioCoveragePercent = int(math.Round((float64(scenarioTested) / float64(len(scenarioCoverage)) * 100)))
  411. }
  412. if showOnlyPercent {
  413. if showAll {
  414. fmt.Printf("parsers=%d%%\nscenarios=%d%%", parserCoveragePercent, scenarioCoveragePercent)
  415. } else if showParserCov {
  416. fmt.Printf("parsers=%d%%", parserCoveragePercent)
  417. } else if showScenarioCov {
  418. fmt.Printf("scenarios=%d%%", scenarioCoveragePercent)
  419. }
  420. os.Exit(0)
  421. }
  422. if csConfig.Cscli.Output == "human" {
  423. if showParserCov || showAll {
  424. hubTestParserCoverageTable(color.Output, parserCoverage)
  425. }
  426. if showScenarioCov || showAll {
  427. hubTestScenarioCoverageTable(color.Output, scenarioCoverage)
  428. }
  429. fmt.Println()
  430. if showParserCov || showAll {
  431. fmt.Printf("PARSERS : %d%% of coverage\n", parserCoveragePercent)
  432. }
  433. if showScenarioCov || showAll {
  434. fmt.Printf("SCENARIOS : %d%% of coverage\n", scenarioCoveragePercent)
  435. }
  436. } else if csConfig.Cscli.Output == "json" {
  437. dump, err := json.MarshalIndent(parserCoverage, "", " ")
  438. if err != nil {
  439. return err
  440. }
  441. fmt.Printf("%s", dump)
  442. dump, err = json.MarshalIndent(scenarioCoverage, "", " ")
  443. if err != nil {
  444. return err
  445. }
  446. fmt.Printf("%s", dump)
  447. } else {
  448. return fmt.Errorf("only human/json output modes are supported")
  449. }
  450. return nil
  451. },
  452. }
  453. cmdHubTestCoverage.PersistentFlags().BoolVar(&showOnlyPercent, "percent", false, "Show only percentages of coverage")
  454. cmdHubTestCoverage.PersistentFlags().BoolVar(&showParserCov, "parsers", false, "Show only parsers coverage")
  455. cmdHubTestCoverage.PersistentFlags().BoolVar(&showScenarioCov, "scenarios", false, "Show only scenarios coverage")
  456. return cmdHubTestCoverage
  457. }
  458. func NewHubTestEvalCmd() *cobra.Command {
  459. var evalExpression string
  460. var cmdHubTestEval = &cobra.Command{
  461. Use: "eval",
  462. Short: "eval [test_name]",
  463. Args: cobra.ExactArgs(1),
  464. DisableAutoGenTag: true,
  465. RunE: func(cmd *cobra.Command, args []string) error {
  466. for _, testName := range args {
  467. test, err := HubTest.LoadTestItem(testName)
  468. if err != nil {
  469. return fmt.Errorf("can't load test: %+v", err)
  470. }
  471. err = test.ParserAssert.LoadTest(test.ParserResultFile)
  472. if err != nil {
  473. return fmt.Errorf("can't load test results from '%s': %+v", test.ParserResultFile, err)
  474. }
  475. output, err := test.ParserAssert.EvalExpression(evalExpression)
  476. if err != nil {
  477. return err
  478. }
  479. fmt.Print(output)
  480. }
  481. return nil
  482. },
  483. }
  484. cmdHubTestEval.PersistentFlags().StringVarP(&evalExpression, "expr", "e", "", "Expression to eval")
  485. return cmdHubTestEval
  486. }
  487. func NewHubTestExplainCmd() *cobra.Command {
  488. var cmdHubTestExplain = &cobra.Command{
  489. Use: "explain",
  490. Short: "explain [test_name]",
  491. Args: cobra.ExactArgs(1),
  492. DisableAutoGenTag: true,
  493. RunE: func(cmd *cobra.Command, args []string) error {
  494. for _, testName := range args {
  495. test, err := HubTest.LoadTestItem(testName)
  496. if err != nil {
  497. return fmt.Errorf("can't load test: %+v", err)
  498. }
  499. err = test.ParserAssert.LoadTest(test.ParserResultFile)
  500. if err != nil {
  501. err := test.Run()
  502. if err != nil {
  503. return fmt.Errorf("running test '%s' failed: %+v", test.Name, err)
  504. }
  505. err = test.ParserAssert.LoadTest(test.ParserResultFile)
  506. if err != nil {
  507. return fmt.Errorf("unable to load parser result after run: %s", err)
  508. }
  509. }
  510. err = test.ScenarioAssert.LoadTest(test.ScenarioResultFile, test.BucketPourResultFile)
  511. if err != nil {
  512. err := test.Run()
  513. if err != nil {
  514. return fmt.Errorf("running test '%s' failed: %+v", test.Name, err)
  515. }
  516. err = test.ScenarioAssert.LoadTest(test.ScenarioResultFile, test.BucketPourResultFile)
  517. if err != nil {
  518. return fmt.Errorf("unable to load scenario result after run: %s", err)
  519. }
  520. }
  521. opts := hubtest.DumpOpts{}
  522. hubtest.DumpTree(*test.ParserAssert.TestData, *test.ScenarioAssert.PourData, opts)
  523. }
  524. return nil
  525. },
  526. }
  527. return cmdHubTestExplain
  528. }