merge hub branch

This commit is contained in:
Sebastien Blot 2023-11-08 20:25:42 +01:00
commit 694028f769
No known key found for this signature in database
GPG key ID: DFC2902F40449F6A
31 changed files with 661 additions and 387 deletions

View file

@ -41,4 +41,4 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
tag_name="${GITHUB_REF##*/}" tag_name="${GITHUB_REF##*/}"
hub release edit -a crowdsec-release.tgz -a vendor.tgz -a *-vendor.tar.xz -m "" "$tag_name" gh release upload "$tag_name" crowdsec-release.tgz vendor.tgz *-vendor.tar.xz

View file

@ -22,6 +22,7 @@ type OldAPICfg struct {
} }
// it's a rip of the cli version, but in silent-mode // it's a rip of the cli version, but in silent-mode
// XXX: redundant, should call InstallItem
func silentInstallItem(hub *cwhub.Hub, name, obtype string) (string, error) { func silentInstallItem(hub *cwhub.Hub, name, obtype string) (string, error) {
var item = hub.GetItem(obtype, name) var item = hub.GetItem(obtype, name)
if item == nil { if item == nil {

View file

@ -1,7 +1,6 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"github.com/fatih/color" "github.com/fatih/color"
@ -94,16 +93,7 @@ func runHubUpdate(cmd *cobra.Command, args []string) error {
// don't use require.Hub because if there is no index file, it would fail // don't use require.Hub because if there is no index file, it would fail
hub, err := cwhub.NewHub(local, remote, true) hub, err := cwhub.NewHub(local, remote, true)
if err != nil { if err != nil {
// XXX: this should be done when downloading items, too return fmt.Errorf("failed to update hub: %w", err)
// but what is the fallback to master actually solving?
if !errors.Is(err, cwhub.ErrIndexNotFound) {
return fmt.Errorf("failed to get Hub index: %w", err)
}
log.Warnf("Could not find index file for branch '%s', using 'master'", remote.Branch)
remote.Branch = "master"
if hub, err = cwhub.NewHub(local, remote, true); err != nil {
return fmt.Errorf("failed to get Hub index after retry: %w", err)
}
} }
// use LocalSync to get warnings about tainted / outdated items // use LocalSync to get warnings about tainted / outdated items

View file

@ -18,9 +18,7 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/hubtest" "github.com/crowdsecurity/crowdsec/pkg/hubtest"
) )
var ( var HubTest hubtest.HubTest
HubTest hubtest.HubTest
)
func NewHubTestCmd() *cobra.Command { func NewHubTestCmd() *cobra.Command {
var hubPath string var hubPath string
@ -43,6 +41,7 @@ func NewHubTestCmd() *cobra.Command {
return nil return nil
}, },
} }
cmdHubTest.PersistentFlags().StringVar(&hubPath, "hub", ".", "Path to hub folder") cmdHubTest.PersistentFlags().StringVar(&hubPath, "hub", ".", "Path to hub folder")
cmdHubTest.PersistentFlags().StringVar(&crowdsecPath, "crowdsec", "crowdsec", "Path to crowdsec") cmdHubTest.PersistentFlags().StringVar(&crowdsecPath, "crowdsec", "crowdsec", "Path to crowdsec")
cmdHubTest.PersistentFlags().StringVar(&cscliPath, "cscli", "cscli", "Path to cscli") cmdHubTest.PersistentFlags().StringVar(&cscliPath, "cscli", "cscli", "Path to cscli")
@ -59,7 +58,6 @@ func NewHubTestCmd() *cobra.Command {
return cmdHubTest return cmdHubTest
} }
func NewHubTestCreateCmd() *cobra.Command { func NewHubTestCreateCmd() *cobra.Command {
parsers := []string{} parsers := []string{}
postoverflows := []string{} postoverflows := []string{}
@ -164,6 +162,7 @@ cscli hubtest create my-scenario-test --parsers crowdsecurity/nginx --scenarios
return nil return nil
}, },
} }
cmdHubTestCreate.PersistentFlags().StringVarP(&logType, "type", "t", "", "Log type of the test") cmdHubTestCreate.PersistentFlags().StringVarP(&logType, "type", "t", "", "Log type of the test")
cmdHubTestCreate.Flags().StringSliceVarP(&parsers, "parsers", "p", parsers, "Parsers to add to test") cmdHubTestCreate.Flags().StringSliceVarP(&parsers, "parsers", "p", parsers, "Parsers to add to test")
cmdHubTestCreate.Flags().StringSliceVar(&postoverflows, "postoverflows", postoverflows, "Postoverflows to add to test") cmdHubTestCreate.Flags().StringSliceVar(&postoverflows, "postoverflows", postoverflows, "Postoverflows to add to test")
@ -173,7 +172,6 @@ cscli hubtest create my-scenario-test --parsers crowdsecurity/nginx --scenarios
return cmdHubTestCreate return cmdHubTestCreate
} }
func NewHubTestRunCmd() *cobra.Command { func NewHubTestRunCmd() *cobra.Command {
var noClean bool var noClean bool
var runAll bool var runAll bool
@ -186,7 +184,7 @@ func NewHubTestRunCmd() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if !runAll && len(args) == 0 { if !runAll && len(args) == 0 {
printHelp(cmd) printHelp(cmd)
return fmt.Errorf("Please provide test to run or --all flag") return fmt.Errorf("please provide test to run or --all flag")
} }
if runAll { if runAll {
@ -202,6 +200,9 @@ func NewHubTestRunCmd() *cobra.Command {
} }
} }
// set timezone to avoid DST issues
os.Setenv("TZ", "UTC")
for _, test := range HubTest.Tests { for _, test := range HubTest.Tests {
if csConfig.Cscli.Output == "human" { if csConfig.Cscli.Output == "human" {
log.Infof("Running test '%s'", test.Name) log.Infof("Running test '%s'", test.Name)
@ -293,9 +294,11 @@ func NewHubTestRunCmd() *cobra.Command {
} }
} }
} }
if csConfig.Cscli.Output == "human" {
switch csConfig.Cscli.Output {
case "human":
hubTestResultTable(color.Output, testResult) hubTestResultTable(color.Output, testResult)
} else if csConfig.Cscli.Output == "json" { case "json":
jsonResult := make(map[string][]string, 0) jsonResult := make(map[string][]string, 0)
jsonResult["success"] = make([]string, 0) jsonResult["success"] = make([]string, 0)
jsonResult["fail"] = make([]string, 0) jsonResult["fail"] = make([]string, 0)
@ -311,6 +314,8 @@ func NewHubTestRunCmd() *cobra.Command {
return fmt.Errorf("unable to json test result: %s", err) return fmt.Errorf("unable to json test result: %s", err)
} }
fmt.Println(string(jsonStr)) fmt.Println(string(jsonStr))
default:
return fmt.Errorf("only human/json output modes are supported")
} }
if !success { if !success {
@ -320,6 +325,7 @@ func NewHubTestRunCmd() *cobra.Command {
return nil return nil
}, },
} }
cmdHubTestRun.Flags().BoolVar(&noClean, "no-clean", false, "Don't clean runtime environment if test succeed") cmdHubTestRun.Flags().BoolVar(&noClean, "no-clean", false, "Don't clean runtime environment if test succeed")
cmdHubTestRun.Flags().BoolVar(&forceClean, "clean", false, "Clean runtime environment if test fail") cmdHubTestRun.Flags().BoolVar(&forceClean, "clean", false, "Clean runtime environment if test fail")
cmdHubTestRun.Flags().BoolVar(&runAll, "all", false, "Run all tests") cmdHubTestRun.Flags().BoolVar(&runAll, "all", false, "Run all tests")
@ -327,7 +333,6 @@ func NewHubTestRunCmd() *cobra.Command {
return cmdHubTestRun return cmdHubTestRun
} }
func NewHubTestCleanCmd() *cobra.Command { func NewHubTestCleanCmd() *cobra.Command {
var cmdHubTestClean = &cobra.Command{ var cmdHubTestClean = &cobra.Command{
Use: "clean", Use: "clean",
@ -352,7 +357,6 @@ func NewHubTestCleanCmd() *cobra.Command {
return cmdHubTestClean return cmdHubTestClean
} }
func NewHubTestInfoCmd() *cobra.Command { func NewHubTestInfoCmd() *cobra.Command {
var cmdHubTestInfo = &cobra.Command{ var cmdHubTestInfo = &cobra.Command{
Use: "info", Use: "info",
@ -381,7 +385,6 @@ func NewHubTestInfoCmd() *cobra.Command {
return cmdHubTestInfo return cmdHubTestInfo
} }
func NewHubTestListCmd() *cobra.Command { func NewHubTestListCmd() *cobra.Command {
var cmdHubTestList = &cobra.Command{ var cmdHubTestList = &cobra.Command{
Use: "list", Use: "list",
@ -412,7 +415,6 @@ func NewHubTestListCmd() *cobra.Command {
return cmdHubTestList return cmdHubTestList
} }
func NewHubTestCoverageCmd() *cobra.Command { func NewHubTestCoverageCmd() *cobra.Command {
var showParserCov bool var showParserCov bool
var showScenarioCov bool var showScenarioCov bool
@ -427,8 +429,8 @@ func NewHubTestCoverageCmd() *cobra.Command {
return fmt.Errorf("unable to load all tests: %+v", err) return fmt.Errorf("unable to load all tests: %+v", err)
} }
var err error var err error
scenarioCoverage := []hubtest.ScenarioCoverage{} scenarioCoverage := []hubtest.Coverage{}
parserCoverage := []hubtest.ParserCoverage{} parserCoverage := []hubtest.Coverage{}
scenarioCoveragePercent := 0 scenarioCoveragePercent := 0
parserCoveragePercent := 0 parserCoveragePercent := 0
@ -443,7 +445,7 @@ func NewHubTestCoverageCmd() *cobra.Command {
parserTested := 0 parserTested := 0
for _, test := range parserCoverage { for _, test := range parserCoverage {
if test.TestsCount > 0 { if test.TestsCount > 0 {
parserTested += 1 parserTested++
} }
} }
parserCoveragePercent = int(math.Round((float64(parserTested) / float64(len(parserCoverage)) * 100))) parserCoveragePercent = int(math.Round((float64(parserTested) / float64(len(parserCoverage)) * 100)))
@ -454,12 +456,14 @@ func NewHubTestCoverageCmd() *cobra.Command {
if err != nil { if err != nil {
return fmt.Errorf("while getting scenario coverage: %s", err) return fmt.Errorf("while getting scenario coverage: %s", err)
} }
scenarioTested := 0 scenarioTested := 0
for _, test := range scenarioCoverage { for _, test := range scenarioCoverage {
if test.TestsCount > 0 { if test.TestsCount > 0 {
scenarioTested += 1 scenarioTested++
} }
} }
scenarioCoveragePercent = int(math.Round((float64(scenarioTested) / float64(len(scenarioCoverage)) * 100))) scenarioCoveragePercent = int(math.Round((float64(scenarioTested) / float64(len(scenarioCoverage)) * 100)))
} }
@ -474,7 +478,8 @@ func NewHubTestCoverageCmd() *cobra.Command {
os.Exit(0) os.Exit(0)
} }
if csConfig.Cscli.Output == "human" { switch csConfig.Cscli.Output {
case "human":
if showParserCov || showAll { if showParserCov || showAll {
hubTestParserCoverageTable(color.Output, parserCoverage) hubTestParserCoverageTable(color.Output, parserCoverage)
} }
@ -489,7 +494,7 @@ func NewHubTestCoverageCmd() *cobra.Command {
if showScenarioCov || showAll { if showScenarioCov || showAll {
fmt.Printf("SCENARIOS : %d%% of coverage\n", scenarioCoveragePercent) fmt.Printf("SCENARIOS : %d%% of coverage\n", scenarioCoveragePercent)
} }
} else if csConfig.Cscli.Output == "json" { case "json":
dump, err := json.MarshalIndent(parserCoverage, "", " ") dump, err := json.MarshalIndent(parserCoverage, "", " ")
if err != nil { if err != nil {
return err return err
@ -500,13 +505,14 @@ func NewHubTestCoverageCmd() *cobra.Command {
return err return err
} }
fmt.Printf("%s", dump) fmt.Printf("%s", dump)
} else { default:
return fmt.Errorf("only human/json output modes are supported") return fmt.Errorf("only human/json output modes are supported")
} }
return nil return nil
}, },
} }
cmdHubTestCoverage.PersistentFlags().BoolVar(&showOnlyPercent, "percent", false, "Show only percentages of coverage") cmdHubTestCoverage.PersistentFlags().BoolVar(&showOnlyPercent, "percent", false, "Show only percentages of coverage")
cmdHubTestCoverage.PersistentFlags().BoolVar(&showParserCov, "parsers", false, "Show only parsers coverage") cmdHubTestCoverage.PersistentFlags().BoolVar(&showParserCov, "parsers", false, "Show only parsers coverage")
cmdHubTestCoverage.PersistentFlags().BoolVar(&showScenarioCov, "scenarios", false, "Show only scenarios coverage") cmdHubTestCoverage.PersistentFlags().BoolVar(&showScenarioCov, "scenarios", false, "Show only scenarios coverage")
@ -514,7 +520,6 @@ func NewHubTestCoverageCmd() *cobra.Command {
return cmdHubTestCoverage return cmdHubTestCoverage
} }
func NewHubTestEvalCmd() *cobra.Command { func NewHubTestEvalCmd() *cobra.Command {
var evalExpression string var evalExpression string
var cmdHubTestEval = &cobra.Command{ var cmdHubTestEval = &cobra.Command{
@ -528,26 +533,29 @@ func NewHubTestEvalCmd() *cobra.Command {
if err != nil { if err != nil {
return fmt.Errorf("can't load test: %+v", err) return fmt.Errorf("can't load test: %+v", err)
} }
err = test.ParserAssert.LoadTest(test.ParserResultFile) err = test.ParserAssert.LoadTest(test.ParserResultFile)
if err != nil { if err != nil {
return fmt.Errorf("can't load test results from '%s': %+v", test.ParserResultFile, err) return fmt.Errorf("can't load test results from '%s': %+v", test.ParserResultFile, err)
} }
output, err := test.ParserAssert.EvalExpression(evalExpression) output, err := test.ParserAssert.EvalExpression(evalExpression)
if err != nil { if err != nil {
return err return err
} }
fmt.Print(output) fmt.Print(output)
} }
return nil return nil
}, },
} }
cmdHubTestEval.PersistentFlags().StringVarP(&evalExpression, "expr", "e", "", "Expression to eval") cmdHubTestEval.PersistentFlags().StringVarP(&evalExpression, "expr", "e", "", "Expression to eval")
return cmdHubTestEval return cmdHubTestEval
} }
func NewHubTestExplainCmd() *cobra.Command { func NewHubTestExplainCmd() *cobra.Command {
var cmdHubTestExplain = &cobra.Command{ var cmdHubTestExplain = &cobra.Command{
Use: "explain", Use: "explain",
@ -562,24 +570,22 @@ func NewHubTestExplainCmd() *cobra.Command {
} }
err = test.ParserAssert.LoadTest(test.ParserResultFile) err = test.ParserAssert.LoadTest(test.ParserResultFile)
if err != nil { if err != nil {
err := test.Run() if err = test.Run(); err != nil {
if err != nil {
return fmt.Errorf("running test '%s' failed: %+v", test.Name, err) return fmt.Errorf("running test '%s' failed: %+v", test.Name, err)
} }
err = test.ParserAssert.LoadTest(test.ParserResultFile)
if err != nil { if err = test.ParserAssert.LoadTest(test.ParserResultFile); err != nil {
return fmt.Errorf("unable to load parser result after run: %s", err) return fmt.Errorf("unable to load parser result after run: %s", err)
} }
} }
err = test.ScenarioAssert.LoadTest(test.ScenarioResultFile, test.BucketPourResultFile) err = test.ScenarioAssert.LoadTest(test.ScenarioResultFile, test.BucketPourResultFile)
if err != nil { if err != nil {
err := test.Run() if err = test.Run(); err != nil {
if err != nil {
return fmt.Errorf("running test '%s' failed: %+v", test.Name, err) return fmt.Errorf("running test '%s' failed: %+v", test.Name, err)
} }
err = test.ScenarioAssert.LoadTest(test.ScenarioResultFile, test.BucketPourResultFile)
if err != nil { if err = test.ScenarioAssert.LoadTest(test.ScenarioResultFile, test.BucketPourResultFile); err != nil {
return fmt.Errorf("unable to load scenario result after run: %s", err) return fmt.Errorf("unable to load scenario result after run: %s", err)
} }
} }

View file

@ -41,39 +41,41 @@ func hubTestListTable(out io.Writer, tests []*hubtest.HubTestItem) {
t.Render() t.Render()
} }
func hubTestParserCoverageTable(out io.Writer, coverage []hubtest.ParserCoverage) { func hubTestParserCoverageTable(out io.Writer, coverage []hubtest.Coverage) {
t := newLightTable(out) t := newLightTable(out)
t.SetHeaders("Parser", "Status", "Number of tests") t.SetHeaders("Parser", "Status", "Number of tests")
t.SetHeaderAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft) t.SetHeaderAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft)
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft) t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft)
parserTested := 0 parserTested := 0
for _, test := range coverage { for _, test := range coverage {
status := emoji.RedCircle.String() status := emoji.RedCircle.String()
if test.TestsCount > 0 { if test.TestsCount > 0 {
status = emoji.GreenCircle.String() status = emoji.GreenCircle.String()
parserTested++ parserTested++
} }
t.AddRow(test.Parser, status, fmt.Sprintf("%d times (across %d tests)", test.TestsCount, len(test.PresentIn))) t.AddRow(test.Name, status, fmt.Sprintf("%d times (across %d tests)", test.TestsCount, len(test.PresentIn)))
} }
t.Render() t.Render()
} }
func hubTestScenarioCoverageTable(out io.Writer, coverage []hubtest.ScenarioCoverage) { func hubTestScenarioCoverageTable(out io.Writer, coverage []hubtest.Coverage) {
t := newLightTable(out) t := newLightTable(out)
t.SetHeaders("Scenario", "Status", "Number of tests") t.SetHeaders("Scenario", "Status", "Number of tests")
t.SetHeaderAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft) t.SetHeaderAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft)
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft) t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft)
parserTested := 0 parserTested := 0
for _, test := range coverage { for _, test := range coverage {
status := emoji.RedCircle.String() status := emoji.RedCircle.String()
if test.TestsCount > 0 { if test.TestsCount > 0 {
status = emoji.GreenCircle.String() status = emoji.GreenCircle.String()
parserTested++ parserTested++
} }
t.AddRow(test.Scenario, status, fmt.Sprintf("%d times (across %d tests)", test.TestsCount, len(test.PresentIn))) t.AddRow(test.Name, status, fmt.Sprintf("%d times (across %d tests)", test.TestsCount, len(test.PresentIn)))
} }
t.Render() t.Render()

