crowdsec/pkg/hubtest/scenario_assert.go
Thibault "bui" Koechlin 6ca053ca67
fix #2720 #2719 (#2724)
* fix order of display of parsers

* add a --no-clean opt
2024-01-15 09:16:03 +01:00

277 lines
6.8 KiB
Go

package hubtest
import (
"bufio"
"fmt"
"io"
"os"
"sort"
"strings"
"github.com/antonmedv/expr"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
"github.com/crowdsecurity/crowdsec/pkg/dumps"
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
"github.com/crowdsecurity/crowdsec/pkg/types"
)
type ScenarioAssert struct {
File string
AutoGenAssert bool
AutoGenAssertData string
NbAssert int
Fails []AssertFail
Success bool
TestData *BucketResults
PourData *dumps.BucketPourInfo
}
type BucketResults []types.Event
func NewScenarioAssert(file string) *ScenarioAssert {
ScenarioAssert := &ScenarioAssert{
File: file,
NbAssert: 0,
Success: false,
Fails: make([]AssertFail, 0),
AutoGenAssert: false,
TestData: &BucketResults{},
PourData: &dumps.BucketPourInfo{},
}
return ScenarioAssert
}
func (s *ScenarioAssert) AutoGenFromFile(filename string) (string, error) {
err := s.LoadTest(filename, "")
if err != nil {
return "", err
}
ret := s.AutoGenScenarioAssert()
return ret, nil
}
func (s *ScenarioAssert) LoadTest(filename string, bucketpour string) error {
bucketDump, err := LoadScenarioDump(filename)
if err != nil {
return fmt.Errorf("loading scenario dump file '%s': %+v", filename, err)
}
s.TestData = bucketDump
if bucketpour != "" {
pourDump, err := dumps.LoadBucketPourDump(bucketpour)
if err != nil {
return fmt.Errorf("loading bucket pour dump file '%s': %+v", filename, err)
}
s.PourData = pourDump
}
return nil
}
func (s *ScenarioAssert) AssertFile(testFile string) error {
file, err := os.Open(s.File)
if err != nil {
return fmt.Errorf("failed to open")
}
if err := s.LoadTest(testFile, ""); err != nil {
return fmt.Errorf("unable to load parser dump file '%s': %s", testFile, err)
}
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
nbLine := 0
for scanner.Scan() {
nbLine++
if scanner.Text() == "" {
continue
}
ok, err := s.Run(scanner.Text())
if err != nil {
return fmt.Errorf("unable to run assert '%s': %+v", scanner.Text(), err)
}
s.NbAssert++
if !ok {
log.Debugf("%s is FALSE", scanner.Text())
failedAssert := &AssertFail{
File: s.File,
Line: nbLine,
Expression: scanner.Text(),
Debug: make(map[string]string),
}
match := variableRE.FindStringSubmatch(scanner.Text())
if len(match) == 0 {
log.Infof("Couldn't get variable of line '%s'", scanner.Text())
continue
}
variable := match[1]
result, err := s.EvalExpression(variable)
if err != nil {
log.Errorf("unable to evaluate variable '%s': %s", variable, err)
continue
}
failedAssert.Debug[variable] = result
s.Fails = append(s.Fails, *failedAssert)
continue
}
//fmt.Printf(" %s '%s'\n", emoji.GreenSquare, scanner.Text())
}
file.Close()
if s.NbAssert == 0 {
assertData, err := s.AutoGenFromFile(testFile)
if err != nil {
return fmt.Errorf("couldn't generate assertion: %s", err)
}
s.AutoGenAssertData = assertData
s.AutoGenAssert = true
}
if len(s.Fails) == 0 {
s.Success = true
}
return nil
}
func (s *ScenarioAssert) RunExpression(expression string) (interface{}, error) {
//debug doesn't make much sense with the ability to evaluate "on the fly"
//var debugFilter *exprhelpers.ExprDebugger
var output interface{}
env := map[string]interface{}{"results": *s.TestData}
runtimeFilter, err := expr.Compile(expression, exprhelpers.GetExprOptions(env)...)
if err != nil {
return nil, err
}
// if debugFilter, err = exprhelpers.NewDebugger(assert, expr.Env(env)); err != nil {
// log.Warningf("Failed building debugher for %s : %s", assert, err)
// }
//dump opcode in trace level
log.Tracef("%s", runtimeFilter.Disassemble())
output, err = expr.Run(runtimeFilter, map[string]interface{}{"results": *s.TestData})
if err != nil {
log.Warningf("running : %s", expression)
log.Warningf("runtime error : %s", err)
return nil, fmt.Errorf("while running expression %s: %w", expression, err)
}
return output, nil
}
func (s *ScenarioAssert) EvalExpression(expression string) (string, error) {
output, err := s.RunExpression(expression)
if err != nil {
return "", err
}
ret, err := yaml.Marshal(output)
if err != nil {
return "", err
}
return string(ret), nil
}
func (s *ScenarioAssert) Run(assert string) (bool, error) {
output, err := s.RunExpression(assert)
if err != nil {
return false, err
}
switch out := output.(type) {
case bool:
return out, nil
default:
return false, fmt.Errorf("assertion '%s' is not a condition", assert)
}
}
func (s *ScenarioAssert) AutoGenScenarioAssert() string {
// attempt to autogen scenario asserts
ret := fmt.Sprintf(`len(results) == %d`+"\n", len(*s.TestData))
for eventIndex, event := range *s.TestData {
for ipSrc, source := range event.Overflow.Sources {
ret += fmt.Sprintf(`"%s" in results[%d].Overflow.GetSources()`+"\n", ipSrc, eventIndex)
ret += fmt.Sprintf(`results[%d].Overflow.Sources["%s"].IP == "%s"`+"\n", eventIndex, ipSrc, source.IP)
ret += fmt.Sprintf(`results[%d].Overflow.Sources["%s"].Range == "%s"`+"\n", eventIndex, ipSrc, source.Range)
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)
}
for evtIndex, evt := range event.Overflow.Alert.Events {
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.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.GetEventsCount() == %d`+"\n", eventIndex, *event.Overflow.Alert.EventsCount)
}
return ret
}
func (b BucketResults) Len() int {
return len(b)
}
func (b BucketResults) Less(i, j int) bool {
return b[i].Overflow.Alert.GetScenario()+strings.Join(b[i].Overflow.GetSources(), "@") > b[j].Overflow.Alert.GetScenario()+strings.Join(b[j].Overflow.GetSources(), "@")
}
func (b BucketResults) Swap(i, j int) {
b[i], b[j] = b[j], b[i]
}
func LoadScenarioDump(filepath string) (*BucketResults, error) {
dumpData, err := os.Open(filepath)
if err != nil {
return nil, err
}
defer dumpData.Close()
results, err := io.ReadAll(dumpData)
if err != nil {
return nil, err
}
var bucketDump BucketResults
if err := yaml.Unmarshal(results, &bucketDump); err != nil {
return nil, err
}
sort.Sort(bucketDump)
return &bucketDump, nil
}