github.com/crowdsecurity/crowdsec@v1.6.1/cmd/crowdsec-cli/hubtest.go (about)

     1  package main
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"math"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  	"text/template"
    12  
    13  	"github.com/AlecAivazis/survey/v2"
    14  	"github.com/fatih/color"
    15  	log "github.com/sirupsen/logrus"
    16  	"github.com/spf13/cobra"
    17  	"gopkg.in/yaml.v2"
    18  
    19  	"github.com/crowdsecurity/crowdsec/pkg/dumps"
    20  	"github.com/crowdsecurity/crowdsec/pkg/emoji"
    21  	"github.com/crowdsecurity/crowdsec/pkg/hubtest"
    22  )
    23  
    24  var (
    25  	HubTest        hubtest.HubTest
    26  	HubAppsecTests hubtest.HubTest
    27  	hubPtr         *hubtest.HubTest
    28  	isAppsecTest   bool
    29  )
    30  
    31  type cliHubTest struct {
    32  	cfg configGetter
    33  }
    34  
    35  func NewCLIHubTest(cfg configGetter) *cliHubTest {
    36  	return &cliHubTest{
    37  		cfg: cfg,
    38  	}
    39  }
    40  
    41  func (cli *cliHubTest) NewCommand() *cobra.Command {
    42  	var (
    43  		hubPath      string
    44  		crowdsecPath string
    45  		cscliPath    string
    46  	)
    47  
    48  	cmd := &cobra.Command{
    49  		Use:               "hubtest",
    50  		Short:             "Run functional tests on hub configurations",
    51  		Long:              "Run functional tests on hub configurations (parsers, scenarios, collections...)",
    52  		Args:              cobra.ExactArgs(0),
    53  		DisableAutoGenTag: true,
    54  		PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
    55  			var err error
    56  			HubTest, err = hubtest.NewHubTest(hubPath, crowdsecPath, cscliPath, false)
    57  			if err != nil {
    58  				return fmt.Errorf("unable to load hubtest: %+v", err)
    59  			}
    60  
    61  			HubAppsecTests, err = hubtest.NewHubTest(hubPath, crowdsecPath, cscliPath, true)
    62  			if err != nil {
    63  				return fmt.Errorf("unable to load appsec specific hubtest: %+v", err)
    64  			}
    65  
    66  			// commands will use the hubPtr, will point to the default hubTest object, or the one dedicated to appsec tests
    67  			hubPtr = &HubTest
    68  			if isAppsecTest {
    69  				hubPtr = &HubAppsecTests
    70  			}
    71  
    72  			return nil
    73  		},
    74  	}
    75  
    76  	cmd.PersistentFlags().StringVar(&hubPath, "hub", ".", "Path to hub folder")
    77  	cmd.PersistentFlags().StringVar(&crowdsecPath, "crowdsec", "crowdsec", "Path to crowdsec")
    78  	cmd.PersistentFlags().StringVar(&cscliPath, "cscli", "cscli", "Path to cscli")
    79  	cmd.PersistentFlags().BoolVar(&isAppsecTest, "appsec", false, "Command relates to appsec tests")
    80  
    81  	cmd.AddCommand(cli.NewCreateCmd())
    82  	cmd.AddCommand(cli.NewRunCmd())
    83  	cmd.AddCommand(cli.NewCleanCmd())
    84  	cmd.AddCommand(cli.NewInfoCmd())
    85  	cmd.AddCommand(cli.NewListCmd())
    86  	cmd.AddCommand(cli.NewCoverageCmd())
    87  	cmd.AddCommand(cli.NewEvalCmd())
    88  	cmd.AddCommand(cli.NewExplainCmd())
    89  
    90  	return cmd
    91  }
    92  
    93  func (cli *cliHubTest) NewCreateCmd() *cobra.Command {
    94  	var (
    95  		ignoreParsers bool
    96  		labels        map[string]string
    97  		logType       string
    98  	)
    99  
   100  	parsers := []string{}
   101  	postoverflows := []string{}
   102  	scenarios := []string{}
   103  
   104  	cmd := &cobra.Command{
   105  		Use:   "create",
   106  		Short: "create [test_name]",
   107  		Example: `cscli hubtest create my-awesome-test --type syslog
   108  cscli hubtest create my-nginx-custom-test --type nginx
   109  cscli hubtest create my-scenario-test --parsers crowdsecurity/nginx --scenarios crowdsecurity/http-probing`,
   110  		Args:              cobra.ExactArgs(1),
   111  		DisableAutoGenTag: true,
   112  		RunE: func(_ *cobra.Command, args []string) error {
   113  			testName := args[0]
   114  			testPath := filepath.Join(hubPtr.HubTestPath, testName)
   115  			if _, err := os.Stat(testPath); os.IsExist(err) {
   116  				return fmt.Errorf("test '%s' already exists in '%s', exiting", testName, testPath)
   117  			}
   118  
   119  			if isAppsecTest {
   120  				logType = "appsec"
   121  			}
   122  
   123  			if logType == "" {
   124  				return errors.New("please provide a type (--type) for the test")
   125  			}
   126  
   127  			if err := os.MkdirAll(testPath, os.ModePerm); err != nil {
   128  				return fmt.Errorf("unable to create folder '%s': %+v", testPath, err)
   129  			}
   130  
   131  			configFilePath := filepath.Join(testPath, "config.yaml")
   132  
   133  			configFileData := &hubtest.HubTestItemConfig{}
   134  			if logType == "appsec" {
   135  				// create empty nuclei template file
   136  				nucleiFileName := fmt.Sprintf("%s.yaml", testName)
   137  				nucleiFilePath := filepath.Join(testPath, nucleiFileName)
   138  				nucleiFile, err := os.OpenFile(nucleiFilePath, os.O_RDWR|os.O_CREATE, 0755)
   139  				if err != nil {
   140  					return err
   141  				}
   142  
   143  				ntpl := template.Must(template.New("nuclei").Parse(hubtest.TemplateNucleiFile))
   144  				if ntpl == nil {
   145  					return errors.New("unable to parse nuclei template")
   146  				}
   147  				ntpl.ExecuteTemplate(nucleiFile, "nuclei", struct{ TestName string }{TestName: testName})
   148  				nucleiFile.Close()
   149  				configFileData.AppsecRules = []string{"./appsec-rules/<author>/your_rule_here.yaml"}
   150  				configFileData.NucleiTemplate = nucleiFileName
   151  				fmt.Println()
   152  				fmt.Printf("  Test name                   :  %s\n", testName)
   153  				fmt.Printf("  Test path                   :  %s\n", testPath)
   154  				fmt.Printf("  Config File                 :  %s\n", configFilePath)
   155  				fmt.Printf("  Nuclei Template             :  %s\n", nucleiFilePath)
   156  			} else {
   157  				// create empty log file
   158  				logFileName := fmt.Sprintf("%s.log", testName)
   159  				logFilePath := filepath.Join(testPath, logFileName)
   160  				logFile, err := os.Create(logFilePath)
   161  				if err != nil {
   162  					return err
   163  				}
   164  				logFile.Close()
   165  
   166  				// create empty parser assertion file
   167  				parserAssertFilePath := filepath.Join(testPath, hubtest.ParserAssertFileName)
   168  				parserAssertFile, err := os.Create(parserAssertFilePath)
   169  				if err != nil {
   170  					return err
   171  				}
   172  				parserAssertFile.Close()
   173  				// create empty scenario assertion file
   174  				scenarioAssertFilePath := filepath.Join(testPath, hubtest.ScenarioAssertFileName)
   175  				scenarioAssertFile, err := os.Create(scenarioAssertFilePath)
   176  				if err != nil {
   177  					return err
   178  				}
   179  				scenarioAssertFile.Close()
   180  
   181  				parsers = append(parsers, "crowdsecurity/syslog-logs")
   182  				parsers = append(parsers, "crowdsecurity/dateparse-enrich")
   183  
   184  				if len(scenarios) == 0 {
   185  					scenarios = append(scenarios, "")
   186  				}
   187  
   188  				if len(postoverflows) == 0 {
   189  					postoverflows = append(postoverflows, "")
   190  				}
   191  				configFileData.Parsers = parsers
   192  				configFileData.Scenarios = scenarios
   193  				configFileData.PostOverflows = postoverflows
   194  				configFileData.LogFile = logFileName
   195  				configFileData.LogType = logType
   196  				configFileData.IgnoreParsers = ignoreParsers
   197  				configFileData.Labels = labels
   198  				fmt.Println()
   199  				fmt.Printf("  Test name                   :  %s\n", testName)
   200  				fmt.Printf("  Test path                   :  %s\n", testPath)
   201  				fmt.Printf("  Log file                    :  %s (please fill it with logs)\n", logFilePath)
   202  				fmt.Printf("  Parser assertion file       :  %s (please fill it with assertion)\n", parserAssertFilePath)
   203  				fmt.Printf("  Scenario assertion file     :  %s (please fill it with assertion)\n", scenarioAssertFilePath)
   204  				fmt.Printf("  Configuration File          :  %s (please fill it with parsers, scenarios...)\n", configFilePath)
   205  			}
   206  
   207  			fd, err := os.Create(configFilePath)
   208  			if err != nil {
   209  				return fmt.Errorf("open: %w", err)
   210  			}
   211  			data, err := yaml.Marshal(configFileData)
   212  			if err != nil {
   213  				return fmt.Errorf("marshal: %w", err)
   214  			}
   215  			_, err = fd.Write(data)
   216  			if err != nil {
   217  				return fmt.Errorf("write: %w", err)
   218  			}
   219  			if err := fd.Close(); err != nil {
   220  				return fmt.Errorf("close: %w", err)
   221  			}
   222  
   223  			return nil
   224  		},
   225  	}
   226  
   227  	cmd.PersistentFlags().StringVarP(&logType, "type", "t", "", "Log type of the test")
   228  	cmd.Flags().StringSliceVarP(&parsers, "parsers", "p", parsers, "Parsers to add to test")
   229  	cmd.Flags().StringSliceVar(&postoverflows, "postoverflows", postoverflows, "Postoverflows to add to test")
   230  	cmd.Flags().StringSliceVarP(&scenarios, "scenarios", "s", scenarios, "Scenarios to add to test")
   231  	cmd.PersistentFlags().BoolVar(&ignoreParsers, "ignore-parsers", false, "Don't run test on parsers")
   232  
   233  	return cmd
   234  }
   235  
   236  func (cli *cliHubTest) NewRunCmd() *cobra.Command {
   237  	var (
   238  		noClean          bool
   239  		runAll           bool
   240  		forceClean       bool
   241  		NucleiTargetHost string
   242  		AppSecHost       string
   243  	)
   244  
   245  	cmd := &cobra.Command{
   246  		Use:               "run",
   247  		Short:             "run [test_name]",
   248  		DisableAutoGenTag: true,
   249  		RunE: func(cmd *cobra.Command, args []string) error {
   250  			cfg := cli.cfg()
   251  
   252  			if !runAll && len(args) == 0 {
   253  				printHelp(cmd)
   254  				return errors.New("please provide test to run or --all flag")
   255  			}
   256  			hubPtr.NucleiTargetHost = NucleiTargetHost
   257  			hubPtr.AppSecHost = AppSecHost
   258  			if runAll {
   259  				if err := hubPtr.LoadAllTests(); err != nil {
   260  					return fmt.Errorf("unable to load all tests: %+v", err)
   261  				}
   262  			} else {
   263  				for _, testName := range args {
   264  					_, err := hubPtr.LoadTestItem(testName)
   265  					if err != nil {
   266  						return fmt.Errorf("unable to load test '%s': %w", testName, err)
   267  					}
   268  				}
   269  			}
   270  
   271  			// set timezone to avoid DST issues
   272  			os.Setenv("TZ", "UTC")
   273  			for _, test := range hubPtr.Tests {
   274  				if cfg.Cscli.Output == "human" {
   275  					log.Infof("Running test '%s'", test.Name)
   276  				}
   277  				err := test.Run()
   278  				if err != nil {
   279  					log.Errorf("running test '%s' failed: %+v", test.Name, err)
   280  				}
   281  			}
   282  
   283  			return nil
   284  		},
   285  		PersistentPostRunE: func(_ *cobra.Command, _ []string) error {
   286  			cfg := cli.cfg()
   287  
   288  			success := true
   289  			testResult := make(map[string]bool)
   290  			for _, test := range hubPtr.Tests {
   291  				if test.AutoGen && !isAppsecTest {
   292  					if test.ParserAssert.AutoGenAssert {
   293  						log.Warningf("Assert file '%s' is empty, generating assertion:", test.ParserAssert.File)
   294  						fmt.Println()
   295  						fmt.Println(test.ParserAssert.AutoGenAssertData)
   296  					}
   297  					if test.ScenarioAssert.AutoGenAssert {
   298  						log.Warningf("Assert file '%s' is empty, generating assertion:", test.ScenarioAssert.File)
   299  						fmt.Println()
   300  						fmt.Println(test.ScenarioAssert.AutoGenAssertData)
   301  					}
   302  					if !noClean {
   303  						if err := test.Clean(); err != nil {
   304  							return fmt.Errorf("unable to clean test '%s' env: %w", test.Name, err)
   305  						}
   306  					}
   307  					fmt.Printf("\nPlease fill your assert file(s) for test '%s', exiting\n", test.Name)
   308  					os.Exit(1)
   309  				}
   310  				testResult[test.Name] = test.Success
   311  				if test.Success {
   312  					if cfg.Cscli.Output == "human" {
   313  						log.Infof("Test '%s' passed successfully (%d assertions)\n", test.Name, test.ParserAssert.NbAssert+test.ScenarioAssert.NbAssert)
   314  					}
   315  					if !noClean {
   316  						if err := test.Clean(); err != nil {
   317  							return fmt.Errorf("unable to clean test '%s' env: %w", test.Name, err)
   318  						}
   319  					}
   320  				} else {
   321  					success = false
   322  					cleanTestEnv := false
   323  					if cfg.Cscli.Output == "human" {
   324  						if len(test.ParserAssert.Fails) > 0 {
   325  							fmt.Println()
   326  							log.Errorf("Parser test '%s' failed (%d errors)\n", test.Name, len(test.ParserAssert.Fails))
   327  							for _, fail := range test.ParserAssert.Fails {
   328  								fmt.Printf("(L.%d)  %s  => %s\n", fail.Line, emoji.RedCircle, fail.Expression)
   329  								fmt.Printf("        Actual expression values:\n")
   330  								for key, value := range fail.Debug {
   331  									fmt.Printf("            %s = '%s'\n", key, strings.TrimSuffix(value, "\n"))
   332  								}
   333  								fmt.Println()
   334  							}
   335  						}
   336  						if len(test.ScenarioAssert.Fails) > 0 {
   337  							fmt.Println()
   338  							log.Errorf("Scenario test '%s' failed (%d errors)\n", test.Name, len(test.ScenarioAssert.Fails))
   339  							for _, fail := range test.ScenarioAssert.Fails {
   340  								fmt.Printf("(L.%d)  %s  => %s\n", fail.Line, emoji.RedCircle, fail.Expression)
   341  								fmt.Printf("        Actual expression values:\n")
   342  								for key, value := range fail.Debug {
   343  									fmt.Printf("            %s = '%s'\n", key, strings.TrimSuffix(value, "\n"))
   344  								}
   345  								fmt.Println()
   346  							}
   347  						}
   348  						if !forceClean && !noClean {
   349  							prompt := &survey.Confirm{
   350  								Message: fmt.Sprintf("\nDo you want to remove runtime folder for test '%s'? (default: Yes)", test.Name),
   351  								Default: true,
   352  							}
   353  							if err := survey.AskOne(prompt, &cleanTestEnv); err != nil {
   354  								return fmt.Errorf("unable to ask to remove runtime folder: %w", err)
   355  							}
   356  						}
   357  					}
   358  
   359  					if cleanTestEnv || forceClean {
   360  						if err := test.Clean(); err != nil {
   361  							return fmt.Errorf("unable to clean test '%s' env: %w", test.Name, err)
   362  						}
   363  					}
   364  				}
   365  			}
   366  
   367  			switch cfg.Cscli.Output {
   368  			case "human":
   369  				hubTestResultTable(color.Output, testResult)
   370  			case "json":
   371  				jsonResult := make(map[string][]string, 0)
   372  				jsonResult["success"] = make([]string, 0)
   373  				jsonResult["fail"] = make([]string, 0)
   374  				for testName, success := range testResult {
   375  					if success {
   376  						jsonResult["success"] = append(jsonResult["success"], testName)
   377  					} else {
   378  						jsonResult["fail"] = append(jsonResult["fail"], testName)
   379  					}
   380  				}
   381  				jsonStr, err := json.Marshal(jsonResult)
   382  				if err != nil {
   383  					return fmt.Errorf("unable to json test result: %w", err)
   384  				}
   385  				fmt.Println(string(jsonStr))
   386  			default:
   387  				return errors.New("only human/json output modes are supported")
   388  			}
   389  
   390  			if !success {
   391  				os.Exit(1)
   392  			}
   393  
   394  			return nil
   395  		},
   396  	}
   397  
   398  	cmd.Flags().BoolVar(&noClean, "no-clean", false, "Don't clean runtime environment if test succeed")
   399  	cmd.Flags().BoolVar(&forceClean, "clean", false, "Clean runtime environment if test fail")
   400  	cmd.Flags().StringVar(&NucleiTargetHost, "target", hubtest.DefaultNucleiTarget, "Target for AppSec Test")
   401  	cmd.Flags().StringVar(&AppSecHost, "host", hubtest.DefaultAppsecHost, "Address to expose AppSec for hubtest")
   402  	cmd.Flags().BoolVar(&runAll, "all", false, "Run all tests")
   403  
   404  	return cmd
   405  }
   406  
   407  func (cli *cliHubTest) NewCleanCmd() *cobra.Command {
   408  	var cmd = &cobra.Command{
   409  		Use:               "clean",
   410  		Short:             "clean [test_name]",
   411  		Args:              cobra.MinimumNArgs(1),
   412  		DisableAutoGenTag: true,
   413  		RunE: func(_ *cobra.Command, args []string) error {
   414  			for _, testName := range args {
   415  				test, err := hubPtr.LoadTestItem(testName)
   416  				if err != nil {
   417  					return fmt.Errorf("unable to load test '%s': %w", testName, err)
   418  				}
   419  				if err := test.Clean(); err != nil {
   420  					return fmt.Errorf("unable to clean test '%s' env: %w", test.Name, err)
   421  				}
   422  			}
   423  
   424  			return nil
   425  		},
   426  	}
   427  
   428  	return cmd
   429  }
   430  
   431  func (cli *cliHubTest) NewInfoCmd() *cobra.Command {
   432  	cmd := &cobra.Command{
   433  		Use:               "info",
   434  		Short:             "info [test_name]",
   435  		Args:              cobra.MinimumNArgs(1),
   436  		DisableAutoGenTag: true,
   437  		RunE: func(_ *cobra.Command, args []string) error {
   438  			for _, testName := range args {
   439  				test, err := hubPtr.LoadTestItem(testName)
   440  				if err != nil {
   441  					return fmt.Errorf("unable to load test '%s': %w", testName, err)
   442  				}
   443  				fmt.Println()
   444  				fmt.Printf("  Test name                   :  %s\n", test.Name)
   445  				fmt.Printf("  Test path                   :  %s\n", test.Path)
   446  				if isAppsecTest {
   447  					fmt.Printf("  Nuclei Template             :  %s\n", test.Config.NucleiTemplate)
   448  					fmt.Printf("  Appsec Rules                  :  %s\n", strings.Join(test.Config.AppsecRules, ", "))
   449  				} else {
   450  					fmt.Printf("  Log file                    :  %s\n", filepath.Join(test.Path, test.Config.LogFile))
   451  					fmt.Printf("  Parser assertion file       :  %s\n", filepath.Join(test.Path, hubtest.ParserAssertFileName))
   452  					fmt.Printf("  Scenario assertion file     :  %s\n", filepath.Join(test.Path, hubtest.ScenarioAssertFileName))
   453  				}
   454  				fmt.Printf("  Configuration File          :  %s\n", filepath.Join(test.Path, "config.yaml"))
   455  			}
   456  
   457  			return nil
   458  		},
   459  	}
   460  
   461  	return cmd
   462  }
   463  
   464  func (cli *cliHubTest) NewListCmd() *cobra.Command {
   465  	cmd := &cobra.Command{
   466  		Use:               "list",
   467  		Short:             "list",
   468  		DisableAutoGenTag: true,
   469  		RunE: func(_ *cobra.Command, _ []string) error {
   470  			cfg := cli.cfg()
   471  
   472  			if err := hubPtr.LoadAllTests(); err != nil {
   473  				return fmt.Errorf("unable to load all tests: %w", err)
   474  			}
   475  
   476  			switch cfg.Cscli.Output {
   477  			case "human":
   478  				hubTestListTable(color.Output, hubPtr.Tests)
   479  			case "json":
   480  				j, err := json.MarshalIndent(hubPtr.Tests, " ", "  ")
   481  				if err != nil {
   482  					return err
   483  				}
   484  				fmt.Println(string(j))
   485  			default:
   486  				return errors.New("only human/json output modes are supported")
   487  			}
   488  
   489  			return nil
   490  		},
   491  	}
   492  
   493  	return cmd
   494  }
   495  
   496  func (cli *cliHubTest) NewCoverageCmd() *cobra.Command {
   497  	var (
   498  		showParserCov   bool
   499  		showScenarioCov bool
   500  		showOnlyPercent bool
   501  		showAppsecCov   bool
   502  	)
   503  
   504  	cmd := &cobra.Command{
   505  		Use:               "coverage",
   506  		Short:             "coverage",
   507  		DisableAutoGenTag: true,
   508  		RunE: func(_ *cobra.Command, _ []string) error {
   509  			cfg := cli.cfg()
   510  
   511  			// for this one we explicitly don't do for appsec
   512  			if err := HubTest.LoadAllTests(); err != nil {
   513  				return fmt.Errorf("unable to load all tests: %+v", err)
   514  			}
   515  			var err error
   516  			scenarioCoverage := []hubtest.Coverage{}
   517  			parserCoverage := []hubtest.Coverage{}
   518  			appsecRuleCoverage := []hubtest.Coverage{}
   519  			scenarioCoveragePercent := 0
   520  			parserCoveragePercent := 0
   521  			appsecRuleCoveragePercent := 0
   522  
   523  			// if both are false (flag by default), show both
   524  			showAll := !showScenarioCov && !showParserCov && !showAppsecCov
   525  
   526  			if showParserCov || showAll {
   527  				parserCoverage, err = HubTest.GetParsersCoverage()
   528  				if err != nil {
   529  					return fmt.Errorf("while getting parser coverage: %w", err)
   530  				}
   531  				parserTested := 0
   532  				for _, test := range parserCoverage {
   533  					if test.TestsCount > 0 {
   534  						parserTested++
   535  					}
   536  				}
   537  				parserCoveragePercent = int(math.Round((float64(parserTested) / float64(len(parserCoverage)) * 100)))
   538  			}
   539  
   540  			if showScenarioCov || showAll {
   541  				scenarioCoverage, err = HubTest.GetScenariosCoverage()
   542  				if err != nil {
   543  					return fmt.Errorf("while getting scenario coverage: %w", err)
   544  				}
   545  
   546  				scenarioTested := 0
   547  				for _, test := range scenarioCoverage {
   548  					if test.TestsCount > 0 {
   549  						scenarioTested++
   550  					}
   551  				}
   552  
   553  				scenarioCoveragePercent = int(math.Round((float64(scenarioTested) / float64(len(scenarioCoverage)) * 100)))
   554  			}
   555  
   556  			if showAppsecCov || showAll {
   557  				appsecRuleCoverage, err = HubTest.GetAppsecCoverage()
   558  				if err != nil {
   559  					return fmt.Errorf("while getting scenario coverage: %w", err)
   560  				}
   561  
   562  				appsecRuleTested := 0
   563  				for _, test := range appsecRuleCoverage {
   564  					if test.TestsCount > 0 {
   565  						appsecRuleTested++
   566  					}
   567  				}
   568  				appsecRuleCoveragePercent = int(math.Round((float64(appsecRuleTested) / float64(len(appsecRuleCoverage)) * 100)))
   569  			}
   570  
   571  			if showOnlyPercent {
   572  				switch {
   573  				case showAll:
   574  					fmt.Printf("parsers=%d%%\nscenarios=%d%%\nappsec_rules=%d%%", parserCoveragePercent, scenarioCoveragePercent, appsecRuleCoveragePercent)
   575  				case showParserCov:
   576  					fmt.Printf("parsers=%d%%", parserCoveragePercent)
   577  				case showScenarioCov:
   578  					fmt.Printf("scenarios=%d%%", scenarioCoveragePercent)
   579  				case showAppsecCov:
   580  					fmt.Printf("appsec_rules=%d%%", appsecRuleCoveragePercent)
   581  				}
   582  				os.Exit(0)
   583  			}
   584  
   585  			switch cfg.Cscli.Output {
   586  			case "human":
   587  				if showParserCov || showAll {
   588  					hubTestParserCoverageTable(color.Output, parserCoverage)
   589  				}
   590  
   591  				if showScenarioCov || showAll {
   592  					hubTestScenarioCoverageTable(color.Output, scenarioCoverage)
   593  				}
   594  
   595  				if showAppsecCov || showAll {
   596  					hubTestAppsecRuleCoverageTable(color.Output, appsecRuleCoverage)
   597  				}
   598  
   599  				fmt.Println()
   600  				if showParserCov || showAll {
   601  					fmt.Printf("PARSERS    : %d%% of coverage\n", parserCoveragePercent)
   602  				}
   603  				if showScenarioCov || showAll {
   604  					fmt.Printf("SCENARIOS  : %d%% of coverage\n", scenarioCoveragePercent)
   605  				}
   606  				if showAppsecCov || showAll {
   607  					fmt.Printf("APPSEC RULES  : %d%% of coverage\n", appsecRuleCoveragePercent)
   608  				}
   609  			case "json":
   610  				dump, err := json.MarshalIndent(parserCoverage, "", " ")
   611  				if err != nil {
   612  					return err
   613  				}
   614  				fmt.Printf("%s", dump)
   615  				dump, err = json.MarshalIndent(scenarioCoverage, "", " ")
   616  				if err != nil {
   617  					return err
   618  				}
   619  				fmt.Printf("%s", dump)
   620  				dump, err = json.MarshalIndent(appsecRuleCoverage, "", " ")
   621  				if err != nil {
   622  					return err
   623  				}
   624  				fmt.Printf("%s", dump)
   625  			default:
   626  				return errors.New("only human/json output modes are supported")
   627  			}
   628  
   629  			return nil
   630  		},
   631  	}
   632  
   633  	cmd.PersistentFlags().BoolVar(&showOnlyPercent, "percent", false, "Show only percentages of coverage")
   634  	cmd.PersistentFlags().BoolVar(&showParserCov, "parsers", false, "Show only parsers coverage")
   635  	cmd.PersistentFlags().BoolVar(&showScenarioCov, "scenarios", false, "Show only scenarios coverage")
   636  	cmd.PersistentFlags().BoolVar(&showAppsecCov, "appsec", false, "Show only appsec coverage")
   637  
   638  	return cmd
   639  }
   640  
   641  func (cli *cliHubTest) NewEvalCmd() *cobra.Command {
   642  	var evalExpression string
   643  
   644  	cmd := &cobra.Command{
   645  		Use:               "eval",
   646  		Short:             "eval [test_name]",
   647  		Args:              cobra.ExactArgs(1),
   648  		DisableAutoGenTag: true,
   649  		RunE: func(_ *cobra.Command, args []string) error {
   650  			for _, testName := range args {
   651  				test, err := hubPtr.LoadTestItem(testName)
   652  				if err != nil {
   653  					return fmt.Errorf("can't load test: %+v", err)
   654  				}
   655  
   656  				err = test.ParserAssert.LoadTest(test.ParserResultFile)
   657  				if err != nil {
   658  					return fmt.Errorf("can't load test results from '%s': %+v", test.ParserResultFile, err)
   659  				}
   660  
   661  				output, err := test.ParserAssert.EvalExpression(evalExpression)
   662  				if err != nil {
   663  					return err
   664  				}
   665  
   666  				fmt.Print(output)
   667  			}
   668  
   669  			return nil
   670  		},
   671  	}
   672  
   673  	cmd.PersistentFlags().StringVarP(&evalExpression, "expr", "e", "", "Expression to eval")
   674  
   675  	return cmd
   676  }
   677  
   678  func (cli *cliHubTest) NewExplainCmd() *cobra.Command {
   679  	cmd := &cobra.Command{
   680  		Use:               "explain",
   681  		Short:             "explain [test_name]",
   682  		Args:              cobra.ExactArgs(1),
   683  		DisableAutoGenTag: true,
   684  		RunE: func(_ *cobra.Command, args []string) error {
   685  			for _, testName := range args {
   686  				test, err := HubTest.LoadTestItem(testName)
   687  				if err != nil {
   688  					return fmt.Errorf("can't load test: %+v", err)
   689  				}
   690  				err = test.ParserAssert.LoadTest(test.ParserResultFile)
   691  				if err != nil {
   692  					if err = test.Run(); err != nil {
   693  						return fmt.Errorf("running test '%s' failed: %+v", test.Name, err)
   694  					}
   695  
   696  					if err = test.ParserAssert.LoadTest(test.ParserResultFile); err != nil {
   697  						return fmt.Errorf("unable to load parser result after run: %w", err)
   698  					}
   699  				}
   700  
   701  				err = test.ScenarioAssert.LoadTest(test.ScenarioResultFile, test.BucketPourResultFile)
   702  				if err != nil {
   703  					if err = test.Run(); err != nil {
   704  						return fmt.Errorf("running test '%s' failed: %+v", test.Name, err)
   705  					}
   706  
   707  					if err = test.ScenarioAssert.LoadTest(test.ScenarioResultFile, test.BucketPourResultFile); err != nil {
   708  						return fmt.Errorf("unable to load scenario result after run: %w", err)
   709  					}
   710  				}
   711  				opts := dumps.DumpOpts{}
   712  				dumps.DumpTree(*test.ParserAssert.TestData, *test.ScenarioAssert.PourData, opts)
   713  			}
   714  
   715  			return nil
   716  		},
   717  	}
   718  
   719  	return cmd
   720  }