View file

@ -19,7 +19,7 @@ import (
) )
// XXX: this should not need hub? // XXX: this should not need hub?
func ShowMetrics(hub *cwhub.Hub, hubItem *cwhub.Item) { func ShowMetrics(hub *cwhub.Hub, hubItem *cwhub.Item) error {
switch hubItem.Type { switch hubItem.Type {
case cwhub.PARSERS: case cwhub.PARSERS:
metrics := GetParserMetric(csConfig.Cscli.PrometheusUrl, hubItem.Name) metrics := GetParserMetric(csConfig.Cscli.PrometheusUrl, hubItem.Name)
@ -28,26 +28,29 @@ func ShowMetrics(hub *cwhub.Hub, hubItem *cwhub.Item) {
metrics := GetScenarioMetric(csConfig.Cscli.PrometheusUrl, hubItem.Name) metrics := GetScenarioMetric(csConfig.Cscli.PrometheusUrl, hubItem.Name)
scenarioMetricsTable(color.Output, hubItem.Name, metrics) scenarioMetricsTable(color.Output, hubItem.Name, metrics)
case cwhub.COLLECTIONS: case cwhub.COLLECTIONS:
for _, item := range hubItem.Parsers { for _, parserName := range hubItem.Parsers {
metrics := GetParserMetric(csConfig.Cscli.PrometheusUrl, item) metrics := GetParserMetric(csConfig.Cscli.PrometheusUrl, parserName)
parserMetricsTable(color.Output, item, metrics) parserMetricsTable(color.Output, parserName, metrics)
} }
for _, item := range hubItem.Scenarios { for _, scenarioName := range hubItem.Scenarios {
metrics := GetScenarioMetric(csConfig.Cscli.PrometheusUrl, item) metrics := GetScenarioMetric(csConfig.Cscli.PrometheusUrl, scenarioName)
scenarioMetricsTable(color.Output, item, metrics) scenarioMetricsTable(color.Output, scenarioName, metrics)
} }
for _, item := range hubItem.Collections { for _, collName := range hubItem.Collections {
hubItem = hub.GetItem(cwhub.COLLECTIONS, item) subColl := hub.GetItem(cwhub.COLLECTIONS, collName)
if hubItem == nil { if subColl == nil {
log.Fatalf("unable to retrieve item '%s' from collection '%s'", item, hubItem.Name) return fmt.Errorf("unable to retrieve sub-collection '%s' from '%s'", collName, hubItem.Name)
}
if err := ShowMetrics(hub, subColl); err != nil {
return err
} }
ShowMetrics(hub, hubItem)
} }
case cwhub.WAAP_RULES: case cwhub.WAAP_RULES:
log.Fatalf("FIXME: not implemented yet") log.Fatalf("FIXME: not implemented yet")
default: default:
log.Errorf("item of type '%s' is unknown", hubItem.Type) log.Errorf("item of type '%s' is unknown", hubItem.Type)
} }
return nil
} }
// GetParserMetric is a complete rip from prom2json // GetParserMetric is a complete rip from prom2json

View file

@ -157,7 +157,9 @@ func InspectItem(hub *cwhub.Hub, item *cwhub.Item, showMetrics bool) error {
if csConfig.Cscli.Output == "human" && showMetrics { if csConfig.Cscli.Output == "human" && showMetrics {
fmt.Printf("\nCurrent metrics: \n") fmt.Printf("\nCurrent metrics: \n")
ShowMetrics(hub, item) if err := ShowMetrics(hub, item); err != nil {
return err
}
} }
return nil return nil

View file

@ -23,6 +23,7 @@ func CAPI(c *csconfig.Config) error {
if c.API.Server.OnlineClient == nil { if c.API.Server.OnlineClient == nil {
return fmt.Errorf("no configuration for Central API (CAPI) in '%s'", *c.FilePath) return fmt.Errorf("no configuration for Central API (CAPI) in '%s'", *c.FilePath)
} }
return nil return nil
} }
@ -30,6 +31,7 @@ func PAPI(c *csconfig.Config) error {
if c.API.Server.OnlineClient.Credentials.PapiURL == "" { if c.API.Server.OnlineClient.Credentials.PapiURL == "" {
return fmt.Errorf("no PAPI URL in configuration") return fmt.Errorf("no PAPI URL in configuration")
} }
return nil return nil
} }
@ -45,6 +47,7 @@ func DB(c *csconfig.Config) error {
if err := c.LoadDBConfig(); err != nil { if err := c.LoadDBConfig(); err != nil {
return fmt.Errorf("this command requires direct database access (must be run on the local API machine): %w", err) return fmt.Errorf("this command requires direct database access (must be run on the local API machine): %w", err)
} }
return nil return nil
} }
@ -88,7 +91,7 @@ func Hub(c *csconfig.Config, remote *cwhub.RemoteHubCfg) (*cwhub.Hub, error) {
hub, err := cwhub.NewHub(local, remote, false) hub, err := cwhub.NewHub(local, remote, false)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read Hub index: '%w'. Run 'sudo cscli hub update' to download the index again", err) return nil, fmt.Errorf("failed to read Hub index: %w. Run 'sudo cscli hub update' to download the index again", err)
} }
return hub, nil return hub, nil

View file

@ -15,7 +15,7 @@ import (
) )
type DataSet struct { type DataSet struct {
Data []*types.DataSource `yaml:"data,omitempty"` Data []types.DataSource `yaml:"data,omitempty"`
} }
func downloadFile(url string, destPath string) error { func downloadFile(url string, destPath string) error {
@ -59,7 +59,7 @@ func downloadFile(url string, destPath string) error {
return nil return nil
} }
func GetData(data []*types.DataSource, dataDir string) error { func GetData(data []types.DataSource, dataDir string) error {
for _, dataS := range data { for _, dataS := range data {
destPath := filepath.Join(dataDir, dataS.DestPath) destPath := filepath.Join(dataDir, dataS.DestPath)
log.Infof("downloading data '%s' in '%s'", dataS.SourceURL, destPath) log.Infof("downloading data '%s' in '%s'", dataS.SourceURL, destPath)

View file

@ -1,7 +1,6 @@
package cwhub package cwhub
// Enable/disable items already installed (no downloading here) // Enable/disable items already downloaded
// This file is not named install.go to avoid confusion with the functions in helpers.go
import ( import (
"fmt" "fmt"
@ -11,8 +10,9 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// creates symlink between actual config file at hub.HubDir and hub.ConfigDir // EnableItem creates a symlink between actual config file at hub.HubDir and hub.ConfigDir
// Handles collections recursively // Handles collections recursively
// XXX: called from config_restore otherwise no need to export
func (h *Hub) EnableItem(target *Item) error { func (h *Hub) EnableItem(target *Item) error {
parentDir := filepath.Clean(h.local.InstallDir + "/" + target.Type + "/" + target.Stage + "/") parentDir := filepath.Clean(h.local.InstallDir + "/" + target.Type + "/" + target.Stage + "/")
@ -42,16 +42,14 @@ func (h *Hub) EnableItem(target *Item) error {
} }
// install sub-items if it's a collection // install sub-items if it's a collection
if target.Type == COLLECTIONS { for _, sub := range target.SubItems() {
for _, sub := range target.SubItems() { val, ok := h.Items[sub.Type][sub.Name]
val, ok := h.Items[sub.Type][sub.Name] if !ok {
if !ok { return fmt.Errorf("required %s %s of %s doesn't exist, abort", sub.Type, sub.Name, target.Name)
return fmt.Errorf("required %s %s of %s doesn't exist, abort", sub.Type, sub.Name, target.Name) }
}
if err := h.EnableItem(&val); err != nil { if err := h.EnableItem(&val); err != nil {
return fmt.Errorf("while installing %s: %w", sub.Name, err) return fmt.Errorf("while installing %s: %w", sub.Name, err)
}
} }
} }
@ -123,32 +121,31 @@ func (h *Hub) DisableItem(target *Item, purge bool, force bool) error {
return fmt.Errorf("%s is tainted, use '--force' to overwrite", target.Name) return fmt.Errorf("%s is tainted, use '--force' to overwrite", target.Name)
} }
// for a COLLECTIONS, disable sub-items // disable sub-items if any - it's a collection
if target.Type == COLLECTIONS { for _, sub := range target.SubItems() {
for _, sub := range target.SubItems() { // XXX: we do this already when syncing, do we really need to do consistency checks here and there?
val, ok := h.Items[sub.Type][sub.Name] val, ok := h.Items[sub.Type][sub.Name]
if !ok { if !ok {
log.Errorf("Referred %s %s in collection %s doesn't exist.", sub.Type, sub.Name, target.Name) log.Errorf("Referred %s %s in collection %s doesn't exist.", sub.Type, sub.Name, target.Name)
continue continue
} }
// check if the item doesn't belong to another collection before removing it // check if the item doesn't belong to another collection before removing it
toRemove := true toRemove := true
for _, collection := range val.BelongsToCollections { for _, collection := range val.BelongsToCollections {
if collection != target.Name { if collection != target.Name {
toRemove = false toRemove = false
break break
}
} }
}
if toRemove { if toRemove {
if err = h.DisableItem(&val, purge, force); err != nil { if err = h.DisableItem(&val, purge, force); err != nil {
return fmt.Errorf("while disabling %s: %w", sub.Name, err) return fmt.Errorf("while disabling %s: %w", sub.Name, err)
}
} else {
log.Infof("%s was not removed because it belongs to another collection", val.Name)
} }
} else {
log.Infof("%s was not removed because it belongs to another collection", val.Name)
} }
} }

View file

@ -2,11 +2,20 @@ package cwhub
import ( import (
"errors" "errors"
"fmt"
) )
var ( var (
// ErrNilRemoteHub is returned when the remote hub configuration is not provided to the NewHub constructor. // ErrNilRemoteHub is returned when the remote hub configuration is not provided to the NewHub constructor.
// All attempts to download index or items will return this error. // All attempts to download index or items will return this error.
ErrMissingReference = errors.New("Reference(s) missing in collection")
ErrNilRemoteHub = errors.New("remote hub configuration is not provided. Please report this issue to the developers") ErrNilRemoteHub = errors.New("remote hub configuration is not provided. Please report this issue to the developers")
) )
type IndexNotFoundError struct {
URL string
Branch string
}
func (e IndexNotFoundError) Error() string {
return fmt.Sprintf("index not found at %s, branch '%s'. Please check the .cscli.hub_branch value if you specified it in config.yaml, or use 'master' if not sure", e.URL, e.Branch)
}

View file

@ -2,7 +2,6 @@ package cwhub
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"strings" "strings"
@ -20,10 +19,7 @@ type Hub struct {
skippedTainted int skippedTainted int
} }
var ( var theHub *Hub
theHub *Hub
ErrIndexNotFound = fmt.Errorf("index not found")
)
// GetHub returns the hub singleton // GetHub returns the hub singleton
// it returns an error if it's not initialized to avoid nil dereference // it returns an error if it's not initialized to avoid nil dereference
@ -48,85 +44,70 @@ func NewHub(local *csconfig.LocalHubCfg, remote *RemoteHubCfg, downloadIndex boo
} }
if downloadIndex { if downloadIndex {
if err := remote.DownloadIndex(local.HubIndexFile); err != nil { if err := remote.downloadIndex(local.HubIndexFile); err != nil {
return nil, err return nil, err
} }
} }
log.Debugf("loading hub idx %s", local.HubIndexFile) log.Debugf("loading hub idx %s", local.HubIndexFile)
bidx, err := os.ReadFile(local.HubIndexFile)
if err != nil {
return nil, fmt.Errorf("unable to read index file: %w", err)
}
ret, err := ParseIndex(bidx)
if err != nil {
if !errors.Is(err, ErrMissingReference) {
return nil, fmt.Errorf("failed to load index: %w", err)
}
// XXX: why the error check if we bail out anyway?
return nil, err
}
theHub = &Hub{ theHub = &Hub{
Items: ret,
local: local, local: local,
remote: remote, remote: remote,
} }
if _, err = theHub.LocalSync(); err != nil { if err := theHub.parseIndex(); err != nil {
return nil, fmt.Errorf("failed to sync hub index: %w", err) return nil, fmt.Errorf("failed to load index: %w", err)
}
if _, err := theHub.LocalSync(); err != nil {
return nil, fmt.Errorf("failed to sync items: %w", err)
} }
return theHub, nil return theHub, nil
} }
// ParseIndex takes the content of an index file and returns the map of associated parsers/scenarios/collections // parseIndex takes the content of an index file and fills the map of associated parsers/scenarios/collections
func ParseIndex(buff []byte) (HubItems, error) { func (h *Hub) parseIndex() error {
var ( bidx, err := os.ReadFile(h.local.HubIndexFile)
RawIndex HubItems if err != nil {
missingItems []string return fmt.Errorf("unable to read index file: %w", err)
) }
if err := json.Unmarshal(buff, &RawIndex); err != nil { if err := json.Unmarshal(bidx, &h.Items); err != nil {
return nil, fmt.Errorf("failed to unmarshal index: %w", err) return fmt.Errorf("failed to unmarshal index: %w", err)
} }
log.Debugf("%d item types in hub index", len(ItemTypes)) log.Debugf("%d item types in hub index", len(ItemTypes))
// Iterate over the different types to complete the struct // Iterate over the different types to complete the struct
for _, itemType := range ItemTypes { for _, itemType := range ItemTypes {
log.Tracef("%s: %d items", itemType, len(RawIndex[itemType])) log.Tracef("%s: %d items", itemType, len(h.Items[itemType]))
for name, item := range RawIndex[itemType] { for name, item := range h.Items[itemType] {
item.Name = name item.Name = name
// if the item has no (redundant) author, take it from the json key
if item.Author == "" && strings.Contains(name, "/") {
item.Author = strings.Split(name, "/")[0]
}
item.Type = itemType item.Type = itemType
x := strings.Split(item.RemotePath, "/") x := strings.Split(item.RemotePath, "/")
item.FileName = x[len(x)-1] item.FileName = x[len(x)-1]
RawIndex[itemType][name] = item h.Items[itemType][name] = item
if itemType != COLLECTIONS {
continue
}
// if it's a collection, check its sub-items are present // if it's a collection, check its sub-items are present
// XXX should be done later // XXX should be done later, maybe report all missing at once?
for _, sub := range item.SubItems() { for _, sub := range item.SubItems() {
if _, ok := RawIndex[sub.Type][sub.Name]; !ok { if _, ok := h.Items[sub.Type][sub.Name]; !ok {
log.Errorf("Referred %s %s in collection %s doesn't exist.", sub.Type, sub.Name, item.Name) log.Errorf("Referred %s %s in collection %s doesn't exist.", sub.Type, sub.Name, item.Name)
missingItems = append(missingItems, sub.Name)
} }
} }
} }
} }
if len(missingItems) > 0 { return nil
return RawIndex, fmt.Errorf("%q: %w", missingItems, ErrMissingReference)
}
return RawIndex, nil
} }
// ItemStats returns total counts of the hub items // ItemStats returns total counts of the hub items
@ -134,11 +115,6 @@ func (h *Hub) ItemStats() []string {
loaded := "" loaded := ""
for _, itemType := range ItemTypes { for _, itemType := range ItemTypes {
// ensure the order is always the same
if h.Items[itemType] == nil {
continue
}
if len(h.Items[itemType]) == 0 { if len(h.Items[itemType]) == 0 {
continue continue
} }

View file

@ -45,7 +45,7 @@ func TestDownloadIndex(t *testing.T) {
IndexPath: "", IndexPath: "",
} }
err = hub.remote.DownloadIndex(tmpIndex.Name()) err = hub.remote.downloadIndex(tmpIndex.Name())
cstest.RequireErrorContains(t, err, "failed to build hub index request: invalid URL template 'x'") cstest.RequireErrorContains(t, err, "failed to build hub index request: invalid URL template 'x'")
// bad domain // bad domain
@ -57,7 +57,7 @@ func TestDownloadIndex(t *testing.T) {
IndexPath: ".index.json", IndexPath: ".index.json",
} }
err = hub.remote.DownloadIndex(tmpIndex.Name()) err = hub.remote.downloadIndex(tmpIndex.Name())
require.NoError(t, err) require.NoError(t, err)
// XXX: this is not failing // XXX: this is not failing
// cstest.RequireErrorContains(t, err, "failed http request for hub index: Get") // cstest.RequireErrorContains(t, err, "failed http request for hub index: Get")
@ -71,6 +71,6 @@ func TestDownloadIndex(t *testing.T) {
IndexPath: ".index.json", IndexPath: ".index.json",
} }
err = hub.remote.DownloadIndex("/does/not/exist/index.json") err = hub.remote.downloadIndex("/does/not/exist/index.json")
cstest.RequireErrorContains(t, err, "while opening hub index file: open /does/not/exist/index.json:") cstest.RequireErrorContains(t, err, "while opening hub index file: open /does/not/exist/index.json:")
} }

View file

@ -4,8 +4,8 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/Masterminds/semver/v3"
"github.com/enescakir/emoji" "github.com/enescakir/emoji"
"golang.org/x/mod/semver"
) )
const ( const (
@ -18,7 +18,14 @@ const (
WAAP_RULES = "waap-rules" WAAP_RULES = "waap-rules"
) )
// XXX: The order is important, as it is used to range over sub-items in collections const (
VersionUpToDate = iota
VersionUpdateAvailable
VersionUnknown
VersionFuture
)
// The order is important, as it is used to range over sub-items in collections
var ItemTypes = []string{PARSERS, POSTOVERFLOWS, SCENARIOS, WAAP_CONFIGS, WAAP_RULES, COLLECTIONS} var ItemTypes = []string{PARSERS, POSTOVERFLOWS, SCENARIOS, WAAP_CONFIGS, WAAP_RULES, COLLECTIONS}
type HubItems map[string]map[string]Item type HubItems map[string]map[string]Item
@ -194,7 +201,23 @@ func (i *Item) Status() (string, emoji.Emoji) {
// versionStatus: semver requires 'v' prefix // versionStatus: semver requires 'v' prefix
func (i *Item) versionStatus() int { func (i *Item) versionStatus() int {
return semver.Compare("v"+i.Version, "v"+i.LocalVersion) local, err := semver.NewVersion(i.LocalVersion)
if err != nil {
return VersionUnknown
}
// hub versions are already validated while syncing, ignore errors
latest, _ := semver.NewVersion(i.Version)
if local.LessThan(latest) {
return VersionUpdateAvailable
}
if local.Equal(latest) {
return VersionUpToDate
}
return VersionFuture
} }
// validPath returns true if the (relative) path is allowed for the item // validPath returns true if the (relative) path is allowed for the item
@ -250,6 +273,7 @@ func (h *Hub) AddItem(item Item) error {
} }
} }
// XXX: can this happen?
return fmt.Errorf("ItemType %s is unknown", item.Type) return fmt.Errorf("ItemType %s is unknown", item.Type)
} }

