Skip to content

Commit

Permalink
cscli refact: package 'clihubtest' (#3174)
Browse files Browse the repository at this point in the history
* cscli refact: package 'clihubtest'

* split hubtest.go subcommands in files

* extract function getCoverage()

* common function hubTestCoverageTable()

* update cyclomatic lint

* lint
  • Loading branch information
mmetc authored Aug 27, 2024
1 parent ec415ed commit 2e970b3
Show file tree
Hide file tree
Showing 13 changed files with 820 additions and 793 deletions.
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ linters-settings:
disabled: true
- name: cyclomatic
# lower this after refactoring
arguments: [42]
arguments: [41]
- name: defer
disabled: true
- name: empty-block
Expand Down
31 changes: 31 additions & 0 deletions cmd/crowdsec-cli/clihubtest/clean.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package clihubtest

import (
"fmt"

"github.com/spf13/cobra"
)

func (cli *cliHubTest) NewCleanCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "clean",
Short: "clean [test_name]",
Args: cobra.MinimumNArgs(1),
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, args []string) error {
for _, testName := range args {
test, err := hubPtr.LoadTestItem(testName)
if err != nil {
return fmt.Errorf("unable to load test '%s': %w", testName, err)
}
if err := test.Clean(); err != nil {
return fmt.Errorf("unable to clean test '%s' env: %w", test.Name, err)
}
}

return nil
},
}

return cmd
}
166 changes: 166 additions & 0 deletions cmd/crowdsec-cli/clihubtest/coverage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package clihubtest

import (
"encoding/json"
"errors"
"fmt"
"math"

"github.com/fatih/color"
"github.com/spf13/cobra"

"github.com/crowdsecurity/crowdsec/pkg/hubtest"
)

// getCoverage returns the coverage and the percentage of tests that passed
func getCoverage(show bool, getCoverageFunc func() ([]hubtest.Coverage, error)) ([]hubtest.Coverage, int, error) {
if !show {
return nil, 0, nil
}

coverage, err := getCoverageFunc()
if err != nil {
return nil, 0, fmt.Errorf("while getting coverage: %w", err)
}

tested := 0

for _, test := range coverage {
if test.TestsCount > 0 {
tested++
}
}

// keep coverage 0 if there's no tests?
percent := 0
if len(coverage) > 0 {
percent = int(math.Round((float64(tested) / float64(len(coverage)) * 100)))
}

return coverage, percent, nil
}

func (cli *cliHubTest) coverage(showScenarioCov bool, showParserCov bool, showAppsecCov bool, showOnlyPercent bool) error {
cfg := cli.cfg()

// for this one we explicitly don't do for appsec
if err := HubTest.LoadAllTests(); err != nil {
return fmt.Errorf("unable to load all tests: %+v", err)
}

var err error

// if all are false (flag by default), show them
if !showParserCov && !showScenarioCov && !showAppsecCov {
showParserCov = true
showScenarioCov = true
showAppsecCov = true
}

parserCoverage, parserCoveragePercent, err := getCoverage(showParserCov, HubTest.GetParsersCoverage)
if err != nil {
return err
}

scenarioCoverage, scenarioCoveragePercent, err := getCoverage(showScenarioCov, HubTest.GetScenariosCoverage)
if err != nil {
return err
}

appsecRuleCoverage, appsecRuleCoveragePercent, err := getCoverage(showAppsecCov, HubTest.GetAppsecCoverage)
if err != nil {
return err
}

if showOnlyPercent {
switch {
case showParserCov:
fmt.Printf("parsers=%d%%", parserCoveragePercent)
case showScenarioCov:
fmt.Printf("scenarios=%d%%", scenarioCoveragePercent)
case showAppsecCov:
fmt.Printf("appsec_rules=%d%%", appsecRuleCoveragePercent)
}

return nil
}

switch cfg.Cscli.Output {
case "human":
if showParserCov {
hubTestCoverageTable(color.Output, cfg.Cscli.Color, []string{"Parser", "Status", "Number of tests"}, parserCoverage)
}

if showScenarioCov {
hubTestCoverageTable(color.Output, cfg.Cscli.Color, []string{"Scenario", "Status", "Number of tests"}, parserCoverage)
}

if showAppsecCov {
hubTestCoverageTable(color.Output, cfg.Cscli.Color, []string{"Appsec Rule", "Status", "Number of tests"}, parserCoverage)
}

fmt.Println()

if showParserCov {
fmt.Printf("PARSERS : %d%% of coverage\n", parserCoveragePercent)
}

if showScenarioCov {
fmt.Printf("SCENARIOS : %d%% of coverage\n", scenarioCoveragePercent)
}

if showAppsecCov {
fmt.Printf("APPSEC RULES : %d%% of coverage\n", appsecRuleCoveragePercent)
}
case "json":
dump, err := json.MarshalIndent(parserCoverage, "", " ")
if err != nil {
return err
}

fmt.Printf("%s", dump)

dump, err = json.MarshalIndent(scenarioCoverage, "", " ")
if err != nil {
return err
}

fmt.Printf("%s", dump)

dump, err = json.MarshalIndent(appsecRuleCoverage, "", " ")
if err != nil {
return err
}

fmt.Printf("%s", dump)
default:
return errors.New("only human/json output modes are supported")
}

return nil
}

