github.com/crowdsecurity/crowdsec@v1.6.1/pkg/parser/parsing_test.go (about) 1 package parser 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "html/template" 8 "io" 9 "os" 10 "sort" 11 "strings" 12 "testing" 13 14 "github.com/davecgh/go-spew/spew" 15 log "github.com/sirupsen/logrus" 16 "gopkg.in/yaml.v2" 17 18 "github.com/crowdsecurity/crowdsec/pkg/exprhelpers" 19 "github.com/crowdsecurity/crowdsec/pkg/types" 20 ) 21 22 type TestFile struct { 23 Lines []types.Event `yaml:"lines,omitempty"` 24 Results []types.Event `yaml:"results,omitempty"` 25 } 26 27 var debug bool = false 28 29 func TestParser(t *testing.T) { 30 debug = true 31 log.SetLevel(log.InfoLevel) 32 var envSetting = os.Getenv("TEST_ONLY") 33 pctx, ectx, err := prepTests() 34 if err != nil { 35 t.Fatalf("failed to load env : %s", err) 36 } 37 //Init the enricher 38 if envSetting != "" { 39 if err := testOneParser(pctx, ectx, envSetting, nil); err != nil { 40 t.Fatalf("Test '%s' failed : %s", envSetting, err) 41 } 42 } else { 43 fds, err := os.ReadDir("./tests/") 44 if err != nil { 45 t.Fatalf("Unable to read test directory : %s", err) 46 } 47 for _, fd := range fds { 48 if !fd.IsDir() { 49 continue 50 } 51 fname := "./tests/" + fd.Name() 52 log.Infof("Running test on %s", fname) 53 if err := testOneParser(pctx, ectx, fname, nil); err != nil { 54 t.Fatalf("Test '%s' failed : %s", fname, err) 55 } 56 } 57 } 58 } 59 60 func BenchmarkParser(t *testing.B) { 61 log.Printf("start bench !!!!") 62 debug = false 63 log.SetLevel(log.ErrorLevel) 64 pctx, ectx, err := prepTests() 65 if err != nil { 66 t.Fatalf("failed to load env : %s", err) 67 } 68 var envSetting = os.Getenv("TEST_ONLY") 69 70 if envSetting != "" { 71 if err := testOneParser(pctx, ectx, envSetting, t); err != nil { 72 t.Fatalf("Test '%s' failed : %s", envSetting, err) 73 } 74 } else { 75 fds, err := os.ReadDir("./tests/") 76 if err != nil { 77 t.Fatalf("Unable to read test directory : %s", err) 78 } 79 for _, fd := range fds { 80 if !fd.IsDir() { 81 continue 82 } 83 fname := "./tests/" + fd.Name() 84 log.Infof("Running test on %s", fname) 85 if err := testOneParser(pctx, ectx, fname, t); err != nil { 86 t.Fatalf("Test '%s' failed : %s", fname, err) 87 } 88 } 89 } 90 } 91 92 func testOneParser(pctx *UnixParserCtx, ectx EnricherCtx, dir string, b *testing.B) error { 93 var ( 94 err error 95 pnodes []Node 96 97 parser_configs []Stagefile 98 ) 99 log.Warningf("testing %s", dir) 100 parser_cfg_file := fmt.Sprintf("%s/parsers.yaml", dir) 101 cfg, err := os.ReadFile(parser_cfg_file) 102 if err != nil { 103 return fmt.Errorf("failed opening %s : %s", parser_cfg_file, err) 104 } 105 tmpl, err := template.New("test").Parse(string(cfg)) 106 if err != nil { 107 return fmt.Errorf("failed to parse template %s : %s", cfg, err) 108 } 109 var out bytes.Buffer 110 err = tmpl.Execute(&out, map[string]string{"TestDirectory": dir}) 111 if err != nil { 112 panic(err) 113 } 114 if err = yaml.UnmarshalStrict(out.Bytes(), &parser_configs); err != nil { 115 return fmt.Errorf("failed unmarshaling %s : %s", parser_cfg_file, err) 116 } 117 118 pnodes, err = LoadStages(parser_configs, pctx, ectx) 119 if err != nil { 120 return fmt.Errorf("unable to load parser config : %s", err) 121 } 122 123 //TBD: Load post overflows 124 //func testFile(t *testing.T, file string, pctx UnixParserCtx, nodes []Node) bool { 125 parser_test_file := fmt.Sprintf("%s/test.yaml", dir) 126 tests := loadTestFile(parser_test_file) 127 count := 1 128 if b != nil { 129 count = b.N 130 b.ResetTimer() 131 } 132 for n := 0; n < count; n++ { 133 if testFile(tests, *pctx, pnodes) != true { 134 return fmt.Errorf("test failed !") 135 } 136 } 137 return nil 138 } 139 140 // 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 141 func prepTests() (*UnixParserCtx, EnricherCtx, error) { 142 var ( 143 err error 144 pctx *UnixParserCtx 145 ectx EnricherCtx 146 ) 147 148 err = exprhelpers.Init(nil) 149 if err != nil { 150 log.Fatalf("exprhelpers init failed: %s", err) 151 } 152 153 //Load enrichment 154 datadir := "./test_data/" 155 ectx, err = Loadplugin(datadir) 156 if err != nil { 157 log.Fatalf("failed to load plugin geoip : %v", err) 158 } 159 log.Printf("Loaded -> %+v", ectx) 160 161 //Load the parser patterns 162 cfgdir := "../../config/" 163 164 /* this should be refactored to 2 lines :p */ 165 // Init the parser 166 pctx, err = Init(map[string]interface{}{"patterns": cfgdir + string("/patterns/"), "data": "./tests/"}) 167 if err != nil { 168 return nil, ectx, fmt.Errorf("failed to initialize parser : %v", err) 169 } 170 return pctx, ectx, nil 171 } 172 173 func loadTestFile(file string) []TestFile { 174 yamlFile, err := os.Open(file) 175 if err != nil { 176 log.Fatalf("yamlFile.Get err #%v ", err) 177 } 178 dec := yaml.NewDecoder(yamlFile) 179 dec.SetStrict(true) 180 var testSet []TestFile 181 for { 182 tf := TestFile{} 183 err := dec.Decode(&tf) 184 if err != nil { 185 if errors.Is(err, io.EOF) { 186 break 187 } 188 log.Fatalf("Failed to load testfile '%s' yaml error : %v", file, err) 189 return nil 190 } 191 testSet = append(testSet, tf) 192 } 193 return testSet 194 } 195 196 func matchEvent(expected types.Event, out types.Event, debug bool) ([]string, bool) { 197 var retInfo []string 198 var valid = false 199 expectMaps := []map[string]string{expected.Parsed, expected.Meta, expected.Enriched} 200 outMaps := []map[string]string{out.Parsed, out.Meta, out.Enriched} 201 outLabels := []string{"Parsed", "Meta", "Enriched"} 202 203 //allow to check as well for stage and processed flags 204 if expected.Stage != "" { 205 if expected.Stage != out.Stage { 206 if debug { 207 retInfo = append(retInfo, fmt.Sprintf("mismatch stage %s != %s", expected.Stage, out.Stage)) 208 } 209 goto checkFinished 210 } else { 211 valid = true 212 if debug { 213 retInfo = append(retInfo, fmt.Sprintf("ok stage %s == %s", expected.Stage, out.Stage)) 214 } 215 } 216 } 217 218 if expected.Process != out.Process { 219 if debug { 220 retInfo = append(retInfo, fmt.Sprintf("mismatch process %t != %t", expected.Process, out.Process)) 221 } 222 goto checkFinished 223 } else { 224 valid = true 225 if debug { 226 retInfo = append(retInfo, fmt.Sprintf("ok process %t == %t", expected.Process, out.Process)) 227 } 228 } 229 230 if expected.Whitelisted != out.Whitelisted { 231 if debug { 232 retInfo = append(retInfo, fmt.Sprintf("mismatch whitelist %t != %t", expected.Whitelisted, out.Whitelisted)) 233 } 234 goto checkFinished 235 } else { 236 if debug { 237 retInfo = append(retInfo, fmt.Sprintf("ok whitelist %t == %t", expected.Whitelisted, out.Whitelisted)) 238 } 239 valid = true 240 } 241 242 for mapIdx := 0; mapIdx < len(expectMaps); mapIdx++ { 243 for expKey, expVal := range expectMaps[mapIdx] { 244 if outVal, ok := outMaps[mapIdx][expKey]; ok { 245 if outVal == expVal { //ok entry 246 if debug { 247 retInfo = append(retInfo, fmt.Sprintf("ok %s[%s] %s == %s", outLabels[mapIdx], expKey, expVal, outVal)) 248 } 249 valid = true 250 } else { //mismatch entry 251 if debug { 252 retInfo = append(retInfo, fmt.Sprintf("mismatch %s[%s] %s != %s", outLabels[mapIdx], expKey, expVal, outVal)) 253 } 254 valid = false 255 goto checkFinished 256 } 257 } else { //missing entry 258 if debug { 259 retInfo = append(retInfo, fmt.Sprintf("missing entry %s[%s]", outLabels[mapIdx], expKey)) 260 } 261 valid = false 262 goto checkFinished 263 } 264 } 265 } 266 checkFinished: 267 if valid { 268 if debug { 269 retInfo = append(retInfo, fmt.Sprintf("OK ! \n\t%s", strings.Join(retInfo, "\n\t"))) 270 } 271 } else { 272 if debug { 273 retInfo = append(retInfo, fmt.Sprintf("KO ! \n\t%s", strings.Join(retInfo, "\n\t"))) 274 } 275 } 276 return retInfo, valid 277 } 278 279 func testSubSet(testSet TestFile, pctx UnixParserCtx, nodes []Node) (bool, error) { 280 var results []types.Event 281 282 for _, in := range testSet.Lines { 283 out, err := Parse(pctx, in, nodes) 284 if err != nil { 285 log.Errorf("Failed to process %s : %v", spew.Sdump(in), err) 286 } 287 //log.Infof("Parser output : %s", spew.Sdump(out)) 288 results = append(results, out) 289 } 290 log.Infof("parsed %d lines", len(testSet.Lines)) 291 log.Infof("got %d results", len(results)) 292 293 /* 294 check the results we got against the expected ones 295 only the keys of the expected part are checked against result 296 */ 297 if len(testSet.Results) == 0 && len(results) == 0 { 298 log.Fatal("No results, no tests, abort.") 299 return false, fmt.Errorf("no tests, no results") 300 } 301 302 reCheck: 303 failinfo := []string{} 304 for ridx, result := range results { 305 for eidx, expected := range testSet.Results { 306 explain, match := matchEvent(expected, result, debug) 307 if match == true { 308 log.Infof("expected %d/%d matches result %d/%d", eidx, len(testSet.Results), ridx, len(results)) 309 if len(explain) > 0 { 310 log.Printf("-> %s", explain[len(explain)-1]) 311 } 312 //don't do this at home : delete current element from list and redo 313 results[len(results)-1], results[ridx] = results[ridx], results[len(results)-1] 314 results = results[:len(results)-1] 315 316 testSet.Results[len(testSet.Results)-1], testSet.Results[eidx] = testSet.Results[eidx], testSet.Results[len(testSet.Results)-1] 317 testSet.Results = testSet.Results[:len(testSet.Results)-1] 318 319 goto reCheck 320 } else { 321 failinfo = append(failinfo, explain...) 322 } 323 } 324 } 325 if len(results) > 0 { 326 log.Printf("Errors : %s", strings.Join(failinfo, " / ")) 327 return false, fmt.Errorf("leftover results : %+v", results) 328 } 329 if len(testSet.Results) > 0 { 330 log.Printf("Errors : %s", strings.Join(failinfo, " / ")) 331 return false, fmt.Errorf("leftover expected results : %+v", testSet.Results) 332 } 333 return true, nil 334 } 335 336 func testFile(testSet []TestFile, pctx UnixParserCtx, nodes []Node) bool { 337 log.Warning("Going to process one test set") 338 for _, tf := range testSet { 339 //func testSubSet(testSet TestFile, pctx UnixParserCtx, nodes []Node) (bool, error) { 340 testOk, err := testSubSet(tf, pctx, nodes) 341 if err != nil { 342 log.Fatalf("test failed : %s", err) 343 } 344 if !testOk { 345 log.Fatalf("failed test : %+v", tf) 346 } 347 } 348 return true 349 } 350 351 /*THIS IS ONLY PRESENT TO BE ABLE TO GENERATE DOCUMENTATION OF EXISTING PATTERNS*/ 352 type Pair struct { 353 Key string 354 Value string 355 } 356 357 type PairList []Pair 358 359 func (p PairList) Len() int { return len(p) } 360 func (p PairList) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 361 func (p PairList) Less(i, j int) bool { return len(p[i].Value) < len(p[j].Value) } 362 363 func TestGeneratePatternsDoc(t *testing.T) { 364 if os.Getenv("GO_WANT_TEST_DOC") != "1" { 365 return 366 } 367 368 pctx, err := Init(map[string]interface{}{"patterns": "../../config/patterns/", "data": "./tests/"}) 369 if err != nil { 370 t.Fatalf("unable to load patterns : %s", err) 371 } 372 log.Infof("-> %s", spew.Sdump(pctx)) 373 /*don't judge me, we do it for the users*/ 374 p := make(PairList, len(pctx.Grok.Patterns)) 375 376 i := 0 377 for key, val := range pctx.Grok.Patterns { 378 p[i] = Pair{key, val} 379 p[i].Value = strings.ReplaceAll(p[i].Value, "{%{", "\\{\\%\\{") 380 i++ 381 } 382 sort.Sort(p) 383 384 f, err := os.OpenFile("./patterns-documentation.md", os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644) 385 if err != nil { 386 t.Fatalf("failed to open : %s", err) 387 } 388 if _, err := f.WriteString("# Patterns documentation\n\n"); err != nil { 389 t.Fatal("failed to write to file") 390 } 391 if _, err := f.WriteString("You will find here a generated documentation of all the patterns loaded by crowdsec.\n"); err != nil { 392 t.Fatal("failed to write to file") 393 } 394 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 { 395 t.Fatal("failed to write to file") 396 } 397 if _, err := f.WriteString("\n\n"); err != nil { 398 t.Fatal("failed to write to file") 399 } 400 for _, k := range p { 401 if _, err := fmt.Fprintf(f, "## %s\n\nPattern :\n```\n%s\n```\n\n", k.Key, k.Value); err != nil { 402 t.Fatal("failed to write to file") 403 } 404 fmt.Printf("%v\t%v\n", k.Key, k.Value) 405 } 406 if _, err := f.WriteString("\n"); err != nil { 407 t.Fatal("failed to write to file") 408 } 409 if _, err := f.WriteString("# Documentation generation\n"); err != nil { 410 t.Fatal("failed to write to file") 411 } 412 if _, err := f.WriteString("This documentation is generated by `pkg/parser` : `GO_WANT_TEST_DOC=1 go test -run TestGeneratePatternsDoc`\n"); err != nil { 413 t.Fatal("failed to write to file") 414 } 415 f.Close() 416 }