View file

@ -38,23 +38,16 @@ func itemKey(itemPath string) (string, error) {
} }
// GetItemByPath retrieves the item from the hub index based on its path. // GetItemByPath retrieves the item from the hub index based on its path.
// To achieve this it resolves a symlink to find the associated hub item.
func (h *Hub) GetItemByPath(itemType string, itemPath string) (*Item, error) { func (h *Hub) GetItemByPath(itemType string, itemPath string) (*Item, error) {
itemKey, err := itemKey(itemPath) itemKey, err := itemKey(itemPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// XXX: use GetItem() item := h.GetItem(itemType, itemKey)
m := h.GetItemMap(itemType) if item == nil {
if m == nil {
return nil, fmt.Errorf("item type %s doesn't exist", itemType)
}
v, ok := m[itemKey]
if !ok {
return nil, fmt.Errorf("%s not found in %s", itemKey, itemType) return nil, fmt.Errorf("%s not found in %s", itemKey, itemType)
} }
return &v, nil return item, nil
} }

View file

@ -29,8 +29,8 @@ func (r *RemoteHubCfg) urlTo(remotePath string) (string, error) {
return fmt.Sprintf(r.URLTemplate, r.Branch, remotePath), nil return fmt.Sprintf(r.URLTemplate, r.Branch, remotePath), nil
} }
// DownloadIndex downloads the latest version of the index // downloadIndex downloads the latest version of the index
func (r *RemoteHubCfg) DownloadIndex(localPath string) error { func (r *RemoteHubCfg) downloadIndex(localPath string) error {
if r == nil { if r == nil {
return ErrNilRemoteHub return ErrNilRemoteHub
} }
@ -53,7 +53,7 @@ func (r *RemoteHubCfg) DownloadIndex(localPath string) error {
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusNotFound { if resp.StatusCode == http.StatusNotFound {
return ErrIndexNotFound return IndexNotFoundError{req.URL.String(), r.Branch}
} }
return fmt.Errorf("bad http code %d for %s", resp.StatusCode, req.URL.String()) return fmt.Errorf("bad http code %d for %s", resp.StatusCode, req.URL.String())

View file

@ -10,7 +10,9 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/Masterminds/semver/v3"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"slices"
) )
func isYAMLFileName(path string) bool { func isYAMLFileName(path string) bool {
@ -31,7 +33,7 @@ func handleSymlink(path string) (string, error) {
return "", fmt.Errorf("failed to unlink %s: %w", path, err) return "", fmt.Errorf("failed to unlink %s: %w", path, err)
} }
// XXX: is this correct? // ignore this file
return "", nil return "", nil
} }
@ -130,6 +132,28 @@ func (h *Hub) getItemInfo(path string) (itemFileInfo, bool, error) {
return ret, inhub, nil return ret, inhub, nil
} }
// sortedVersions returns the input data, sorted in reverse order by semver
func sortedVersions(raw []string) ([]string, error) {
vs := make([]*semver.Version, len(raw))
for i, r := range raw {
v, err := semver.NewVersion(r)
if err != nil {
return nil, fmt.Errorf("%s: %w", r, err)
}
vs[i] = v
}
sort.Sort(sort.Reverse(semver.Collection(vs)))
ret := make([]string, len(vs))
for i, v := range vs {
ret[i] = v.Original()
}
return ret, nil
}
func (h *Hub) itemVisit(path string, f os.DirEntry, err error) error { func (h *Hub) itemVisit(path string, f os.DirEntry, err error) error {
var ( var (
local bool local bool
@ -179,7 +203,7 @@ func (h *Hub) itemVisit(path string, f os.DirEntry, err error) error {
log.Tracef("%s points to %s", path, hubpath) log.Tracef("%s points to %s", path, hubpath)
if hubpath == "" { if hubpath == "" {
// XXX: is this correct? // ignore this file
return nil return nil
} }
} }
@ -251,17 +275,18 @@ func (h *Hub) itemVisit(path string, f os.DirEntry, err error) error {
} }
// let's reverse sort the versions to deal with hash collisions (#154) // let's reverse sort the versions to deal with hash collisions (#154)
// XXX: we sure, lexical sorting?
versions := make([]string, 0, len(item.Versions)) versions := make([]string, 0, len(item.Versions))
for k := range item.Versions { for k := range item.Versions {
versions = append(versions, k) versions = append(versions, k)
} }
sort.Sort(sort.Reverse(sort.StringSlice(versions))) versions, err = sortedVersions(versions)
if err != nil {
return fmt.Errorf("while syncing %s %s: %w", info.ftype, info.fname, err)
}
for _, version := range versions { for _, version := range versions {
if item.Versions[version].Digest != sha { if item.Versions[version].Digest != sha {
// log.Infof("matching filenames, wrong hash %s != %s -- %s", sha, val.Digest, spew.Sdump(v))
continue continue
} }
@ -321,7 +346,7 @@ func (h *Hub) CollectDepsCheck(v *Item) error {
return nil return nil
} }
if v.versionStatus() != 0 { // not up-to-date if v.versionStatus() != VersionUpToDate { // not up-to-date
log.Debugf("%s dependencies not checked: not up-to-date", v.Name) log.Debugf("%s dependencies not checked: not up-to-date", v.Name)
return nil return nil
} }
@ -371,15 +396,7 @@ func (h *Hub) CollectDepsCheck(v *Item) error {
return fmt.Errorf("outdated %s %s", sub.Type, sub.Name) return fmt.Errorf("outdated %s %s", sub.Type, sub.Name)
} }
skip := false if !slices.Contains(subItem.BelongsToCollections, v.Name) {
for idx := range subItem.BelongsToCollections {
if subItem.BelongsToCollections[idx] == v.Name {
skip = true
}
}
if !skip {
subItem.BelongsToCollections = append(subItem.BelongsToCollections, v.Name) subItem.BelongsToCollections = append(subItem.BelongsToCollections, v.Name)
} }
@ -419,15 +436,17 @@ func (h *Hub) SyncDir(dir string) ([]string, error) {
vs := item.versionStatus() vs := item.versionStatus()
switch vs { switch vs {
case 0: // latest case VersionUpToDate: // latest
if err := h.CollectDepsCheck(&item); err != nil { if err := h.CollectDepsCheck(&item); err != nil {
warnings = append(warnings, fmt.Sprintf("dependency of %s: %s", item.Name, err)) warnings = append(warnings, fmt.Sprintf("dependency of %s: %s", item.Name, err))
h.Items[COLLECTIONS][name] = item h.Items[COLLECTIONS][name] = item
} }
case 1: // not up-to-date case VersionUpdateAvailable: // not up-to-date
warnings = append(warnings, fmt.Sprintf("update for collection %s available (currently:%s, latest:%s)", item.Name, item.LocalVersion, item.Version)) warnings = append(warnings, fmt.Sprintf("update for collection %s available (currently:%s, latest:%s)", item.Name, item.LocalVersion, item.Version))
default: // version is higher than the highest available from hub? case VersionFuture:
warnings = append(warnings, fmt.Sprintf("collection %s is in the future (currently:%s, latest:%s)", item.Name, item.LocalVersion, item.Version)) warnings = append(warnings, fmt.Sprintf("collection %s is in the future (currently:%s, latest:%s)", item.Name, item.LocalVersion, item.Version))
case VersionUnknown:
warnings = append(warnings, fmt.Sprintf("collection %s is tainted (latest:%s)", item.Name, item.Version))
} }
log.Debugf("installed (%s) - status: %d | installed: %s | latest: %s | full: %+v", item.Name, vs, item.LocalVersion, item.Version, item.Versions) log.Debugf("installed (%s) - status: %d | installed: %s | latest: %s | full: %+v", item.Name, vs, item.LocalVersion, item.Version, item.Versions)

View file

@ -5,173 +5,194 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"sort"
"strings" "strings"
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
) )
type ParserCoverage struct { type Coverage struct {
Parser string Name string
TestsCount int TestsCount int
PresentIn map[string]bool //poorman's set PresentIn map[string]bool //poorman's set
} }
type ScenarioCoverage struct { func (h *HubTest) GetParsersCoverage() ([]Coverage, error) {
Scenario string
TestsCount int
PresentIn map[string]bool
}
func (h *HubTest) GetParsersCoverage() ([]ParserCoverage, error) {
var coverage []ParserCoverage
if _, ok := h.HubIndex.Items[cwhub.PARSERS]; !ok { if _, ok := h.HubIndex.Items[cwhub.PARSERS]; !ok {
return coverage, fmt.Errorf("no parsers in hub index") return nil, fmt.Errorf("no parsers in hub index")
} }
//populate from hub, iterate in alphabetical order
var pkeys []string // populate from hub, iterate in alphabetical order
for pname := range h.HubIndex.Items[cwhub.PARSERS] { pkeys := sortedMapKeys(h.HubIndex.Items[cwhub.PARSERS])
pkeys = append(pkeys, pname) coverage := make([]Coverage, len(pkeys))
}
sort.Strings(pkeys) for i, name := range pkeys {
for _, pname := range pkeys { coverage[i] = Coverage{
coverage = append(coverage, ParserCoverage{ Name: name,
Parser: pname,
TestsCount: 0, TestsCount: 0,
PresentIn: make(map[string]bool), PresentIn: make(map[string]bool),
}) }
} }
//parser the expressions a-la-oneagain // parser the expressions a-la-oneagain
passerts, err := filepath.Glob(".tests/*/parser.assert") passerts, err := filepath.Glob(".tests/*/parser.assert")
if err != nil { if err != nil {
return coverage, fmt.Errorf("while find parser asserts : %s", err) return nil, fmt.Errorf("while find parser asserts : %s", err)
} }
for _, assert := range passerts { for _, assert := range passerts {
file, err := os.Open(assert) file, err := os.Open(assert)
if err != nil { if err != nil {
return coverage, fmt.Errorf("while reading %s : %s", assert, err) return nil, fmt.Errorf("while reading %s : %s", assert, err)
} }
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
for scanner.Scan() { for scanner.Scan() {
assertLine := regexp.MustCompile(`^results\["[^"]+"\]\["(?P<parser>[^"]+)"\]\[[0-9]+\]\.Evt\..*`)
line := scanner.Text() line := scanner.Text()
log.Debugf("assert line : %s", line) log.Debugf("assert line : %s", line)
match := assertLine.FindStringSubmatch(line)
match := parserResultRE.FindStringSubmatch(line)
if len(match) == 0 { if len(match) == 0 {
log.Debugf("%s doesn't match", line) log.Debugf("%s doesn't match", line)
continue continue
} }
sidx := assertLine.SubexpIndex("parser")
sidx := parserResultRE.SubexpIndex("parser")
capturedParser := match[sidx] capturedParser := match[sidx]
for idx, pcover := range coverage { for idx, pcover := range coverage {
if pcover.Parser == capturedParser { if pcover.Name == capturedParser {
coverage[idx].TestsCount++ coverage[idx].TestsCount++
coverage[idx].PresentIn[assert] = true coverage[idx].PresentIn[assert] = true
continue continue
} }
parserNameSplit := strings.Split(pcover.Parser, "/")
parserNameSplit := strings.Split(pcover.Name, "/")
parserNameOnly := parserNameSplit[len(parserNameSplit)-1] parserNameOnly := parserNameSplit[len(parserNameSplit)-1]
if parserNameOnly == capturedParser { if parserNameOnly == capturedParser {
coverage[idx].TestsCount++ coverage[idx].TestsCount++
coverage[idx].PresentIn[assert] = true coverage[idx].PresentIn[assert] = true
continue continue
} }
capturedParserSplit := strings.Split(capturedParser, "/") capturedParserSplit := strings.Split(capturedParser, "/")
capturedParserName := capturedParserSplit[len(capturedParserSplit)-1] capturedParserName := capturedParserSplit[len(capturedParserSplit)-1]
if capturedParserName == parserNameOnly { if capturedParserName == parserNameOnly {
coverage[idx].TestsCount++ coverage[idx].TestsCount++
coverage[idx].PresentIn[assert] = true coverage[idx].PresentIn[assert] = true
continue continue
} }
if capturedParserName == parserNameOnly+"-logs" { if capturedParserName == parserNameOnly+"-logs" {
coverage[idx].TestsCount++ coverage[idx].TestsCount++
coverage[idx].PresentIn[assert] = true coverage[idx].PresentIn[assert] = true
continue continue
} }
} }
} }
file.Close() file.Close()
} }
return coverage, nil return coverage, nil
} }
func (h *HubTest) GetScenariosCoverage() ([]ScenarioCoverage, error) { func (h *HubTest) GetScenariosCoverage() ([]Coverage, error) {
var coverage []ScenarioCoverage
if _, ok := h.HubIndex.Items[cwhub.SCENARIOS]; !ok { if _, ok := h.HubIndex.Items[cwhub.SCENARIOS]; !ok {
return coverage, fmt.Errorf("no scenarios in hub index") return nil, fmt.Errorf("no scenarios in hub index")
}
//populate from hub, iterate in alphabetical order
var pkeys []string
for scenarioName := range h.HubIndex.Items[cwhub.SCENARIOS] {
pkeys = append(pkeys, scenarioName)
}
sort.Strings(pkeys)
for _, scenarioName := range pkeys {
coverage = append(coverage, ScenarioCoverage{
Scenario: scenarioName,
TestsCount: 0,
PresentIn: make(map[string]bool),
})
} }
//parser the expressions a-la-oneagain // populate from hub, iterate in alphabetical order
pkeys := sortedMapKeys(h.HubIndex.Items[cwhub.SCENARIOS])
coverage := make([]Coverage, len(pkeys))
for i, name := range pkeys {
coverage[i] = Coverage{
Name: name,
TestsCount: 0,
PresentIn: make(map[string]bool),
}
}
// parser the expressions a-la-oneagain
passerts, err := filepath.Glob(".tests/*/scenario.assert") passerts, err := filepath.Glob(".tests/*/scenario.assert")
if err != nil { if err != nil {
return coverage, fmt.Errorf("while find scenario asserts : %s", err) return nil, fmt.Errorf("while find scenario asserts : %s", err)
} }
for _, assert := range passerts { for _, assert := range passerts {
file, err := os.Open(assert) file, err := os.Open(assert)
if err != nil { if err != nil {
return coverage, fmt.Errorf("while reading %s : %s", assert, err) return nil, fmt.Errorf("while reading %s : %s", assert, err)
} }
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
for scanner.Scan() { for scanner.Scan() {
assertLine := regexp.MustCompile(`^results\[[0-9]+\].Overflow.Alert.GetScenario\(\) == "(?P<scenario>[^"]+)"`)
line := scanner.Text() line := scanner.Text()
log.Debugf("assert line : %s", line) log.Debugf("assert line : %s", line)
match := assertLine.FindStringSubmatch(line) match := scenarioResultRE.FindStringSubmatch(line)
if len(match) == 0 { if len(match) == 0 {
log.Debugf("%s doesn't match", line) log.Debugf("%s doesn't match", line)
continue continue
} }
sidx := assertLine.SubexpIndex("scenario")
scanner_name := match[sidx] sidx := scenarioResultRE.SubexpIndex("scenario")
scannerName := match[sidx]
for idx, pcover := range coverage { for idx, pcover := range coverage {
if pcover.Scenario == scanner_name { if pcover.Name == scannerName {
coverage[idx].TestsCount++ coverage[idx].TestsCount++
coverage[idx].PresentIn[assert] = true coverage[idx].PresentIn[assert] = true
continue continue
} }
scenarioNameSplit := strings.Split(pcover.Scenario, "/")
scenarioNameSplit := strings.Split(pcover.Name, "/")
scenarioNameOnly := scenarioNameSplit[len(scenarioNameSplit)-1] scenarioNameOnly := scenarioNameSplit[len(scenarioNameSplit)-1]
if scenarioNameOnly == scanner_name {
if scenarioNameOnly == scannerName {
coverage[idx].TestsCount++ coverage[idx].TestsCount++
coverage[idx].PresentIn[assert] = true coverage[idx].PresentIn[assert] = true
continue continue
} }
fixedProbingWord := strings.ReplaceAll(pcover.Scenario, "probbing", "probing")
fixedProbingAssert := strings.ReplaceAll(scanner_name, "probbing", "probing") fixedProbingWord := strings.ReplaceAll(pcover.Name, "probbing", "probing")
fixedProbingAssert := strings.ReplaceAll(scannerName, "probbing", "probing")
if fixedProbingWord == fixedProbingAssert { if fixedProbingWord == fixedProbingAssert {
coverage[idx].TestsCount++ coverage[idx].TestsCount++
coverage[idx].PresentIn[assert] = true coverage[idx].PresentIn[assert] = true
continue continue
} }
if fmt.Sprintf("%s-detection", pcover.Scenario) == scanner_name {
if fmt.Sprintf("%s-detection", pcover.Name) == scannerName {
coverage[idx].TestsCount++ coverage[idx].TestsCount++
coverage[idx].PresentIn[assert] = true coverage[idx].PresentIn[assert] = true
continue continue
} }
if fmt.Sprintf("%s-detection", fixedProbingWord) == fixedProbingAssert { if fmt.Sprintf("%s-detection", fixedProbingWord) == fixedProbingAssert {
coverage[idx].TestsCount++ coverage[idx].TestsCount++
coverage[idx].PresentIn[assert] = true coverage[idx].PresentIn[assert] = true
continue continue
} }
} }
} }
file.Close() file.Close()
} }
return coverage, nil return coverage, nil
} }