func (cli *cliHubTest) NewCoverageCmd() *cobra.Command {
var (
showParserCov bool
showScenarioCov bool
showOnlyPercent bool
showAppsecCov bool
)

cmd := &cobra.Command{
Use: "coverage",
Short: "coverage",
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error {
return cli.coverage(showScenarioCov, showParserCov, showAppsecCov, showOnlyPercent)
},
}

cmd.PersistentFlags().BoolVar(&showOnlyPercent, "percent", false, "Show only percentages of coverage")
cmd.PersistentFlags().BoolVar(&showParserCov, "parsers", false, "Show only parsers coverage")
cmd.PersistentFlags().BoolVar(&showScenarioCov, "scenarios", false, "Show only scenarios coverage")
cmd.PersistentFlags().BoolVar(&showAppsecCov, "appsec", false, "Show only appsec coverage")

return cmd
}
158 changes: 158 additions & 0 deletions cmd/crowdsec-cli/clihubtest/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package clihubtest

import (
"errors"
"fmt"
"os"
"path/filepath"
"text/template"

"github.com/spf13/cobra"
"gopkg.in/yaml.v3"

"github.com/crowdsecurity/crowdsec/pkg/hubtest"
)

func (cli *cliHubTest) NewCreateCmd() *cobra.Command {
var (
ignoreParsers bool
labels map[string]string
logType string
)

parsers := []string{}
postoverflows := []string{}
scenarios := []string{}

cmd := &cobra.Command{
Use: "create",
Short: "create [test_name]",
Example: `cscli hubtest create my-awesome-test --type syslog
cscli hubtest create my-nginx-custom-test --type nginx
cscli hubtest create my-scenario-test --parsers crowdsecurity/nginx --scenarios crowdsecurity/http-probing`,
Args: cobra.ExactArgs(1),
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, args []string) error {
testName := args[0]
testPath := filepath.Join(hubPtr.HubTestPath, testName)
if _, err := os.Stat(testPath); os.IsExist(err) {
return fmt.Errorf("test '%s' already exists in '%s', exiting", testName, testPath)
}

if isAppsecTest {
logType = "appsec"
}

if logType == "" {
return errors.New("please provide a type (--type) for the test")
}

if err := os.MkdirAll(testPath, os.ModePerm); err != nil {
return fmt.Errorf("unable to create folder '%s': %+v", testPath, err)
}

configFilePath := filepath.Join(testPath, "config.yaml")

configFileData := &hubtest.HubTestItemConfig{}
if logType == "appsec" {
// create empty nuclei template file
nucleiFileName := testName + ".yaml"
nucleiFilePath := filepath.Join(testPath, nucleiFileName)

nucleiFile, err := os.OpenFile(nucleiFilePath, os.O_RDWR|os.O_CREATE, 0o755)
if err != nil {
return err
}

ntpl := template.Must(template.New("nuclei").Parse(hubtest.TemplateNucleiFile))
if ntpl == nil {
return errors.New("unable to parse nuclei template")
}
ntpl.ExecuteTemplate(nucleiFile, "nuclei", struct{ TestName string }{TestName: testName})
nucleiFile.Close()
configFileData.AppsecRules = []string{"./appsec-rules/<author>/your_rule_here.yaml"}
configFileData.NucleiTemplate = nucleiFileName
fmt.Println()
fmt.Printf(" Test name : %s\n", testName)
fmt.Printf(" Test path : %s\n", testPath)
fmt.Printf(" Config File : %s\n", configFilePath)
fmt.Printf(" Nuclei Template : %s\n", nucleiFilePath)
} else {
// create empty log file
logFileName := testName + ".log"
logFilePath := filepath.Join(testPath, logFileName)
logFile, err := os.Create(logFilePath)
if err != nil {
return err
}
logFile.Close()

// create empty parser assertion file
parserAssertFilePath := filepath.Join(testPath, hubtest.ParserAssertFileName)
parserAssertFile, err := os.Create(parserAssertFilePath)
if err != nil {
return err
}
parserAssertFile.Close()
// create empty scenario assertion file
scenarioAssertFilePath := filepath.Join(testPath, hubtest.ScenarioAssertFileName)
scenarioAssertFile, err := os.Create(scenarioAssertFilePath)
if err != nil {
return err
}
scenarioAssertFile.Close()

parsers = append(parsers, "crowdsecurity/syslog-logs")
parsers = append(parsers, "crowdsecurity/dateparse-enrich")

if len(scenarios) == 0 {
scenarios = append(scenarios, "")
}

if len(postoverflows) == 0 {
postoverflows = append(postoverflows, "")
}
configFileData.Parsers = parsers
configFileData.Scenarios = scenarios
configFileData.PostOverflows = postoverflows
configFileData.LogFile = logFileName
configFileData.LogType = logType
configFileData.IgnoreParsers = ignoreParsers
configFileData.Labels = labels
fmt.Println()
fmt.Printf(" Test name : %s\n", testName)
fmt.Printf(" Test path : %s\n", testPath)
fmt.Printf(" Log file : %s (please fill it with logs)\n", logFilePath)
fmt.Printf(" Parser assertion file : %s (please fill it with assertion)\n", parserAssertFilePath)
fmt.Printf(" Scenario assertion file : %s (please fill it with assertion)\n", scenarioAssertFilePath)
fmt.Printf(" Configuration File : %s (please fill it with parsers, scenarios...)\n", configFilePath)
}

fd, err := os.Create(configFilePath)
if err != nil {
return fmt.Errorf("open: %w", err)
}
data, err := yaml.Marshal(configFileData)
if err != nil {
return fmt.Errorf("marshal: %w", err)
}
_, err = fd.Write(data)
if err != nil {
return fmt.Errorf("write: %w", err)
}
if err := fd.Close(); err != nil {
return fmt.Errorf("close: %w", err)
}

return nil
},
}

cmd.PersistentFlags().StringVarP(&logType, "type", "t", "", "Log type of the test")
cmd.Flags().StringSliceVarP(&parsers, "parsers", "p", parsers, "Parsers to add to test")
cmd.Flags().StringSliceVar(&postoverflows, "postoverflows", postoverflows, "Postoverflows to add to test")
cmd.Flags().StringSliceVarP(&scenarios, "scenarios", "s", scenarios, "Scenarios to add to test")
cmd.PersistentFlags().BoolVar(&ignoreParsers, "ignore-parsers", false, "Don't run test on parsers")

return cmd
}
Loading

0 comments on commit 2e970b3

Please sign in to comment.