2020-05-15 09:39:16 +00:00
package parser
import (
"bytes"
2022-11-29 08:16:07 +00:00
"errors"
2020-05-15 09:39:16 +00:00
"fmt"
"html/template"
"io"
"os"
2020-11-30 09:37:17 +00:00
"sort"
2020-05-24 10:44:33 +00:00
"strings"
2020-05-15 09:39:16 +00:00
"testing"
"github.com/davecgh/go-spew/spew"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
2023-06-13 11:16:13 +00:00
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
"github.com/crowdsecurity/crowdsec/pkg/types"
2020-05-15 09:39:16 +00:00
)
type TestFile struct {
Lines [ ] types . Event ` yaml:"lines,omitempty" `
Results [ ] types . Event ` yaml:"results,omitempty" `
}
2020-05-24 10:44:33 +00:00
var debug bool = false
2020-05-15 09:39:16 +00:00
func TestParser ( t * testing . T ) {
2020-05-24 10:44:33 +00:00
debug = true
log . SetLevel ( log . InfoLevel )
var envSetting = os . Getenv ( "TEST_ONLY" )
2020-11-30 09:37:17 +00:00
pctx , ectx , err := prepTests ( )
2020-05-24 10:44:33 +00:00
if err != nil {
t . Fatalf ( "failed to load env : %s" , err )
}
//Init the enricher
if envSetting != "" {
2020-11-30 09:37:17 +00:00
if err := testOneParser ( pctx , ectx , envSetting , nil ) ; err != nil {
2020-05-24 10:44:33 +00:00
t . Fatalf ( "Test '%s' failed : %s" , envSetting , err )
}
} else {
2022-09-06 11:55:03 +00:00
fds , err := os . ReadDir ( "./tests/" )
2020-05-24 10:44:33 +00:00
if err != nil {
t . Fatalf ( "Unable to read test directory : %s" , err )
}
for _ , fd := range fds {
2020-05-27 16:22:49 +00:00
if ! fd . IsDir ( ) {
continue
}
2020-05-24 10:44:33 +00:00
fname := "./tests/" + fd . Name ( )
log . Infof ( "Running test on %s" , fname )
2020-11-30 09:37:17 +00:00
if err := testOneParser ( pctx , ectx , fname , nil ) ; err != nil {
2020-05-24 10:44:33 +00:00
t . Fatalf ( "Test '%s' failed : %s" , fname , err )
}
}
}
}
func BenchmarkParser ( t * testing . B ) {
log . Printf ( "start bench !!!!" )
2020-05-24 10:59:26 +00:00
debug = false
2020-05-24 10:44:33 +00:00
log . SetLevel ( log . ErrorLevel )
2020-11-30 09:37:17 +00:00
pctx , ectx , err := prepTests ( )
2020-05-24 10:44:33 +00:00
if err != nil {
t . Fatalf ( "failed to load env : %s" , err )
}
2020-05-15 09:39:16 +00:00
var envSetting = os . Getenv ( "TEST_ONLY" )
if envSetting != "" {
2020-11-30 09:37:17 +00:00
if err := testOneParser ( pctx , ectx , envSetting , t ) ; err != nil {
2020-05-15 09:39:16 +00:00
t . Fatalf ( "Test '%s' failed : %s" , envSetting , err )
}
} else {
2022-09-06 11:55:03 +00:00
fds , err := os . ReadDir ( "./tests/" )
2020-05-15 09:39:16 +00:00
if err != nil {
t . Fatalf ( "Unable to read test directory : %s" , err )
}
for _ , fd := range fds {
2020-05-27 16:22:49 +00:00
if ! fd . IsDir ( ) {
continue
}
2020-05-15 09:39:16 +00:00
fname := "./tests/" + fd . Name ( )
log . Infof ( "Running test on %s" , fname )
2020-11-30 09:37:17 +00:00
if err := testOneParser ( pctx , ectx , fname , t ) ; err != nil {
2020-05-15 09:39:16 +00:00
t . Fatalf ( "Test '%s' failed : %s" , fname , err )
}
}
}
}
2021-09-09 14:27:30 +00:00
func testOneParser ( pctx * UnixParserCtx , ectx EnricherCtx , dir string , b * testing . B ) error {
2020-11-30 09:37:17 +00:00
var (
err error
pnodes [ ] Node
2020-05-15 09:39:16 +00:00
2020-11-30 09:37:17 +00:00
parser_configs [ ] Stagefile
)
2020-05-24 10:44:33 +00:00
log . Warningf ( "testing %s" , dir )
2020-05-15 09:39:16 +00:00
parser_cfg_file := fmt . Sprintf ( "%s/parsers.yaml" , dir )
2022-09-06 11:55:03 +00:00
cfg , err := os . ReadFile ( parser_cfg_file )
2020-05-15 09:39:16 +00:00
if err != nil {
return fmt . Errorf ( "failed opening %s : %s" , parser_cfg_file , err )
}
2020-05-24 10:44:33 +00:00
tmpl , err := template . New ( "test" ) . Parse ( string ( cfg ) )
2020-05-15 09:39:16 +00:00
if err != nil {
2020-05-24 10:44:33 +00:00
return fmt . Errorf ( "failed to parse template %s : %s" , cfg , err )
2020-05-15 09:39:16 +00:00
}
var out bytes . Buffer
err = tmpl . Execute ( & out , map [ string ] string { "TestDirectory" : dir } )
if err != nil {
panic ( err )
}
2023-06-13 11:16:13 +00:00
if err = yaml . UnmarshalStrict ( out . Bytes ( ) , & parser_configs ) ; err != nil {
2020-05-15 09:39:16 +00:00
return fmt . Errorf ( "failed unmarshaling %s : %s" , parser_cfg_file , err )
}
2020-11-30 09:37:17 +00:00
pnodes , err = LoadStages ( parser_configs , pctx , ectx )
2020-05-15 09:39:16 +00:00
if err != nil {
2020-05-20 08:49:17 +00:00
return fmt . Errorf ( "unable to load parser config : %s" , err )
2020-05-15 09:39:16 +00:00
}
//TBD: Load post overflows
//func testFile(t *testing.T, file string, pctx UnixParserCtx, nodes []Node) bool {
parser_test_file := fmt . Sprintf ( "%s/test.yaml" , dir )
2020-05-24 10:44:33 +00:00
tests := loadTestFile ( parser_test_file )
count := 1
if b != nil {
count = b . N
b . ResetTimer ( )
}
for n := 0 ; n < count ; n ++ {
if testFile ( tests , * pctx , pnodes ) != true {
return fmt . Errorf ( "test failed !" )
}
2020-05-15 09:39:16 +00:00
}
return nil
}
2023-01-11 14:01:02 +00:00
// prepTests is going to do the initialisation of parser : it's going to load enrichment plugins and load the patterns. This is done here so that we don't redo it for each test
2021-09-09 14:27:30 +00:00
func prepTests ( ) ( * UnixParserCtx , EnricherCtx , error ) {
2020-11-30 09:37:17 +00:00
var (
err error
pctx * UnixParserCtx
2021-09-09 14:27:30 +00:00
ectx EnricherCtx
2020-11-30 09:37:17 +00:00
)
2022-06-22 09:29:52 +00:00
err = exprhelpers . Init ( nil )
2020-07-02 15:56:39 +00:00
if err != nil {
log . Fatalf ( "exprhelpers init failed: %s" , err )
}
2020-05-24 10:44:33 +00:00
//Load enrichment
2020-11-30 09:37:17 +00:00
datadir := "./test_data/"
ectx , err = Loadplugin ( datadir )
2020-05-24 10:44:33 +00:00
if err != nil {
log . Fatalf ( "failed to load plugin geoip : %v" , err )
}
2020-11-30 09:37:17 +00:00
log . Printf ( "Loaded -> %+v" , ectx )
2020-05-24 10:44:33 +00:00
//Load the parser patterns
cfgdir := "../../config/"
2020-05-15 09:39:16 +00:00
2020-05-24 10:44:33 +00:00
/* this should be refactored to 2 lines :p */
// Init the parser
2020-11-30 09:37:17 +00:00
pctx , err = Init ( map [ string ] interface { } { "patterns" : cfgdir + string ( "/patterns/" ) , "data" : "./tests/" } )
2020-05-24 10:44:33 +00:00
if err != nil {
2021-09-09 14:27:30 +00:00
return nil , ectx , fmt . Errorf ( "failed to initialize parser : %v" , err )
2020-05-24 10:44:33 +00:00
}
2020-11-30 09:37:17 +00:00
return pctx , ectx , nil
2020-05-24 10:44:33 +00:00
}
2020-05-15 09:39:16 +00:00
2020-05-24 10:44:33 +00:00
func loadTestFile ( file string ) [ ] TestFile {
2020-05-15 09:39:16 +00:00
yamlFile , err := os . Open ( file )
if err != nil {
2020-05-24 10:44:33 +00:00
log . Fatalf ( "yamlFile.Get err #%v " , err )
2020-05-15 09:39:16 +00:00
}
dec := yaml . NewDecoder ( yamlFile )
dec . SetStrict ( true )
2020-05-24 10:44:33 +00:00
var testSet [ ] TestFile
2020-05-15 09:39:16 +00:00
for {
tf := TestFile { }
err := dec . Decode ( & tf )
if err != nil {
2022-11-29 08:16:07 +00:00
if errors . Is ( err , io . EOF ) {
2020-05-15 09:39:16 +00:00
break
}
2020-05-24 10:44:33 +00:00
log . Fatalf ( "Failed to load testfile '%s' yaml error : %v" , file , err )
return nil
2020-05-15 09:39:16 +00:00
}
2020-05-24 10:44:33 +00:00
testSet = append ( testSet , tf )
}
return testSet
}
func matchEvent ( expected types . Event , out types . Event , debug bool ) ( [ ] string , bool ) {
var retInfo [ ] string
2023-03-09 10:56:02 +00:00
var valid = false
2020-05-24 10:44:33 +00:00
expectMaps := [ ] map [ string ] string { expected . Parsed , expected . Meta , expected . Enriched }
outMaps := [ ] map [ string ] string { out . Parsed , out . Meta , out . Enriched }
outLabels := [ ] string { "Parsed" , "Meta" , "Enriched" }
//allow to check as well for stage and processed flags
if expected . Stage != "" {
if expected . Stage != out . Stage {
if debug {
retInfo = append ( retInfo , fmt . Sprintf ( "mismatch stage %s != %s" , expected . Stage , out . Stage ) )
}
goto checkFinished
} else {
valid = true
if debug {
retInfo = append ( retInfo , fmt . Sprintf ( "ok stage %s == %s" , expected . Stage , out . Stage ) )
2020-05-15 09:39:16 +00:00
}
}
2020-05-24 10:44:33 +00:00
}
2020-05-15 09:39:16 +00:00
2020-05-24 10:44:33 +00:00
if expected . Process != out . Process {
if debug {
retInfo = append ( retInfo , fmt . Sprintf ( "mismatch process %t != %t" , expected . Process , out . Process ) )
2020-05-15 09:39:16 +00:00
}
2020-05-24 10:44:33 +00:00
goto checkFinished
} else {
valid = true
if debug {
retInfo = append ( retInfo , fmt . Sprintf ( "ok process %t == %t" , expected . Process , out . Process ) )
2020-05-15 09:39:16 +00:00
}
2020-05-24 10:44:33 +00:00
}
2020-05-15 09:39:16 +00:00
2020-05-24 10:44:33 +00:00
if expected . Whitelisted != out . Whitelisted {
if debug {
retInfo = append ( retInfo , fmt . Sprintf ( "mismatch whitelist %t != %t" , expected . Whitelisted , out . Whitelisted ) )
}
goto checkFinished
} else {
if debug {
retInfo = append ( retInfo , fmt . Sprintf ( "ok whitelist %t == %t" , expected . Whitelisted , out . Whitelisted ) )
}
valid = true
}
2020-05-15 09:39:16 +00:00
2020-05-24 10:44:33 +00:00
for mapIdx := 0 ; mapIdx < len ( expectMaps ) ; mapIdx ++ {
for expKey , expVal := range expectMaps [ mapIdx ] {
if outVal , ok := outMaps [ mapIdx ] [ expKey ] ; ok {
if outVal == expVal { //ok entry
if debug {
retInfo = append ( retInfo , fmt . Sprintf ( "ok %s[%s] %s == %s" , outLabels [ mapIdx ] , expKey , expVal , outVal ) )
}
valid = true
} else { //mismatch entry
if debug {
retInfo = append ( retInfo , fmt . Sprintf ( "mismatch %s[%s] %s != %s" , outLabels [ mapIdx ] , expKey , expVal , outVal ) )
2020-05-15 09:39:16 +00:00
}
2023-01-11 14:01:02 +00:00
valid = false
2020-05-24 10:44:33 +00:00
goto checkFinished
2020-05-15 09:39:16 +00:00
}
2020-05-24 10:44:33 +00:00
} else { //missing entry
if debug {
retInfo = append ( retInfo , fmt . Sprintf ( "missing entry %s[%s]" , outLabels [ mapIdx ] , expKey ) )
2020-05-15 09:39:16 +00:00
}
2020-05-24 10:44:33 +00:00
valid = false
goto checkFinished
}
}
}
checkFinished :
if valid {
if debug {
2023-01-11 14:01:02 +00:00
retInfo = append ( retInfo , fmt . Sprintf ( "OK ! \n\t%s" , strings . Join ( retInfo , "\n\t" ) ) )
2020-05-24 10:44:33 +00:00
}
} else {
if debug {
2023-01-11 14:01:02 +00:00
retInfo = append ( retInfo , fmt . Sprintf ( "KO ! \n\t%s" , strings . Join ( retInfo , "\n\t" ) ) )
2020-05-24 10:44:33 +00:00
}
}
return retInfo , valid
}
2020-05-15 09:39:16 +00:00
2020-05-24 10:44:33 +00:00
func testSubSet ( testSet TestFile , pctx UnixParserCtx , nodes [ ] Node ) ( bool , error ) {
var results [ ] types . Event
2020-05-15 09:39:16 +00:00
2020-05-24 10:44:33 +00:00
for _ , in := range testSet . Lines {
out , err := Parse ( pctx , in , nodes )
if err != nil {
log . Errorf ( "Failed to process %s : %v" , spew . Sdump ( in ) , err )
}
//log.Infof("Parser output : %s", spew.Sdump(out))
results = append ( results , out )
}
log . Infof ( "parsed %d lines" , len ( testSet . Lines ) )
log . Infof ( "got %d results" , len ( results ) )
2020-05-15 09:39:16 +00:00
2020-05-24 10:44:33 +00:00
/ *
check the results we got against the expected ones
only the keys of the expected part are checked against result
* /
if len ( testSet . Results ) == 0 && len ( results ) == 0 {
2022-06-06 13:24:48 +00:00
log . Fatal ( "No results, no tests, abort." )
2020-05-24 10:44:33 +00:00
return false , fmt . Errorf ( "no tests, no results" )
}
2020-05-15 09:39:16 +00:00
2020-05-24 10:44:33 +00:00
reCheck :
failinfo := [ ] string { }
for ridx , result := range results {
for eidx , expected := range testSet . Results {
explain , match := matchEvent ( expected , result , debug )
if match == true {
log . Infof ( "expected %d/%d matches result %d/%d" , eidx , len ( testSet . Results ) , ridx , len ( results ) )
if len ( explain ) > 0 {
log . Printf ( "-> %s" , explain [ len ( explain ) - 1 ] )
2020-05-15 09:39:16 +00:00
}
2020-05-24 10:44:33 +00:00
//don't do this at home : delete current element from list and redo
results [ len ( results ) - 1 ] , results [ ridx ] = results [ ridx ] , results [ len ( results ) - 1 ]
results = results [ : len ( results ) - 1 ]
2020-05-15 09:39:16 +00:00
2020-05-24 10:44:33 +00:00
testSet . Results [ len ( testSet . Results ) - 1 ] , testSet . Results [ eidx ] = testSet . Results [ eidx ] , testSet . Results [ len ( testSet . Results ) - 1 ]
testSet . Results = testSet . Results [ : len ( testSet . Results ) - 1 ]
2020-05-15 09:39:16 +00:00
2020-05-24 10:44:33 +00:00
goto reCheck
} else {
failinfo = append ( failinfo , explain ... )
}
2020-05-15 09:39:16 +00:00
}
2020-05-24 10:44:33 +00:00
}
if len ( results ) > 0 {
log . Printf ( "Errors : %s" , strings . Join ( failinfo , " / " ) )
return false , fmt . Errorf ( "leftover results : %+v" , results )
}
if len ( testSet . Results ) > 0 {
log . Printf ( "Errors : %s" , strings . Join ( failinfo , " / " ) )
return false , fmt . Errorf ( "leftover expected results : %+v" , testSet . Results )
}
return true , nil
}
2020-05-15 09:39:16 +00:00
2020-05-24 10:44:33 +00:00
func testFile ( testSet [ ] TestFile , pctx UnixParserCtx , nodes [ ] Node ) bool {
2022-06-22 07:38:23 +00:00
log . Warning ( "Going to process one test set" )
2020-05-24 10:44:33 +00:00
for _ , tf := range testSet {
//func testSubSet(testSet TestFile, pctx UnixParserCtx, nodes []Node) (bool, error) {
testOk , err := testSubSet ( tf , pctx , nodes )
if err != nil {
log . Fatalf ( "test failed : %s" , err )
}
if ! testOk {
log . Fatalf ( "failed test : %+v" , tf )
}
2020-05-15 09:39:16 +00:00
}
2020-05-24 10:44:33 +00:00
return true
2020-05-15 09:39:16 +00:00
}
2020-11-30 09:37:17 +00:00
/*THIS IS ONLY PRESENT TO BE ABLE TO GENERATE DOCUMENTATION OF EXISTING PATTERNS*/
type Pair struct {
Key string
Value string
}
type PairList [ ] Pair
func ( p PairList ) Len ( ) int { return len ( p ) }
func ( p PairList ) Swap ( i , j int ) { p [ i ] , p [ j ] = p [ j ] , p [ i ] }
func ( p PairList ) Less ( i , j int ) bool { return len ( p [ i ] . Value ) < len ( p [ j ] . Value ) }
func TestGeneratePatternsDoc ( t * testing . T ) {
if os . Getenv ( "GO_WANT_TEST_DOC" ) != "1" {
return
}
pctx , err := Init ( map [ string ] interface { } { "patterns" : "../../config/patterns/" , "data" : "./tests/" } )
if err != nil {
t . Fatalf ( "unable to load patterns : %s" , err )
}
log . Infof ( "-> %s" , spew . Sdump ( pctx ) )
/*don't judge me, we do it for the users*/
2023-03-28 14:26:47 +00:00
p := make ( PairList , len ( pctx . Grok . Patterns ) )
2020-11-30 09:37:17 +00:00
i := 0
2023-03-28 14:26:47 +00:00
for key , val := range pctx . Grok . Patterns {
2020-11-30 09:37:17 +00:00
p [ i ] = Pair { key , val }
2022-11-07 09:36:50 +00:00
p [ i ] . Value = strings . ReplaceAll ( p [ i ] . Value , "{%{" , "\\{\\%\\{" )
2020-11-30 09:37:17 +00:00
i ++
}
sort . Sort ( p )
f , err := os . OpenFile ( "./patterns-documentation.md" , os . O_TRUNC | os . O_CREATE | os . O_WRONLY , 0644 )
if err != nil {
t . Fatalf ( "failed to open : %s" , err )
}
if _ , err := f . WriteString ( "# Patterns documentation\n\n" ) ; err != nil {
t . Fatal ( "failed to write to file" )
}
if _ , err := f . WriteString ( "You will find here a generated documentation of all the patterns loaded by crowdsec.\n" ) ; err != nil {
t . Fatal ( "failed to write to file" )
}
if _ , err := f . WriteString ( "They are sorted by pattern length, and are meant to be used in parsers, in the form %{PATTERN_NAME}.\n" ) ; err != nil {
t . Fatal ( "failed to write to file" )
}
if _ , err := f . WriteString ( "\n\n" ) ; err != nil {
t . Fatal ( "failed to write to file" )
}
for _ , k := range p {
2023-06-13 11:16:13 +00:00
if _ , err := fmt . Fprintf ( f , "## %s\n\nPattern :\n```\n%s\n```\n\n" , k . Key , k . Value ) ; err != nil {
2020-11-30 09:37:17 +00:00
t . Fatal ( "failed to write to file" )
}
fmt . Printf ( "%v\t%v\n" , k . Key , k . Value )
}
if _ , err := f . WriteString ( "\n" ) ; err != nil {
t . Fatal ( "failed to write to file" )
}
if _ , err := f . WriteString ( "# Documentation generation\n" ) ; err != nil {
t . Fatal ( "failed to write to file" )
}
if _ , err := f . WriteString ( "This documentation is generated by `pkg/parser` : `GO_WANT_TEST_DOC=1 go test -run TestGeneratePatternsDoc`\n" ) ; err != nil {
t . Fatal ( "failed to write to file" )
}
f . Close ( )
}