View file

@ -6,6 +6,7 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/cwhub" "github.com/crowdsecurity/crowdsec/pkg/cwhub"
) )
@ -29,42 +30,44 @@ const (
) )
func NewHubTest(hubPath string, crowdsecPath string, cscliPath string) (HubTest, error) { func NewHubTest(hubPath string, crowdsecPath string, cscliPath string) (HubTest, error) {
var err error hubPath, err := filepath.Abs(hubPath)
hubPath, err = filepath.Abs(hubPath)
if err != nil { if err != nil {
return HubTest{}, fmt.Errorf("can't get absolute path of hub: %+v", err) return HubTest{}, fmt.Errorf("can't get absolute path of hub: %+v", err)
} }
// we can't use hubtest without the hub // we can't use hubtest without the hub
if _, err := os.Stat(hubPath); os.IsNotExist(err) { if _, err = os.Stat(hubPath); os.IsNotExist(err) {
return HubTest{}, fmt.Errorf("path to hub '%s' doesn't exist, can't run", hubPath) return HubTest{}, fmt.Errorf("path to hub '%s' doesn't exist, can't run", hubPath)
} }
HubTestPath := filepath.Join(hubPath, "./.tests/") HubTestPath := filepath.Join(hubPath, "./.tests/")
// we can't use hubtest without crowdsec binary // we can't use hubtest without crowdsec binary
if _, err := exec.LookPath(crowdsecPath); err != nil { if _, err = exec.LookPath(crowdsecPath); err != nil {
if _, err := os.Stat(crowdsecPath); os.IsNotExist(err) { if _, err = os.Stat(crowdsecPath); os.IsNotExist(err) {
return HubTest{}, fmt.Errorf("path to crowdsec binary '%s' doesn't exist or is not in $PATH, can't run", crowdsecPath) return HubTest{}, fmt.Errorf("path to crowdsec binary '%s' doesn't exist or is not in $PATH, can't run", crowdsecPath)
} }
} }
// we can't use hubtest without cscli binary // we can't use hubtest without cscli binary
if _, err := exec.LookPath(cscliPath); err != nil { if _, err = exec.LookPath(cscliPath); err != nil {
if _, err := os.Stat(cscliPath); os.IsNotExist(err) { if _, err = os.Stat(cscliPath); os.IsNotExist(err) {
return HubTest{}, fmt.Errorf("path to cscli binary '%s' doesn't exist or is not in $PATH, can't run", cscliPath) return HubTest{}, fmt.Errorf("path to cscli binary '%s' doesn't exist or is not in $PATH, can't run", cscliPath)
} }
} }
hubIndexFile := filepath.Join(hubPath, ".index.json") hubIndexFile := filepath.Join(hubPath, ".index.json")
bidx, err := os.ReadFile(hubIndexFile)
if err != nil { local := &csconfig.LocalHubCfg{
return HubTest{}, fmt.Errorf("unable to read index file: %s", err) HubDir: hubPath,
HubIndexFile: hubIndexFile,
InstallDir: HubTestPath,
InstallDataDir: HubTestPath,
} }
// load hub index hub, err := cwhub.NewHub(local, nil, false)
hubIndex, err := cwhub.ParseIndex(bidx)
if err != nil { if err != nil {
return HubTest{}, fmt.Errorf("unable to load hub index file: %s", err) return HubTest{}, fmt.Errorf("unable to load hub: %s", err)
} }
templateConfigFilePath := filepath.Join(HubTestPath, templateConfigFile) templateConfigFilePath := filepath.Join(HubTestPath, templateConfigFile)
@ -80,16 +83,18 @@ func NewHubTest(hubPath string, crowdsecPath string, cscliPath string) (HubTest,
TemplateConfigPath: templateConfigFilePath, TemplateConfigPath: templateConfigFilePath,
TemplateProfilePath: templateProfilePath, TemplateProfilePath: templateProfilePath,
TemplateSimulationPath: templateSimulationPath, TemplateSimulationPath: templateSimulationPath,
HubIndex: &cwhub.Hub{Items: hubIndex}, HubIndex: hub,
}, nil }, nil
} }
func (h *HubTest) LoadTestItem(name string) (*HubTestItem, error) { func (h *HubTest) LoadTestItem(name string) (*HubTestItem, error) {
HubTestItem := &HubTestItem{} HubTestItem := &HubTestItem{}
testItem, err := NewTest(name, h) testItem, err := NewTest(name, h)
if err != nil { if err != nil {
return HubTestItem, err return HubTestItem, err
} }
h.Tests = append(h.Tests, testItem) h.Tests = append(h.Tests, testItem)
return testItem, nil return testItem, nil
@ -108,5 +113,6 @@ func (h *HubTest) LoadAllTests() error {
} }
} }
} }
return nil return nil
} }

View file

@ -7,11 +7,12 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
"github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/cwhub" "github.com/crowdsecurity/crowdsec/pkg/cwhub"
"github.com/crowdsecurity/crowdsec/pkg/parser" "github.com/crowdsecurity/crowdsec/pkg/parser"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
) )
type HubTestItemConfig struct { type HubTestItemConfig struct {
@ -76,8 +77,6 @@ const (
BucketPourResultFileName = "bucketpour-dump.yaml" BucketPourResultFileName = "bucketpour-dump.yaml"
) )
var crowdsecPatternsFolder = csconfig.DefaultConfigPath("patterns")
func NewTest(name string, hubTest *HubTest) (*HubTestItem, error) { func NewTest(name string, hubTest *HubTest) (*HubTestItem, error) {
testPath := filepath.Join(hubTest.HubTestPath, name) testPath := filepath.Join(hubTest.HubTestPath, name)
runtimeFolder := filepath.Join(testPath, "runtime") runtimeFolder := filepath.Join(testPath, "runtime")
@ -87,10 +86,12 @@ func NewTest(name string, hubTest *HubTest) (*HubTestItem, error) {
// read test configuration file // read test configuration file
configFileData := &HubTestItemConfig{} configFileData := &HubTestItemConfig{}
yamlFile, err := os.ReadFile(configFilePath) yamlFile, err := os.ReadFile(configFilePath)
if err != nil { if err != nil {
log.Printf("no config file found in '%s': %v", testPath, err) log.Printf("no config file found in '%s': %v", testPath, err)
} }
err = yaml.Unmarshal(yamlFile, configFileData) err = yaml.Unmarshal(yamlFile, configFileData)
if err != nil { if err != nil {
return nil, fmt.Errorf("unmarshal: %v", err) return nil, fmt.Errorf("unmarshal: %v", err)
@ -101,6 +102,7 @@ func NewTest(name string, hubTest *HubTest) (*HubTestItem, error) {
scenarioAssertFilePath := filepath.Join(testPath, ScenarioAssertFileName) scenarioAssertFilePath := filepath.Join(testPath, ScenarioAssertFileName)
ScenarioAssert := NewScenarioAssert(scenarioAssertFilePath) ScenarioAssert := NewScenarioAssert(scenarioAssertFilePath)
return &HubTestItem{ return &HubTestItem{
Name: name, Name: name,
Path: testPath, Path: testPath,
@ -143,23 +145,25 @@ func (t *HubTestItem) InstallHub() error {
if parser == "" { if parser == "" {
continue continue
} }
var parserDirDest string
if hubParser, ok := t.HubIndex.Items[cwhub.PARSERS][parser]; ok { if hubParser, ok := t.HubIndex.Items[cwhub.PARSERS][parser]; ok {
parserSource, err := filepath.Abs(filepath.Join(t.HubPath, hubParser.RemotePath)) parserSource, err := filepath.Abs(filepath.Join(t.HubPath, hubParser.RemotePath))
if err != nil { if err != nil {
return fmt.Errorf("can't get absolute path of '%s': %s", parserSource, err) return fmt.Errorf("can't get absolute path of '%s': %s", parserSource, err)
} }
parserFileName := filepath.Base(parserSource) parserFileName := filepath.Base(parserSource)
// runtime/hub/parsers/s00-raw/crowdsecurity/ // runtime/hub/parsers/s00-raw/crowdsecurity/
hubDirParserDest := filepath.Join(t.RuntimeHubPath, filepath.Dir(hubParser.RemotePath)) hubDirParserDest := filepath.Join(t.RuntimeHubPath, filepath.Dir(hubParser.RemotePath))
// runtime/parsers/s00-raw/ // runtime/parsers/s00-raw/
parserDirDest = fmt.Sprintf("%s/parsers/%s/", t.RuntimePath, hubParser.Stage) parserDirDest := fmt.Sprintf("%s/parsers/%s/", t.RuntimePath, hubParser.Stage)
if err := os.MkdirAll(hubDirParserDest, os.ModePerm); err != nil { if err := os.MkdirAll(hubDirParserDest, os.ModePerm); err != nil {
return fmt.Errorf("unable to create folder '%s': %s", hubDirParserDest, err) return fmt.Errorf("unable to create folder '%s': %s", hubDirParserDest, err)
} }
if err := os.MkdirAll(parserDirDest, os.ModePerm); err != nil { if err := os.MkdirAll(parserDirDest, os.ModePerm); err != nil {
return fmt.Errorf("unable to create folder '%s': %s", parserDirDest, err) return fmt.Errorf("unable to create folder '%s': %s", parserDirDest, err)
} }
@ -200,7 +204,7 @@ func (t *HubTestItem) InstallHub() error {
//return fmt.Errorf("stage '%s' extracted from '%s' doesn't exist in the hub", customParserStage, hubStagePath) //return fmt.Errorf("stage '%s' extracted from '%s' doesn't exist in the hub", customParserStage, hubStagePath)
} }
parserDirDest = fmt.Sprintf("%s/parsers/%s/", t.RuntimePath, customParserStage) parserDirDest := fmt.Sprintf("%s/parsers/%s/", t.RuntimePath, customParserStage)
if err := os.MkdirAll(parserDirDest, os.ModePerm); err != nil { if err := os.MkdirAll(parserDirDest, os.ModePerm); err != nil {
continue continue
//return fmt.Errorf("unable to create folder '%s': %s", parserDirDest, err) //return fmt.Errorf("unable to create folder '%s': %s", parserDirDest, err)
@ -227,23 +231,25 @@ func (t *HubTestItem) InstallHub() error {
if scenario == "" { if scenario == "" {
continue continue
} }
var scenarioDirDest string
if hubScenario, ok := t.HubIndex.Items[cwhub.SCENARIOS][scenario]; ok { if hubScenario, ok := t.HubIndex.Items[cwhub.SCENARIOS][scenario]; ok {
scenarioSource, err := filepath.Abs(filepath.Join(t.HubPath, hubScenario.RemotePath)) scenarioSource, err := filepath.Abs(filepath.Join(t.HubPath, hubScenario.RemotePath))
if err != nil { if err != nil {
return fmt.Errorf("can't get absolute path to: %s", scenarioSource) return fmt.Errorf("can't get absolute path to: %s", scenarioSource)
} }
scenarioFileName := filepath.Base(scenarioSource) scenarioFileName := filepath.Base(scenarioSource)
// runtime/hub/scenarios/crowdsecurity/ // runtime/hub/scenarios/crowdsecurity/
hubDirScenarioDest := filepath.Join(t.RuntimeHubPath, filepath.Dir(hubScenario.RemotePath)) hubDirScenarioDest := filepath.Join(t.RuntimeHubPath, filepath.Dir(hubScenario.RemotePath))
// runtime/parsers/scenarios/ // runtime/parsers/scenarios/
scenarioDirDest = fmt.Sprintf("%s/scenarios/", t.RuntimePath) scenarioDirDest := fmt.Sprintf("%s/scenarios/", t.RuntimePath)
if err := os.MkdirAll(hubDirScenarioDest, os.ModePerm); err != nil { if err := os.MkdirAll(hubDirScenarioDest, os.ModePerm); err != nil {
return fmt.Errorf("unable to create folder '%s': %s", hubDirScenarioDest, err) return fmt.Errorf("unable to create folder '%s': %s", hubDirScenarioDest, err)
} }
if err := os.MkdirAll(scenarioDirDest, os.ModePerm); err != nil { if err := os.MkdirAll(scenarioDirDest, os.ModePerm); err != nil {
return fmt.Errorf("unable to create folder '%s': %s", scenarioDirDest, err) return fmt.Errorf("unable to create folder '%s': %s", scenarioDirDest, err)
} }
@ -271,7 +277,7 @@ func (t *HubTestItem) InstallHub() error {
//return fmt.Errorf("scenarios '%s' doesn't exist in the hub and doesn't appear to be a custom one.", scenario) //return fmt.Errorf("scenarios '%s' doesn't exist in the hub and doesn't appear to be a custom one.", scenario)
} }
scenarioDirDest = fmt.Sprintf("%s/scenarios/", t.RuntimePath) scenarioDirDest := fmt.Sprintf("%s/scenarios/", t.RuntimePath)
if err := os.MkdirAll(scenarioDirDest, os.ModePerm); err != nil { if err := os.MkdirAll(scenarioDirDest, os.ModePerm); err != nil {
return fmt.Errorf("unable to create folder '%s': %s", scenarioDirDest, err) return fmt.Errorf("unable to create folder '%s': %s", scenarioDirDest, err)
} }
@ -296,23 +302,25 @@ func (t *HubTestItem) InstallHub() error {
if postoverflow == "" { if postoverflow == "" {
continue continue
} }
var postoverflowDirDest string
if hubPostOverflow, ok := t.HubIndex.Items[cwhub.POSTOVERFLOWS][postoverflow]; ok { if hubPostOverflow, ok := t.HubIndex.Items[cwhub.POSTOVERFLOWS][postoverflow]; ok {
postoverflowSource, err := filepath.Abs(filepath.Join(t.HubPath, hubPostOverflow.RemotePath)) postoverflowSource, err := filepath.Abs(filepath.Join(t.HubPath, hubPostOverflow.RemotePath))
if err != nil { if err != nil {
return fmt.Errorf("can't get absolute path of '%s': %s", postoverflowSource, err) return fmt.Errorf("can't get absolute path of '%s': %s", postoverflowSource, err)
} }
postoverflowFileName := filepath.Base(postoverflowSource) postoverflowFileName := filepath.Base(postoverflowSource)
// runtime/hub/postoverflows/s00-enrich/crowdsecurity/ // runtime/hub/postoverflows/s00-enrich/crowdsecurity/
hubDirPostoverflowDest := filepath.Join(t.RuntimeHubPath, filepath.Dir(hubPostOverflow.RemotePath)) hubDirPostoverflowDest := filepath.Join(t.RuntimeHubPath, filepath.Dir(hubPostOverflow.RemotePath))
// runtime/postoverflows/s00-enrich // runtime/postoverflows/s00-enrich
postoverflowDirDest = fmt.Sprintf("%s/postoverflows/%s/", t.RuntimePath, hubPostOverflow.Stage) postoverflowDirDest := fmt.Sprintf("%s/postoverflows/%s/", t.RuntimePath, hubPostOverflow.Stage)
if err := os.MkdirAll(hubDirPostoverflowDest, os.ModePerm); err != nil { if err := os.MkdirAll(hubDirPostoverflowDest, os.ModePerm); err != nil {
return fmt.Errorf("unable to create folder '%s': %s", hubDirPostoverflowDest, err) return fmt.Errorf("unable to create folder '%s': %s", hubDirPostoverflowDest, err)
} }
if err := os.MkdirAll(postoverflowDirDest, os.ModePerm); err != nil { if err := os.MkdirAll(postoverflowDirDest, os.ModePerm); err != nil {
return fmt.Errorf("unable to create folder '%s': %s", postoverflowDirDest, err) return fmt.Errorf("unable to create folder '%s': %s", postoverflowDirDest, err)
} }
@ -353,7 +361,7 @@ func (t *HubTestItem) InstallHub() error {
//return fmt.Errorf("stage '%s' from extracted '%s' doesn't exist in the hub", customPostoverflowStage, hubStagePath) //return fmt.Errorf("stage '%s' from extracted '%s' doesn't exist in the hub", customPostoverflowStage, hubStagePath)
} }
postoverflowDirDest = fmt.Sprintf("%s/postoverflows/%s/", t.RuntimePath, customPostoverflowStage) postoverflowDirDest := fmt.Sprintf("%s/postoverflows/%s/", t.RuntimePath, customPostoverflowStage)
if err := os.MkdirAll(postoverflowDirDest, os.ModePerm); err != nil { if err := os.MkdirAll(postoverflowDirDest, os.ModePerm); err != nil {
continue continue
//return fmt.Errorf("unable to create folder '%s': %s", postoverflowDirDest, err) //return fmt.Errorf("unable to create folder '%s': %s", postoverflowDirDest, err)
@ -380,10 +388,12 @@ func (t *HubTestItem) InstallHub() error {
Filter: "1==1", Filter: "1==1",
Statics: t.Config.OverrideStatics, Statics: t.Config.OverrideStatics,
} }
b, err := yaml.Marshal(n) b, err := yaml.Marshal(n)
if err != nil { if err != nil {
return fmt.Errorf("unable to marshal overrides: %s", err) return fmt.Errorf("unable to marshal overrides: %s", err)
} }
tgtFilename := fmt.Sprintf("%s/parsers/s00-raw/00_overrides.yaml", t.RuntimePath) tgtFilename := fmt.Sprintf("%s/parsers/s00-raw/00_overrides.yaml", t.RuntimePath)
if err := os.WriteFile(tgtFilename, b, os.ModePerm); err != nil { if err := os.WriteFile(tgtFilename, b, os.ModePerm); err != nil {
return fmt.Errorf("unable to write overrides to '%s': %s", tgtFilename, err) return fmt.Errorf("unable to write overrides to '%s': %s", tgtFilename, err)
@ -403,6 +413,7 @@ func (t *HubTestItem) InstallHub() error {
if err := hub.DownloadDataIfNeeded(item, true); err != nil { if err := hub.DownloadDataIfNeeded(item, true); err != nil {
return fmt.Errorf("unable to download data for parser '%s': %+v", parserName, err) return fmt.Errorf("unable to download data for parser '%s': %+v", parserName, err)
} }
log.Debugf("parser '%s' installed successfully in runtime environment", parserName) log.Debugf("parser '%s' installed successfully in runtime environment", parserName)
} }
} }
@ -414,6 +425,7 @@ func (t *HubTestItem) InstallHub() error {
if err := hub.DownloadDataIfNeeded(item, true); err != nil { if err := hub.DownloadDataIfNeeded(item, true); err != nil {
return fmt.Errorf("unable to download data for parser '%s': %+v", scenarioName, err) return fmt.Errorf("unable to download data for parser '%s': %+v", scenarioName, err)
} }
log.Debugf("scenario '%s' installed successfully in runtime environment", scenarioName) log.Debugf("scenario '%s' installed successfully in runtime environment", scenarioName)
} }
} }
@ -425,6 +437,7 @@ func (t *HubTestItem) InstallHub() error {
if err := hub.DownloadDataIfNeeded(item, true); err != nil { if err := hub.DownloadDataIfNeeded(item, true); err != nil {
return fmt.Errorf("unable to download data for parser '%s': %+v", postoverflowName, err) return fmt.Errorf("unable to download data for parser '%s': %+v", postoverflowName, err)
} }
log.Debugf("postoverflow '%s' installed successfully in runtime environment", postoverflowName) log.Debugf("postoverflow '%s' installed successfully in runtime environment", postoverflowName)
} }
} }
@ -489,6 +502,8 @@ func (t *HubTestItem) Run() error {
return fmt.Errorf("unable to copy '%s' to '%s': %v", t.TemplateSimulationPath, t.RuntimeSimulationFilePath, err) return fmt.Errorf("unable to copy '%s' to '%s': %v", t.TemplateSimulationPath, t.RuntimeSimulationFilePath, err)
} }
crowdsecPatternsFolder := csconfig.DefaultConfigPath("patterns")
// copy template patterns folder to runtime folder // copy template patterns folder to runtime folder
if err = CopyDir(crowdsecPatternsFolder, t.RuntimePatternsPath); err != nil { if err = CopyDir(crowdsecPatternsFolder, t.RuntimePatternsPath); err != nil {
return fmt.Errorf("unable to copy 'patterns' from '%s' to '%s': %s", crowdsecPatternsFolder, t.RuntimePatternsPath, err) return fmt.Errorf("unable to copy 'patterns' from '%s' to '%s': %s", crowdsecPatternsFolder, t.RuntimePatternsPath, err)
@ -511,6 +526,7 @@ func (t *HubTestItem) Run() error {
if err != nil { if err != nil {
return fmt.Errorf("unable to stat log file '%s': %s", logFile, err) return fmt.Errorf("unable to stat log file '%s': %s", logFile, err)
} }
if logFileStat.Size() == 0 { if logFileStat.Size() == 0 {
return fmt.Errorf("log file '%s' is empty, please fill it with log", logFile) return fmt.Errorf("log file '%s' is empty, please fill it with log", logFile)
} }
@ -518,6 +534,7 @@ func (t *HubTestItem) Run() error {
cmdArgs := []string{"-c", t.RuntimeConfigFilePath, "machines", "add", "testMachine", "--auto"} cmdArgs := []string{"-c", t.RuntimeConfigFilePath, "machines", "add", "testMachine", "--auto"}
cscliRegisterCmd := exec.Command(t.CscliPath, cmdArgs...) cscliRegisterCmd := exec.Command(t.CscliPath, cmdArgs...)
log.Debugf("%s", cscliRegisterCmd.String()) log.Debugf("%s", cscliRegisterCmd.String())
output, err := cscliRegisterCmd.CombinedOutput() output, err := cscliRegisterCmd.CombinedOutput()
if err != nil { if err != nil {
if !strings.Contains(string(output), "unable to create machine: user 'testMachine': user already exist") { if !strings.Contains(string(output), "unable to create machine: user 'testMachine': user already exist") {
@ -527,16 +544,20 @@ func (t *HubTestItem) Run() error {
} }
cmdArgs = []string{"-c", t.RuntimeConfigFilePath, "-type", logType, "-dsn", dsn, "-dump-data", t.ResultsPath, "-order-event"} cmdArgs = []string{"-c", t.RuntimeConfigFilePath, "-type", logType, "-dsn", dsn, "-dump-data", t.ResultsPath, "-order-event"}
for labelKey, labelValue := range t.Config.Labels { for labelKey, labelValue := range t.Config.Labels {
arg := fmt.Sprintf("%s:%s", labelKey, labelValue) arg := fmt.Sprintf("%s:%s", labelKey, labelValue)
cmdArgs = append(cmdArgs, "-label", arg) cmdArgs = append(cmdArgs, "-label", arg)
} }
crowdsecCmd := exec.Command(t.CrowdSecPath, cmdArgs...) crowdsecCmd := exec.Command(t.CrowdSecPath, cmdArgs...)
log.Debugf("%s", crowdsecCmd.String()) log.Debugf("%s", crowdsecCmd.String())
output, err = crowdsecCmd.CombinedOutput() output, err = crowdsecCmd.CombinedOutput()
if log.GetLevel() >= log.DebugLevel || err != nil { if log.GetLevel() >= log.DebugLevel || err != nil {
fmt.Println(string(output)) fmt.Println(string(output))
} }
if err != nil { if err != nil {
return fmt.Errorf("fail to run '%s' for test '%s': %v", crowdsecCmd.String(), t.Name, err) return fmt.Errorf("fail to run '%s' for test '%s': %v", crowdsecCmd.String(), t.Name, err)
} }
@ -553,8 +574,10 @@ func (t *HubTestItem) Run() error {
if err != nil { if err != nil {
return err return err
} }
parserAssertFile.Close() parserAssertFile.Close()
} }
assertFileStat, err := os.Stat(t.ParserAssert.File) assertFileStat, err := os.Stat(t.ParserAssert.File)
if err != nil { if err != nil {
return fmt.Errorf("error while stats '%s': %s", t.ParserAssert.File, err) return fmt.Errorf("error while stats '%s': %s", t.ParserAssert.File, err)
@ -565,6 +588,7 @@ func (t *HubTestItem) Run() error {
if err != nil { if err != nil {
return fmt.Errorf("couldn't generate assertion: %s", err) return fmt.Errorf("couldn't generate assertion: %s", err)
} }
t.ParserAssert.AutoGenAssertData = assertData t.ParserAssert.AutoGenAssertData = assertData
t.ParserAssert.AutoGenAssert = true t.ParserAssert.AutoGenAssert = true
} else { } else {
@ -576,12 +600,15 @@ func (t *HubTestItem) Run() error {
// assert scenarios // assert scenarios
nbScenario := 0 nbScenario := 0
for _, scenario := range t.Config.Scenarios { for _, scenario := range t.Config.Scenarios {
if scenario == "" { if scenario == "" {
continue continue
} }
nbScenario += 1
nbScenario++
} }
if nbScenario > 0 { if nbScenario > 0 {
_, err := os.Stat(t.ScenarioAssert.File) _, err := os.Stat(t.ScenarioAssert.File)
if os.IsNotExist(err) { if os.IsNotExist(err) {
@ -589,8 +616,10 @@ func (t *HubTestItem) Run() error {
if err != nil { if err != nil {
return err return err
} }
scenarioAssertFile.Close() scenarioAssertFile.Close()
} }
assertFileStat, err := os.Stat(t.ScenarioAssert.File) assertFileStat, err := os.Stat(t.ScenarioAssert.File)
if err != nil { if err != nil {
return fmt.Errorf("error while stats '%s': %s", t.ScenarioAssert.File, err) return fmt.Errorf("error while stats '%s': %s", t.ScenarioAssert.File, err)
@ -601,6 +630,7 @@ func (t *HubTestItem) Run() error {
if err != nil { if err != nil {
return fmt.Errorf("couldn't generate assertion: %s", err) return fmt.Errorf("couldn't generate assertion: %s", err)
} }
t.ScenarioAssert.AutoGenAssertData = assertData t.ScenarioAssert.AutoGenAssertData = assertData
t.ScenarioAssert.AutoGenAssert = true t.ScenarioAssert.AutoGenAssert = true
} else { } else {

View file

@ -5,13 +5,11 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"regexp"
"sort" "sort"
"strings" "strings"
"time" "time"
"github.com/antonmedv/expr" "github.com/antonmedv/expr"
"github.com/antonmedv/expr/vm"
"github.com/enescakir/emoji" "github.com/enescakir/emoji"
"github.com/fatih/color" "github.com/fatih/color"
diff "github.com/r3labs/diff/v2" diff "github.com/r3labs/diff/v2"
@ -43,10 +41,10 @@ type ParserResult struct {
Evt types.Event Evt types.Event
Success bool Success bool
} }
type ParserResults map[string]map[string][]ParserResult type ParserResults map[string]map[string][]ParserResult
func NewParserAssert(file string) *ParserAssert { func NewParserAssert(file string) *ParserAssert {
ParserAssert := &ParserAssert{ ParserAssert := &ParserAssert{
File: file, File: file,
NbAssert: 0, NbAssert: 0,
@ -55,6 +53,7 @@ func NewParserAssert(file string) *ParserAssert {
AutoGenAssert: false, AutoGenAssert: false,
TestData: &ParserResults{}, TestData: &ParserResults{},
} }
return ParserAssert return ParserAssert
} }
@ -63,22 +62,24 @@ func (p *ParserAssert) AutoGenFromFile(filename string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
ret := p.AutoGenParserAssert() ret := p.AutoGenParserAssert()
return ret, nil return ret, nil
} }
func (p *ParserAssert) LoadTest(filename string) error { func (p *ParserAssert) LoadTest(filename string) error {
var err error
parserDump, err := LoadParserDump(filename) parserDump, err := LoadParserDump(filename)
if err != nil { if err != nil {
return fmt.Errorf("loading parser dump file: %+v", err) return fmt.Errorf("loading parser dump file: %+v", err)
} }
p.TestData = parserDump p.TestData = parserDump
return nil return nil
} }
func (p *ParserAssert) AssertFile(testFile string) error { func (p *ParserAssert) AssertFile(testFile string) error {
file, err := os.Open(p.File) file, err := os.Open(p.File)
if err != nil { if err != nil {
@ -88,19 +89,26 @@ func (p *ParserAssert) AssertFile(testFile string) error {
if err := p.LoadTest(testFile); err != nil { if err := p.LoadTest(testFile); err != nil {
return fmt.Errorf("unable to load parser dump file '%s': %s", testFile, err) return fmt.Errorf("unable to load parser dump file '%s': %s", testFile, err)
} }
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines) scanner.Split(bufio.ScanLines)
nbLine := 0 nbLine := 0
for scanner.Scan() { for scanner.Scan() {
nbLine += 1 nbLine++
if scanner.Text() == "" { if scanner.Text() == "" {
continue continue
} }
ok, err := p.Run(scanner.Text()) ok, err := p.Run(scanner.Text())
if err != nil { if err != nil {
return fmt.Errorf("unable to run assert '%s': %+v", scanner.Text(), err) return fmt.Errorf("unable to run assert '%s': %+v", scanner.Text(), err)
} }
p.NbAssert += 1
p.NbAssert++
if !ok { if !ok {
log.Debugf("%s is FALSE", scanner.Text()) log.Debugf("%s is FALSE", scanner.Text())
failedAssert := &AssertFail{ failedAssert := &AssertFail{
@ -109,37 +117,43 @@ func (p *ParserAssert) AssertFile(testFile string) error {
Expression: scanner.Text(), Expression: scanner.Text(),
Debug: make(map[string]string), Debug: make(map[string]string),
} }
variableRE := regexp.MustCompile(`(?P<variable>[^ =]+) == .*`)
match := variableRE.FindStringSubmatch(scanner.Text()) match := variableRE.FindStringSubmatch(scanner.Text())
variable := "" variable := ""
if len(match) == 0 { if len(match) == 0 {
log.Infof("Couldn't get variable of line '%s'", scanner.Text()) log.Infof("Couldn't get variable of line '%s'", scanner.Text())
variable = scanner.Text() variable = scanner.Text()
} else { } else {
variable = match[1] variable = match[1]
} }
result, err := p.EvalExpression(variable) result, err := p.EvalExpression(variable)
if err != nil { if err != nil {
log.Errorf("unable to evaluate variable '%s': %s", variable, err) log.Errorf("unable to evaluate variable '%s': %s", variable, err)
continue continue
} }
failedAssert.Debug[variable] = result failedAssert.Debug[variable] = result
p.Fails = append(p.Fails, *failedAssert) p.Fails = append(p.Fails, *failedAssert)
continue continue
} }
//fmt.Printf(" %s '%s'\n", emoji.GreenSquare, scanner.Text()) //fmt.Printf(" %s '%s'\n", emoji.GreenSquare, scanner.Text())
} }
file.Close() file.Close()
if p.NbAssert == 0 { if p.NbAssert == 0 {
assertData, err := p.AutoGenFromFile(testFile) assertData, err := p.AutoGenFromFile(testFile)
if err != nil { if err != nil {
return fmt.Errorf("couldn't generate assertion: %s", err) return fmt.Errorf("couldn't generate assertion: %s", err)
} }
p.AutoGenAssertData = assertData p.AutoGenAssertData = assertData
p.AutoGenAssert = true p.AutoGenAssert = true
} }
if len(p.Fails) == 0 { if len(p.Fails) == 0 {
p.Success = true p.Success = true
} }
@ -148,15 +162,14 @@ func (p *ParserAssert) AssertFile(testFile string) error {
} }
func (p *ParserAssert) RunExpression(expression string) (interface{}, error) { func (p *ParserAssert) RunExpression(expression string) (interface{}, error) {
var err error
//debug doesn't make much sense with the ability to evaluate "on the fly" //debug doesn't make much sense with the ability to evaluate "on the fly"
//var debugFilter *exprhelpers.ExprDebugger //var debugFilter *exprhelpers.ExprDebugger
var runtimeFilter *vm.Program
var output interface{} var output interface{}
env := map[string]interface{}{"results": *p.TestData} env := map[string]interface{}{"results": *p.TestData}
if runtimeFilter, err = expr.Compile(expression, exprhelpers.GetExprOptions(env)...); err != nil { runtimeFilter, err := expr.Compile(expression, exprhelpers.GetExprOptions(env)...)
if err != nil {
log.Errorf("failed to compile '%s' : %s", expression, err) log.Errorf("failed to compile '%s' : %s", expression, err)
return output, err return output, err
} }
@ -168,8 +181,10 @@ func (p *ParserAssert) RunExpression(expression string) (interface{}, error) {
if err != nil { if err != nil {
log.Warningf("running : %s", expression) log.Warningf("running : %s", expression)
log.Warningf("runtime error : %s", err) log.Warningf("runtime error : %s", err)
return output, fmt.Errorf("while running expression %s: %w", expression, err) return output, fmt.Errorf("while running expression %s: %w", expression, err)
} }
return output, nil return output, nil
} }
@ -178,10 +193,13 @@ func (p *ParserAssert) EvalExpression(expression string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
ret, err := yaml.Marshal(output) ret, err := yaml.Marshal(output)
if err != nil { if err != nil {
return "", err return "", err
} }
return string(ret), nil return string(ret), nil
} }
@ -190,6 +208,7 @@ func (p *ParserAssert) Run(assert string) (bool, error) {
if err != nil { if err != nil {
return false, err return false, err
} }
switch out := output.(type) { switch out := output.(type) {
case bool: case bool:
return out, nil return out, nil
@ -201,80 +220,89 @@ func (p *ParserAssert) Run(assert string) (bool, error) {
func Escape(val string) string { func Escape(val string) string {
val = strings.ReplaceAll(val, `\`, `\\`) val = strings.ReplaceAll(val, `\`, `\\`)
val = strings.ReplaceAll(val, `"`, `\"`) val = strings.ReplaceAll(val, `"`, `\"`)
return val return val
} }
func (p *ParserAssert) AutoGenParserAssert() string { func (p *ParserAssert) AutoGenParserAssert() string {
//attempt to autogen parser asserts //attempt to autogen parser asserts
var ret string ret := fmt.Sprintf("len(results) == %d\n", len(*p.TestData))
//sort map keys for consistent order
stages := sortedMapKeys(*p.TestData)
//sort map keys for consistent ordre
var stages []string
for stage := range *p.TestData {
stages = append(stages, stage)
}
sort.Strings(stages)
ret += fmt.Sprintf("len(results) == %d\n", len(*p.TestData))
for _, stage := range stages { for _, stage := range stages {
parsers := (*p.TestData)[stage] parsers := (*p.TestData)[stage]
//sort map keys for consistent ordre
var pnames []string //sort map keys for consistent order
for pname := range parsers { pnames := sortedMapKeys(parsers)
pnames = append(pnames, pname)
}
sort.Strings(pnames)
for _, parser := range pnames { for _, parser := range pnames {
presults := parsers[parser] presults := parsers[parser]
ret += fmt.Sprintf(`len(results["%s"]["%s"]) == %d`+"\n", stage, parser, len(presults)) ret += fmt.Sprintf(`len(results["%s"]["%s"]) == %d`+"\n", stage, parser, len(presults))
for pidx, result := range presults { for pidx, result := range presults {
ret += fmt.Sprintf(`results["%s"]["%s"][%d].Success == %t`+"\n", stage, parser, pidx, result.Success) ret += fmt.Sprintf(`results["%s"]["%s"][%d].Success == %t`+"\n", stage, parser, pidx, result.Success)
if !result.Success { if !result.Success {
continue continue
} }
for _, pkey := range sortedMapKeys(result.Evt.Parsed) { for _, pkey := range sortedMapKeys(result.Evt.Parsed) {
pval := result.Evt.Parsed[pkey] pval := result.Evt.Parsed[pkey]
if pval == "" { if pval == "" {
continue continue
} }
ret += fmt.Sprintf(`results["%s"]["%s"][%d].Evt.Parsed["%s"] == "%s"`+"\n", stage, parser, pidx, pkey, Escape(pval)) ret += fmt.Sprintf(`results["%s"]["%s"][%d].Evt.Parsed["%s"] == "%s"`+"\n", stage, parser, pidx, pkey, Escape(pval))
} }
for _, mkey := range sortedMapKeys(result.Evt.Meta) { for _, mkey := range sortedMapKeys(result.Evt.Meta) {
mval := result.Evt.Meta[mkey] mval := result.Evt.Meta[mkey]
if mval == "" { if mval == "" {
continue continue
} }
ret += fmt.Sprintf(`results["%s"]["%s"][%d].Evt.Meta["%s"] == "%s"`+"\n", stage, parser, pidx, mkey, Escape(mval)) ret += fmt.Sprintf(`results["%s"]["%s"][%d].Evt.Meta["%s"] == "%s"`+"\n", stage, parser, pidx, mkey, Escape(mval))
} }
for _, ekey := range sortedMapKeys(result.Evt.Enriched) { for _, ekey := range sortedMapKeys(result.Evt.Enriched) {
eval := result.Evt.Enriched[ekey] eval := result.Evt.Enriched[ekey]
if eval == "" { if eval == "" {
continue continue
} }
ret += fmt.Sprintf(`results["%s"]["%s"][%d].Evt.Enriched["%s"] == "%s"`+"\n", stage, parser, pidx, ekey, Escape(eval)) ret += fmt.Sprintf(`results["%s"]["%s"][%d].Evt.Enriched["%s"] == "%s"`+"\n", stage, parser, pidx, ekey, Escape(eval))
} }
for _, ukey := range sortedMapKeys(result.Evt.Unmarshaled) { for _, ukey := range sortedMapKeys(result.Evt.Unmarshaled) {
uval := result.Evt.Unmarshaled[ukey] uval := result.Evt.Unmarshaled[ukey]
if uval == "" { if uval == "" {
continue continue
} }
base := fmt.Sprintf(`results["%s"]["%s"][%d].Evt.Unmarshaled["%s"]`, stage, parser, pidx, ukey) base := fmt.Sprintf(`results["%s"]["%s"][%d].Evt.Unmarshaled["%s"]`, stage, parser, pidx, ukey)
for _, line := range p.buildUnmarshaledAssert(base, uval) { for _, line := range p.buildUnmarshaledAssert(base, uval) {
ret += line ret += line
} }
} }
ret += fmt.Sprintf(`results["%s"]["%s"][%d].Evt.Whitelisted == %t`+"\n", stage, parser, pidx, result.Evt.Whitelisted) ret += fmt.Sprintf(`results["%s"]["%s"][%d].Evt.Whitelisted == %t`+"\n", stage, parser, pidx, result.Evt.Whitelisted)
if result.Evt.WhitelistReason != "" { if result.Evt.WhitelistReason != "" {
ret += fmt.Sprintf(`results["%s"]["%s"][%d].Evt.WhitelistReason == "%s"`+"\n", stage, parser, pidx, Escape(result.Evt.WhitelistReason)) ret += fmt.Sprintf(`results["%s"]["%s"][%d].Evt.WhitelistReason == "%s"`+"\n", stage, parser, pidx, Escape(result.Evt.WhitelistReason))
} }
} }
} }
} }
return ret return ret
} }
func (p *ParserAssert) buildUnmarshaledAssert(ekey string, eval interface{}) []string { func (p *ParserAssert) buildUnmarshaledAssert(ekey string, eval interface{}) []string {
ret := make([]string, 0) ret := make([]string, 0)
switch val := eval.(type) { switch val := eval.(type) {
case map[string]interface{}: case map[string]interface{}:
for k, v := range val { for k, v := range val {
@ -297,12 +325,11 @@ func (p *ParserAssert) buildUnmarshaledAssert(ekey string, eval interface{}) []s
default: default:
log.Warningf("unknown type '%T' for key '%s'", val, ekey) log.Warningf("unknown type '%T' for key '%s'", val, ekey)
} }
return ret return ret
} }
func LoadParserDump(filepath string) (*ParserResults, error) { func LoadParserDump(filepath string) (*ParserResults, error) {
var pdump ParserResults
dumpData, err := os.Open(filepath) dumpData, err := os.Open(filepath)
if err != nil { if err != nil {
return nil, err return nil, err
@ -314,18 +341,19 @@ func LoadParserDump(filepath string) (*ParserResults, error) {
return nil, err return nil, err
} }
pdump := ParserResults{}
if err := yaml.Unmarshal(results, &pdump); err != nil { if err := yaml.Unmarshal(results, &pdump); err != nil {
return nil, err return nil, err
} }
/* we know that some variables should always be set, /* we know that some variables should always be set,
let's check if they're present in last parser output of last stage */ let's check if they're present in last parser output of last stage */
stages := make([]string, 0, len(pdump))
for k := range pdump { stages := sortedMapKeys(pdump)
stages = append(stages, k)
}
sort.Strings(stages)
var lastStage string var lastStage string
//Loop over stages to find last successful one with at least one parser //Loop over stages to find last successful one with at least one parser
for i := len(stages) - 2; i >= 0; i-- { for i := len(stages) - 2; i >= 0; i-- {
if len(pdump[stages[i]]) != 0 { if len(pdump[stages[i]]) != 0 {
@ -333,10 +361,13 @@ func LoadParserDump(filepath string) (*ParserResults, error) {
break break
} }
} }
parsers := make([]string, 0, len(pdump[lastStage])) parsers := make([]string, 0, len(pdump[lastStage]))
for k := range pdump[lastStage] { for k := range pdump[lastStage] {
parsers = append(parsers, k) parsers = append(parsers, k)
} }
sort.Strings(parsers) sort.Strings(parsers)
lastParser := parsers[len(parsers)-1] lastParser := parsers[len(parsers)-1]
@ -357,47 +388,51 @@ type DumpOpts struct {
ShowNotOkParsers bool ShowNotOkParsers bool
} }
func DumpTree(parser_results ParserResults, bucket_pour BucketPourInfo, opts DumpOpts) { func DumpTree(parserResults ParserResults, bucketPour BucketPourInfo, opts DumpOpts) {
//note : we can use line -> time as the unique identifier (of acquisition) //note : we can use line -> time as the unique identifier (of acquisition)
state := make(map[time.Time]map[string]map[string]ParserResult) state := make(map[time.Time]map[string]map[string]ParserResult)
assoc := make(map[time.Time]string, 0) assoc := make(map[time.Time]string, 0)
for stage, parsers := range parser_results { for stage, parsers := range parserResults {
for parser, results := range parsers { for parser, results := range parsers {
for _, parser_res := range results { for _, parserRes := range results {
evt := parser_res.Evt evt := parserRes.Evt
if _, ok := state[evt.Line.Time]; !ok { if _, ok := state[evt.Line.Time]; !ok {
state[evt.Line.Time] = make(map[string]map[string]ParserResult) state[evt.Line.Time] = make(map[string]map[string]ParserResult)
assoc[evt.Line.Time] = evt.Line.Raw assoc[evt.Line.Time] = evt.Line.Raw
} }
if _, ok := state[evt.Line.Time][stage]; !ok { if _, ok := state[evt.Line.Time][stage]; !ok {
state[evt.Line.Time][stage] = make(map[string]ParserResult) state[evt.Line.Time][stage] = make(map[string]ParserResult)
} }
state[evt.Line.Time][stage][parser] = ParserResult{Evt: evt, Success: parser_res.Success}
}
state[evt.Line.Time][stage][parser] = ParserResult{Evt: evt, Success: parserRes.Success}
}
} }
} }
for bname, evtlist := range bucket_pour { for bname, evtlist := range bucketPour {
for _, evt := range evtlist { for _, evt := range evtlist {
if evt.Line.Raw == "" { if evt.Line.Raw == "" {
continue continue
} }
//it might be bucket overflow being reprocessed, skip this //it might be bucket overflow being reprocessed, skip this
if _, ok := state[evt.Line.Time]; !ok { if _, ok := state[evt.Line.Time]; !ok {
state[evt.Line.Time] = make(map[string]map[string]ParserResult) state[evt.Line.Time] = make(map[string]map[string]ParserResult)
assoc[evt.Line.Time] = evt.Line.Raw assoc[evt.Line.Time] = evt.Line.Raw
} }
//there is a trick : to know if an event successfully exit the parsers, we check if it reached the pour() phase //there is a trick : to know if an event successfully exit the parsers, we check if it reached the pour() phase
//we thus use a fake stage "buckets" and a fake parser "OK" to know if it entered //we thus use a fake stage "buckets" and a fake parser "OK" to know if it entered
if _, ok := state[evt.Line.Time]["buckets"]; !ok { if _, ok := state[evt.Line.Time]["buckets"]; !ok {
state[evt.Line.Time]["buckets"] = make(map[string]ParserResult) state[evt.Line.Time]["buckets"] = make(map[string]ParserResult)
} }
state[evt.Line.Time]["buckets"][bname] = ParserResult{Success: true} state[evt.Line.Time]["buckets"][bname] = ParserResult{Success: true}
} }
} }
yellow := color.New(color.FgYellow).SprintFunc() yellow := color.New(color.FgYellow).SprintFunc()
red := color.New(color.FgRed).SprintFunc() red := color.New(color.FgRed).SprintFunc()
green := color.New(color.FgGreen).SprintFunc() green := color.New(color.FgGreen).SprintFunc()
@ -409,19 +444,25 @@ func DumpTree(parser_results ParserResults, bucket_pour BucketPourInfo, opts Dum
continue continue
} }
} }
fmt.Printf("line: %s\n", rawstr) fmt.Printf("line: %s\n", rawstr)
skeys := make([]string, 0, len(state[tstamp])) skeys := make([]string, 0, len(state[tstamp]))
for k := range state[tstamp] { for k := range state[tstamp] {
//there is a trick : to know if an event successfully exit the parsers, we check if it reached the pour() phase //there is a trick : to know if an event successfully exit the parsers, we check if it reached the pour() phase
//we thus use a fake stage "buckets" and a fake parser "OK" to know if it entered //we thus use a fake stage "buckets" and a fake parser "OK" to know if it entered
if k == "buckets" { if k == "buckets" {
continue continue
} }
skeys = append(skeys, k) skeys = append(skeys, k)
} }
sort.Strings(skeys) sort.Strings(skeys)
//iterate stage
var prev_item types.Event // iterate stage
var prevItem types.Event
for _, stage := range skeys { for _, stage := range skeys {
parsers := state[tstamp][stage] parsers := state[tstamp][stage]
@ -431,18 +472,16 @@ func DumpTree(parser_results ParserResults, bucket_pour BucketPourInfo, opts Dum
fmt.Printf("\t%s %s\n", sep, stage) fmt.Printf("\t%s %s\n", sep, stage)
pkeys := make([]string, 0, len(parsers)) pkeys := sortedMapKeys(parsers)
for k := range parsers {
pkeys = append(pkeys, k)
}
sort.Strings(pkeys)
for idx, parser := range pkeys { for idx, parser := range pkeys {
res := parsers[parser].Success res := parsers[parser].Success
sep := "├" sep := "├"
if idx == len(pkeys)-1 { if idx == len(pkeys)-1 {
sep = "└" sep = "└"
} }
created := 0 created := 0
updated := 0 updated := 0
deleted := 0 deleted := 0
@ -451,16 +490,19 @@ func DumpTree(parser_results ParserResults, bucket_pour BucketPourInfo, opts Dum
detailsDisplay := "" detailsDisplay := ""
if res { if res {
changelog, _ := diff.Diff(prev_item, parsers[parser].Evt) changelog, _ := diff.Diff(prevItem, parsers[parser].Evt)
for _, change := range changelog { for _, change := range changelog {
switch change.Type { switch change.Type {
case "create": case "create":
created++ created++
detailsDisplay += fmt.Sprintf("\t%s\t\t%s %s evt.%s : %s\n", presep, sep, change.Type, strings.Join(change.Path, "."), green(change.To)) detailsDisplay += fmt.Sprintf("\t%s\t\t%s %s evt.%s : %s\n", presep, sep, change.Type, strings.Join(change.Path, "."), green(change.To))
case "update": case "update":
detailsDisplay += fmt.Sprintf("\t%s\t\t%s %s evt.%s : %s -> %s\n", presep, sep, change.Type, strings.Join(change.Path, "."), change.From, yellow(change.To)) detailsDisplay += fmt.Sprintf("\t%s\t\t%s %s evt.%s : %s -> %s\n", presep, sep, change.Type, strings.Join(change.Path, "."), change.From, yellow(change.To))
if change.Path[0] == "Whitelisted" && change.To == true { if change.Path[0] == "Whitelisted" && change.To == true {
whitelisted = true whitelisted = true
if whitelistReason == "" { if whitelistReason == "" {
whitelistReason = parsers[parser].Evt.WhitelistReason whitelistReason = parsers[parser].Evt.WhitelistReason
} }
@ -468,51 +510,64 @@ func DumpTree(parser_results ParserResults, bucket_pour BucketPourInfo, opts Dum
updated++ updated++
case "delete": case "delete":
deleted++ deleted++
detailsDisplay += fmt.Sprintf("\t%s\t\t%s %s evt.%s\n", presep, sep, change.Type, red(strings.Join(change.Path, "."))) detailsDisplay += fmt.Sprintf("\t%s\t\t%s %s evt.%s\n", presep, sep, change.Type, red(strings.Join(change.Path, ".")))
} }
} }
prev_item = parsers[parser].Evt
prevItem = parsers[parser].Evt
} }
if created > 0 { if created > 0 {
changeStr += green(fmt.Sprintf("+%d", created)) changeStr += green(fmt.Sprintf("+%d", created))
} }
if updated > 0 { if updated > 0 {
if len(changeStr) > 0 { if len(changeStr) > 0 {
changeStr += " " changeStr += " "
} }
changeStr += yellow(fmt.Sprintf("~%d", updated)) changeStr += yellow(fmt.Sprintf("~%d", updated))
} }
if deleted > 0 { if deleted > 0 {
if len(changeStr) > 0 { if len(changeStr) > 0 {
changeStr += " " changeStr += " "
} }
changeStr += red(fmt.Sprintf("-%d", deleted)) changeStr += red(fmt.Sprintf("-%d", deleted))
} }
if whitelisted { if whitelisted {
if len(changeStr) > 0 { if len(changeStr) > 0 {
changeStr += " " changeStr += " "
} }
changeStr += red("[whitelisted]") changeStr += red("[whitelisted]")
} }
if changeStr == "" { if changeStr == "" {
changeStr = yellow("unchanged") changeStr = yellow("unchanged")
} }
if res { if res {
fmt.Printf("\t%s\t%s %s %s (%s)\n", presep, sep, emoji.GreenCircle, parser, changeStr) fmt.Printf("\t%s\t%s %s %s (%s)\n", presep, sep, emoji.GreenCircle, parser, changeStr)
if opts.Details { if opts.Details {
fmt.Print(detailsDisplay) fmt.Print(detailsDisplay)
} }
} else if opts.ShowNotOkParsers { } else if opts.ShowNotOkParsers {
fmt.Printf("\t%s\t%s %s %s\n", presep, sep, emoji.RedCircle, parser) fmt.Printf("\t%s\t%s %s %s\n", presep, sep, emoji.RedCircle, parser)
} }
} }
} }
sep := "└" sep := "└"
if len(state[tstamp]["buckets"]) > 0 { if len(state[tstamp]["buckets"]) > 0 {
sep = "├" sep = "├"
} }
//did the event enter the bucket pour phase ? //did the event enter the bucket pour phase ?
if _, ok := state[tstamp]["buckets"]["OK"]; ok { if _, ok := state[tstamp]["buckets"]["OK"]; ok {
fmt.Printf("\t%s-------- parser success %s\n", sep, emoji.GreenCircle) fmt.Printf("\t%s-------- parser success %s\n", sep, emoji.GreenCircle)
@ -521,27 +576,35 @@ func DumpTree(parser_results ParserResults, bucket_pour BucketPourInfo, opts Dum
} else { } else {
fmt.Printf("\t%s-------- parser failure %s\n", sep, emoji.RedCircle) fmt.Printf("\t%s-------- parser failure %s\n", sep, emoji.RedCircle)
} }
//now print bucket info //now print bucket info
if len(state[tstamp]["buckets"]) > 0 { if len(state[tstamp]["buckets"]) > 0 {
fmt.Printf("\t├ Scenarios\n") fmt.Printf("\t├ Scenarios\n")
} }
bnames := make([]string, 0, len(state[tstamp]["buckets"])) bnames := make([]string, 0, len(state[tstamp]["buckets"]))
for k := range state[tstamp]["buckets"] { for k := range state[tstamp]["buckets"] {
//there is a trick : to know if an event successfully exit the parsers, we check if it reached the pour() phase //there is a trick : to know if an event successfully exit the parsers, we check if it reached the pour() phase
//we thus use a fake stage "buckets" and a fake parser "OK" to know if it entered //we thus use a fake stage "buckets" and a fake parser "OK" to know if it entered
if k == "OK" { if k == "OK" {
continue continue
} }
bnames = append(bnames, k) bnames = append(bnames, k)
} }
sort.Strings(bnames) sort.Strings(bnames)
for idx, bname := range bnames { for idx, bname := range bnames {
sep := "├" sep := "├"
if idx == len(bnames)-1 { if idx == len(bnames)-1 {
sep = "└" sep = "└"
} }
fmt.Printf("\t\t%s %s %s\n", sep, emoji.GreenCircle, bname) fmt.Printf("\t\t%s %s %s\n", sep, emoji.GreenCircle, bname)
} }
fmt.Println() fmt.Println()
} }
} }

11
pkg/hubtest/regexp.go Normal file
View file

@ -0,0 +1,11 @@
package hubtest
import (
"regexp"
)
var (
variableRE = regexp.MustCompile(`(?P<variable>[^ =]+) == .*`)
parserResultRE = regexp.MustCompile(`^results\["[^"]+"\]\["(?P<parser>[^"]+)"\]\[[0-9]+\]\.Evt\..*`)
scenarioResultRE = regexp.MustCompile(`^results\[[0-9]+\].Overflow.Alert.GetScenario\(\) == "(?P<scenario>[^"]+)"`)
)

View file

@ -5,12 +5,10 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"regexp"
"sort" "sort"
"strings" "strings"
"github.com/antonmedv/expr" "github.com/antonmedv/expr"
"github.com/antonmedv/expr/vm"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
@ -42,6 +40,7 @@ func NewScenarioAssert(file string) *ScenarioAssert {
TestData: &BucketResults{}, TestData: &BucketResults{},
PourData: &BucketPourInfo{}, PourData: &BucketPourInfo{},
} }
return ScenarioAssert return ScenarioAssert
} }
@ -50,7 +49,9 @@ func (s *ScenarioAssert) AutoGenFromFile(filename string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
ret := s.AutoGenScenarioAssert() ret := s.AutoGenScenarioAssert()
return ret, nil return ret, nil
} }
@ -59,6 +60,7 @@ func (s *ScenarioAssert) LoadTest(filename string, bucketpour string) error {
if err != nil { if err != nil {
return fmt.Errorf("loading scenario dump file '%s': %+v", filename, err) return fmt.Errorf("loading scenario dump file '%s': %+v", filename, err)
} }
s.TestData = bucketDump s.TestData = bucketDump
if bucketpour != "" { if bucketpour != "" {
@ -66,8 +68,10 @@ func (s *ScenarioAssert) LoadTest(filename string, bucketpour string) error {
if err != nil { if err != nil {
return fmt.Errorf("loading bucket pour dump file '%s': %+v", filename, err) return fmt.Errorf("loading bucket pour dump file '%s': %+v", filename, err)
} }
s.PourData = pourDump s.PourData = pourDump
} }
return nil return nil
} }
@ -81,19 +85,26 @@ func (s *ScenarioAssert) AssertFile(testFile string) error {
if err := s.LoadTest(testFile, ""); err != nil { if err := s.LoadTest(testFile, ""); err != nil {
return fmt.Errorf("unable to load parser dump file '%s': %s", testFile, err) return fmt.Errorf("unable to load parser dump file '%s': %s", testFile, err)
} }
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines) scanner.Split(bufio.ScanLines)
nbLine := 0 nbLine := 0
for scanner.Scan() { for scanner.Scan() {
nbLine += 1 nbLine++
if scanner.Text() == "" { if scanner.Text() == "" {
continue continue
} }
ok, err := s.Run(scanner.Text()) ok, err := s.Run(scanner.Text())
if err != nil { if err != nil {
return fmt.Errorf("unable to run assert '%s': %+v", scanner.Text(), err) return fmt.Errorf("unable to run assert '%s': %+v", scanner.Text(), err)
} }
s.NbAssert += 1
s.NbAssert++
if !ok { if !ok {
log.Debugf("%s is FALSE", scanner.Text()) log.Debugf("%s is FALSE", scanner.Text())
failedAssert := &AssertFail{ failedAssert := &AssertFail{
@ -102,31 +113,38 @@ func (s *ScenarioAssert) AssertFile(testFile string) error {
Expression: scanner.Text(), Expression: scanner.Text(),
Debug: make(map[string]string), Debug: make(map[string]string),
} }
variableRE := regexp.MustCompile(`(?P<variable>[^ ]+) == .*`)
match := variableRE.FindStringSubmatch(scanner.Text()) match := variableRE.FindStringSubmatch(scanner.Text())
if len(match) == 0 { if len(match) == 0 {
log.Infof("Couldn't get variable of line '%s'", scanner.Text()) log.Infof("Couldn't get variable of line '%s'", scanner.Text())
continue continue
} }
variable := match[1] variable := match[1]
result, err := s.EvalExpression(variable) result, err := s.EvalExpression(variable)
if err != nil { if err != nil {
log.Errorf("unable to evaluate variable '%s': %s", variable, err) log.Errorf("unable to evaluate variable '%s': %s", variable, err)
continue continue
} }
failedAssert.Debug[variable] = result failedAssert.Debug[variable] = result
s.Fails = append(s.Fails, *failedAssert) s.Fails = append(s.Fails, *failedAssert)
continue continue
} }
//fmt.Printf(" %s '%s'\n", emoji.GreenSquare, scanner.Text()) //fmt.Printf(" %s '%s'\n", emoji.GreenSquare, scanner.Text())
} }
file.Close() file.Close()
if s.NbAssert == 0 { if s.NbAssert == 0 {
assertData, err := s.AutoGenFromFile(testFile) assertData, err := s.AutoGenFromFile(testFile)
if err != nil { if err != nil {
return fmt.Errorf("couldn't generate assertion: %s", err) return fmt.Errorf("couldn't generate assertion: %s", err)
} }
s.AutoGenAssertData = assertData s.AutoGenAssertData = assertData
s.AutoGenAssert = true s.AutoGenAssert = true
} }
@ -139,15 +157,14 @@ func (s *ScenarioAssert) AssertFile(testFile string) error {
} }
func (s *ScenarioAssert) RunExpression(expression string) (interface{}, error) { func (s *ScenarioAssert) RunExpression(expression string) (interface{}, error) {
var err error
//debug doesn't make much sense with the ability to evaluate "on the fly" //debug doesn't make much sense with the ability to evaluate "on the fly"
//var debugFilter *exprhelpers.ExprDebugger //var debugFilter *exprhelpers.ExprDebugger
var runtimeFilter *vm.Program
var output interface{} var output interface{}
env := map[string]interface{}{"results": *s.TestData} env := map[string]interface{}{"results": *s.TestData}
if runtimeFilter, err = expr.Compile(expression, exprhelpers.GetExprOptions(env)...); err != nil { runtimeFilter, err := expr.Compile(expression, exprhelpers.GetExprOptions(env)...)
if err != nil {
return nil, err return nil, err
} }
// if debugFilter, err = exprhelpers.NewDebugger(assert, expr.Env(env)); err != nil { // if debugFilter, err = exprhelpers.NewDebugger(assert, expr.Env(env)); err != nil {
@ -161,8 +178,10 @@ func (s *ScenarioAssert) RunExpression(expression string) (interface{}, error) {
if err != nil { if err != nil {
log.Warningf("running : %s", expression) log.Warningf("running : %s", expression)
log.Warningf("runtime error : %s", err) log.Warningf("runtime error : %s", err)
return nil, fmt.Errorf("while running expression %s: %w", expression, err) return nil, fmt.Errorf("while running expression %s: %w", expression, err)
} }
return output, nil return output, nil
} }
@ -171,10 +190,12 @@ func (s *ScenarioAssert) EvalExpression(expression string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
ret, err := yaml.Marshal(output) ret, err := yaml.Marshal(output)
if err != nil { if err != nil {
return "", err return "", err
} }
return string(ret), nil return string(ret), nil
} }
@ -183,6 +204,7 @@ func (s *ScenarioAssert) Run(assert string) (bool, error) {
if err != nil { if err != nil {
return false, err return false, err
} }
switch out := output.(type) { switch out := output.(type) {
case bool: case bool:
return out, nil return out, nil
@ -192,9 +214,9 @@ func (s *ScenarioAssert) Run(assert string) (bool, error) {
} }
func (s *ScenarioAssert) AutoGenScenarioAssert() string { func (s *ScenarioAssert) AutoGenScenarioAssert() string {
//attempt to autogen parser asserts // attempt to autogen scenario asserts
var ret string ret := fmt.Sprintf(`len(results) == %d`+"\n", len(*s.TestData))
ret += fmt.Sprintf(`len(results) == %d`+"\n", len(*s.TestData))
for eventIndex, event := range *s.TestData { for eventIndex, event := range *s.TestData {
for ipSrc, source := range event.Overflow.Sources { for ipSrc, source := range event.Overflow.Sources {
ret += fmt.Sprintf(`"%s" in results[%d].Overflow.GetSources()`+"\n", ipSrc, eventIndex) ret += fmt.Sprintf(`"%s" in results[%d].Overflow.GetSources()`+"\n", ipSrc, eventIndex)
@ -203,15 +225,18 @@ func (s *ScenarioAssert) AutoGenScenarioAssert() string {
ret += fmt.Sprintf(`results[%d].Overflow.Sources["%s"].GetScope() == "%s"`+"\n", eventIndex, ipSrc, *source.Scope) ret += fmt.Sprintf(`results[%d].Overflow.Sources["%s"].GetScope() == "%s"`+"\n", eventIndex, ipSrc, *source.Scope)
ret += fmt.Sprintf(`results[%d].Overflow.Sources["%s"].GetValue() == "%s"`+"\n", eventIndex, ipSrc, *source.Value) ret += fmt.Sprintf(`results[%d].Overflow.Sources["%s"].GetValue() == "%s"`+"\n", eventIndex, ipSrc, *source.Value)
} }
for evtIndex, evt := range event.Overflow.Alert.Events { for evtIndex, evt := range event.Overflow.Alert.Events {
for _, meta := range evt.Meta { for _, meta := range evt.Meta {
ret += fmt.Sprintf(`results[%d].Overflow.Alert.Events[%d].GetMeta("%s") == "%s"`+"\n", eventIndex, evtIndex, meta.Key, Escape(meta.Value)) ret += fmt.Sprintf(`results[%d].Overflow.Alert.Events[%d].GetMeta("%s") == "%s"`+"\n", eventIndex, evtIndex, meta.Key, Escape(meta.Value))
} }
} }
ret += fmt.Sprintf(`results[%d].Overflow.Alert.GetScenario() == "%s"`+"\n", eventIndex, *event.Overflow.Alert.Scenario) ret += fmt.Sprintf(`results[%d].Overflow.Alert.GetScenario() == "%s"`+"\n", eventIndex, *event.Overflow.Alert.Scenario)
ret += fmt.Sprintf(`results[%d].Overflow.Alert.Remediation == %t`+"\n", eventIndex, event.Overflow.Alert.Remediation) ret += fmt.Sprintf(`results[%d].Overflow.Alert.Remediation == %t`+"\n", eventIndex, event.Overflow.Alert.Remediation)
ret += fmt.Sprintf(`results[%d].Overflow.Alert.GetEventsCount() == %d`+"\n", eventIndex, *event.Overflow.Alert.EventsCount) ret += fmt.Sprintf(`results[%d].Overflow.Alert.GetEventsCount() == %d`+"\n", eventIndex, *event.Overflow.Alert.EventsCount)
} }
return ret return ret
} }
@ -228,8 +253,6 @@ func (b BucketResults) Swap(i, j int) {
} }
func LoadBucketPourDump(filepath string) (*BucketPourInfo, error) { func LoadBucketPourDump(filepath string) (*BucketPourInfo, error) {
var bucketDump BucketPourInfo
dumpData, err := os.Open(filepath) dumpData, err := os.Open(filepath)
if err != nil { if err != nil {
return nil, err return nil, err
@ -241,6 +264,8 @@ func LoadBucketPourDump(filepath string) (*BucketPourInfo, error) {
return nil, err return nil, err
} }
var bucketDump BucketPourInfo
if err := yaml.Unmarshal(results, &bucketDump); err != nil { if err := yaml.Unmarshal(results, &bucketDump); err != nil {
return nil, err return nil, err
} }
@ -249,8 +274,6 @@ func LoadBucketPourDump(filepath string) (*BucketPourInfo, error) {
} }
func LoadScenarioDump(filepath string) (*BucketResults, error) { func LoadScenarioDump(filepath string) (*BucketResults, error) {
var bucketDump BucketResults
dumpData, err := os.Open(filepath) dumpData, err := os.Open(filepath)
if err != nil { if err != nil {
return nil, err return nil, err
@ -262,6 +285,8 @@ func LoadScenarioDump(filepath string) (*BucketResults, error) {
return nil, err return nil, err
} }
var bucketDump BucketResults
if err := yaml.Unmarshal(results, &bucketDump); err != nil { if err := yaml.Unmarshal(results, &bucketDump); err != nil {
return nil, err return nil, err
} }

View file

@ -12,7 +12,9 @@ func sortedMapKeys[V any](m map[string]V) []string {
for k := range m { for k := range m {
keys = append(keys, k) keys = append(keys, k)
} }
sort.Strings(keys) sort.Strings(keys)
return keys return keys
} }
@ -43,16 +45,20 @@ func checkPathNotContained(path string, subpath string) error {
} }
current := absSubPath current := absSubPath
for { for {
if current == absPath { if current == absPath {
return fmt.Errorf("cannot copy a folder onto itself") return fmt.Errorf("cannot copy a folder onto itself")
} }
up := filepath.Dir(current) up := filepath.Dir(current)
if current == up { if current == up {
break break
} }
current = up current = up
} }
return nil return nil
} }

View file

@ -3,16 +3,16 @@ package hubtest
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/require"
) )
func TestCheckPathNotContained(t *testing.T) { func TestCheckPathNotContained(t *testing.T) {
assert.Nil(t, checkPathNotContained("/foo", "/bar")) require.NoError(t, checkPathNotContained("/foo", "/bar"))
assert.Nil(t, checkPathNotContained("/foo/bar", "/foo")) require.NoError(t, checkPathNotContained("/foo/bar", "/foo"))
assert.Nil(t, checkPathNotContained("/foo/bar", "/")) require.NoError(t, checkPathNotContained("/foo/bar", "/"))
assert.Nil(t, checkPathNotContained("/path/to/somewhere", "/path/to/somewhere-else")) require.NoError(t, checkPathNotContained("/path/to/somewhere", "/path/to/somewhere-else"))
assert.Nil(t, checkPathNotContained("~/.local/path/to/somewhere", "~/.local/path/to/somewhere-else")) require.NoError(t, checkPathNotContained("~/.local/path/to/somewhere", "~/.local/path/to/somewhere-else"))
assert.NotNil(t, checkPathNotContained("/foo", "/foo/bar")) require.Error(t, checkPathNotContained("/foo", "/foo/bar"))
assert.NotNil(t, checkPathNotContained("/", "/foo")) require.Error(t, checkPathNotContained("/", "/foo"))
assert.NotNil(t, checkPathNotContained("/", "/foo/bar/baz")) require.Error(t, checkPathNotContained("/", "/foo/bar/baz"))
} }

View file

@ -62,6 +62,14 @@ teardown() {
assert_output --partial 'crowdsecurity/linux' assert_output --partial 'crowdsecurity/linux'
} }
@test "missing reference in hub index" {
new_hub=$(jq <"$HUB_DIR/.index.json" 'del(.parsers."crowdsecurity/smb-logs") | del (.scenarios."crowdsecurity/mysql-bf")')
echo "$new_hub" >"$HUB_DIR/.index.json"
rune -0 cscli hub list --error
assert_stderr --partial "Referred parsers crowdsecurity/smb-logs in collection crowdsecurity/smb doesn't exist."
assert_stderr --partial "Referred scenarios crowdsecurity/mysql-bf in collection crowdsecurity/mysql doesn't exist."
}
@test "cscli hub update" { @test "cscli hub update" {
#XXX: todo #XXX: todo
: :

View file

@ -79,26 +79,35 @@ teardown() {
# XXX: check alphabetical order in human, json, raw # XXX: check alphabetical order in human, json, raw
} }
@test "cscli collections list [collection]..." { @test "cscli collections list [collection]..." {
# non-existent
rune -1 cscli collections install foo/bar
assert_stderr --partial "can't find 'foo/bar' in collections"
# not installed
rune -0 cscli collections list crowdsecurity/smb
assert_output --regexp 'crowdsecurity/smb.*disabled'
# install two items
rune -0 cscli collections install crowdsecurity/sshd crowdsecurity/smb rune -0 cscli collections install crowdsecurity/sshd crowdsecurity/smb
# list one item # list an installed item
rune -0 cscli collections list crowdsecurity/sshd rune -0 cscli collections list crowdsecurity/sshd
assert_output --partial "crowdsecurity/sshd" assert_output --regexp "crowdsecurity/sshd"
refute_output --partial "crowdsecurity/smb" refute_output --partial "crowdsecurity/smb"
# list multiple items # list multiple installed and non installed items
rune -0 cscli collections list crowdsecurity/sshd crowdsecurity/smb rune -0 cscli collections list crowdsecurity/sshd crowdsecurity/smb crowdsecurity/nginx
assert_output --partial "crowdsecurity/sshd" assert_output --partial "crowdsecurity/sshd"
assert_output --partial "crowdsecurity/smb" assert_output --partial "crowdsecurity/smb"
assert_output --partial "crowdsecurity/nginx"
rune -0 cscli collections list crowdsecurity/sshd -o json rune -0 cscli collections list crowdsecurity/sshd -o json
rune -0 jq '.collections | length' <(output) rune -0 jq '.collections | length' <(output)
assert_output "1" assert_output "1"
rune -0 cscli collections list crowdsecurity/sshd crowdsecurity/smb -o json rune -0 cscli collections list crowdsecurity/sshd crowdsecurity/smb crowdsecurity/nginx -o json
rune -0 jq '.collections | length' <(output) rune -0 jq '.collections | length' <(output)
assert_output "2" assert_output "3"
rune -0 cscli collections list crowdsecurity/sshd -o raw rune -0 cscli collections list crowdsecurity/sshd -o raw
rune -0 grep -vc 'name,status,version,description' <(output) rune -0 grep -vc 'name,status,version,description' <(output)
@ -108,14 +117,6 @@ teardown() {
assert_output "2" assert_output "2"
} }
@test "cscli collections list [collection]... (not installed / not existing)" {
skip "not implemented yet"
# not installed
rune -1 cscli collections list crowdsecurity/sshd
# not existing
rune -1 cscli collections list blahblah/blahblah
}
@test "cscli collections install [collection]..." { @test "cscli collections install [collection]..." {
rune -1 cscli collections install rune -1 cscli collections install
assert_stderr --partial 'requires at least 1 arg(s), only received 0' assert_stderr --partial 'requires at least 1 arg(s), only received 0'
@ -132,7 +133,7 @@ teardown() {
# autocorrect # autocorrect
rune -1 cscli collections install crowdsecurity/ssshd rune -1 cscli collections install crowdsecurity/ssshd
assert_stderr --partial "can't find 'crowdsecurity/ssshd' in collections, did you mean crowdsecurity/sshd?" assert_stderr --partial "can't find 'crowdsecurity/ssshd' in collections, did you mean 'crowdsecurity/sshd'?"
# install multiple # install multiple
rune -0 cscli collections install crowdsecurity/sshd crowdsecurity/smb rune -0 cscli collections install crowdsecurity/sshd crowdsecurity/smb

View file

@ -0,0 +1,72 @@
#!/usr/bin/env bats
# vim: ft=bats:list:ts=8:sts=4:sw=4:et:ai:si:
set -u
setup_file() {
load "../lib/setup_file.sh"
./instance-data load
HUB_DIR=$(config_get '.config_paths.hub_dir')
export HUB_DIR
CONFIG_DIR=$(config_get '.config_paths.config_dir')
export CONFIG_DIR
}
teardown_file() {
load "../lib/teardown_file.sh"
}
setup() {
load "../lib/setup.sh"
load "../lib/bats-file/load.bash"
./instance-data load
hub_purge_all
hub_strip_index
}
teardown() {
./instance-crowdsec stop
}
#----------
#
# Tests that don't need to be repeated for each hub type
#
@test "hub versions are correctly sorted during sync" {
# hash of an empty file
sha256_empty="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
# add two versions with the same hash, that don't sort the same way
# in a lexical vs semver sort. CrowdSec should report the latest version
new_hub=$( \
jq --arg DIGEST "$sha256_empty" <"$HUB_DIR/.index.json" \
'. * {collections:{"crowdsecurity/sshd":{"versions":{"1.2":{"digest":$DIGEST, "deprecated": false}, "1.10": {"digest":$DIGEST, "deprecated": false}}}}}' \
)
echo "$new_hub" >"$HUB_DIR/.index.json"
rune -0 cscli collections install crowdsecurity/sshd
truncate -s 0 "$CONFIG_DIR/collections/sshd.yaml"
rune -0 cscli collections inspect crowdsecurity/sshd -o json
# XXX: is this supposed to be tainted or up to date?
rune -0 jq -c '[.local_version,.up_to_date,.tainted]' <(output)
assert_json '["1.10",null,null]'
}
@test "hub index with invalid (non semver) version numbers" {
new_hub=$( \
jq <"$HUB_DIR/.index.json" \
'. * {collections:{"crowdsecurity/sshd":{"versions":{"1.2.3.4":{"digest":"foo", "deprecated": false}}}}}' \
)
echo "$new_hub" >"$HUB_DIR/.index.json"
rune -0 cscli collections install crowdsecurity/sshd
rune -1 cscli collections inspect crowdsecurity/sshd --no-metrics
# XXX: we are on the verbose side here...
assert_stderr --partial "failed to read Hub index: failed to sync items: failed to scan $CONFIG_DIR: while syncing collections sshd.yaml: 1.2.3.4: Invalid Semantic Version"
}

View file

@ -133,7 +133,7 @@ teardown() {
# autocorrect # autocorrect
rune -1 cscli parsers install crowdsecurity/sshd-logz rune -1 cscli parsers install crowdsecurity/sshd-logz
assert_stderr --partial "can't find 'crowdsecurity/sshd-logz' in parsers, did you mean crowdsecurity/sshd-logs?" assert_stderr --partial "can't find 'crowdsecurity/sshd-logz' in parsers, did you mean 'crowdsecurity/sshd-logs'?"
# install multiple # install multiple
rune -0 cscli parsers install crowdsecurity/pgsql-logs crowdsecurity/postfix-logs rune -0 cscli parsers install crowdsecurity/pgsql-logs crowdsecurity/postfix-logs

View file

@ -79,41 +79,43 @@ teardown() {
# XXX: check alphabetical order in human, json, raw # XXX: check alphabetical order in human, json, raw
} }
@test "cscli postoverflows list [scenario]..." { @test "cscli postoverflows list [scenario]..." {
# non-existent
rune -1 cscli postoverflows install foo/bar
assert_stderr --partial "can't find 'foo/bar' in postoverflows"
# not installed
rune -0 cscli postoverflows list crowdsecurity/rdns
assert_output --regexp 'crowdsecurity/rdns.*disabled'
# install two items
rune -0 cscli postoverflows install crowdsecurity/rdns crowdsecurity/cdn-whitelist rune -0 cscli postoverflows install crowdsecurity/rdns crowdsecurity/cdn-whitelist
# list one item # list an installed item
rune -0 cscli postoverflows list crowdsecurity/rdns rune -0 cscli postoverflows list crowdsecurity/rdns
assert_output --partial "crowdsecurity/rdns" assert_output --regexp "crowdsecurity/rdns.*enabled"
refute_output --partial "crowdsecurity/cdn-whitelist" refute_output --partial "crowdsecurity/cdn-whitelist"
# list multiple items # list multiple installed and non installed items
rune -0 cscli postoverflows list crowdsecurity/rdns crowdsecurity/cdn-whitelist rune -0 cscli postoverflows list crowdsecurity/rdns crowdsecurity/cdn-whitelist crowdsecurity/ipv6_to_range
assert_output --partial "crowdsecurity/rdns" assert_output --partial "crowdsecurity/rdns"
assert_output --partial "crowdsecurity/cdn-whitelist" assert_output --partial "crowdsecurity/cdn-whitelist"
assert_output --partial "crowdsecurity/ipv6_to_range"
rune -0 cscli postoverflows list crowdsecurity/rdns -o json rune -0 cscli postoverflows list crowdsecurity/rdns -o json
rune -0 jq '.postoverflows | length' <(output) rune -0 jq '.postoverflows | length' <(output)
assert_output "1" assert_output "1"
rune -0 cscli postoverflows list crowdsecurity/rdns crowdsecurity/cdn-whitelist -o json rune -0 cscli postoverflows list crowdsecurity/rdns crowdsecurity/cdn-whitelist crowdsecurity/ipv6_to_range -o json
rune -0 jq '.postoverflows | length' <(output) rune -0 jq '.postoverflows | length' <(output)
assert_output "2" assert_output "3"
rune -0 cscli postoverflows list crowdsecurity/rdns -o raw rune -0 cscli postoverflows list crowdsecurity/rdns -o raw
rune -0 grep -vc 'name,status,version,description' <(output) rune -0 grep -vc 'name,status,version,description' <(output)
assert_output "1" assert_output "1"
rune -0 cscli postoverflows list crowdsecurity/rdns crowdsecurity/cdn-whitelist -o raw rune -0 cscli postoverflows list crowdsecurity/rdns crowdsecurity/cdn-whitelist crowdsecurity/ipv6_to_range -o raw
rune -0 grep -vc 'name,status,version,description' <(output) rune -0 grep -vc 'name,status,version,description' <(output)
assert_output "2" assert_output "3"
}
@test "cscli postoverflows list [scenario]... (not installed / not existing)" {
skip "not implemented yet"
# not installed
rune -1 cscli postoverflows list crowdsecurity/rdns
# not existing
rune -1 cscli postoverflows list blahblah/blahblah
} }
@test "cscli postoverflows install [scenario]..." { @test "cscli postoverflows install [scenario]..." {
@ -132,7 +134,7 @@ teardown() {
# autocorrect # autocorrect
rune -1 cscli postoverflows install crowdsecurity/rdnf rune -1 cscli postoverflows install crowdsecurity/rdnf
assert_stderr --partial "can't find 'crowdsecurity/rdnf' in postoverflows, did you mean crowdsecurity/rdns?" assert_stderr --partial "can't find 'crowdsecurity/rdnf' in postoverflows, did you mean 'crowdsecurity/rdns'?"
# install multiple # install multiple
rune -0 cscli postoverflows install crowdsecurity/rdns crowdsecurity/cdn-whitelist rune -0 cscli postoverflows install crowdsecurity/rdns crowdsecurity/cdn-whitelist
@ -157,6 +159,8 @@ teardown() {
assert_file_exists "$CONFIG_DIR/postoverflows/s00-enrich/rdns.yaml" assert_file_exists "$CONFIG_DIR/postoverflows/s00-enrich/rdns.yaml"
} }
# XXX: test install with --force
# XXX: test install with --ignore
@test "cscli postoverflows inspect [scenario]..." { @test "cscli postoverflows inspect [scenario]..." {
rune -1 cscli postoverflows inspect rune -1 cscli postoverflows inspect

View file

@ -133,7 +133,7 @@ teardown() {
# autocorrect # autocorrect
rune -1 cscli scenarios install crowdsecurity/ssh-tf rune -1 cscli scenarios install crowdsecurity/ssh-tf
assert_stderr --partial "can't find 'crowdsecurity/ssh-tf' in scenarios, did you mean crowdsecurity/ssh-bf?" assert_stderr --partial "can't find 'crowdsecurity/ssh-tf' in scenarios, did you mean 'crowdsecurity/ssh-bf'?"
# install multiple # install multiple
rune -0 cscli scenarios install crowdsecurity/ssh-bf crowdsecurity/telnet-bf rune -0 cscli scenarios install crowdsecurity/ssh-bf crowdsecurity/telnet-bf
@ -145,7 +145,6 @@ teardown() {
assert_output --partial 'installed: true' assert_output --partial 'installed: true'
} }
@test "cscli scenarios install [scenario]... (file location and download-only)" { @test "cscli scenarios install [scenario]... (file location and download-only)" {
# simple install # simple install
rune -0 cscli scenarios install crowdsecurity/ssh-bf --download-only rune -0 cscli scenarios install crowdsecurity/ssh-bf --download-only
@ -159,6 +158,9 @@ teardown() {
assert_file_exists "$CONFIG_DIR/scenarios/ssh-bf.yaml" assert_file_exists "$CONFIG_DIR/scenarios/ssh-bf.yaml"
} }
# XXX: test install with --force
# XXX: test install with --ignore
@test "cscli scenarios inspect [scenario]..." { @test "cscli scenarios inspect [scenario]..." {
rune -1 cscli scenarios inspect rune -1 cscli scenarios inspect