@@ -78,13 +78,23 @@ def load_info_yaml(
7878 }
7979 )
8080
81+ base_dir = os .path .dirname (regression_tests_path )
82+ pipelines = [
83+ get_absolute_path (base_dir , p ) for p in test .get ("pipelines" , [])
84+ ]
85+ filters = [
86+ get_absolute_path (base_dir , f ) for f in test .get ("filters" , [])
87+ ]
88+
8189 test_data .append (
8290 {
8391 "type" : test .get ("type" , "unknown" ),
8492 "path" : test_path ,
8593 "name" : test .get ("name" , "Unnamed Test" ),
8694 "provider" : test .get ("provider" , "" ),
8795 "match_count" : test .get ("match_count" ),
96+ "pipelines" : pipelines ,
97+ "filters" : filters ,
8898 }
8999 )
90100 info_metadata_rule_id = None
@@ -293,12 +303,88 @@ def run_evtx_checker(
293303 return False , ""
294304
295305
306+ def compile_rule_to_expr (
307+ rule_path : str , pipelines : List [str ], filters : List [str ]
308+ ) -> str :
309+ """Compile a Sigma rule to a golang_expr query via the sigma CLI."""
310+ cmd = ["sigma" , "convert" , "-t" , "golang_expr" ]
311+ for pipeline in pipelines :
312+ cmd += ["-p" , pipeline ]
313+ if len (pipelines ) == 0 :
314+ cmd += ["--without-pipeline" ]
315+ for filter in filters :
316+ cmd += ["--filter" , filter ]
317+ cmd .append (rule_path )
318+
319+ result = subprocess .run (
320+ cmd , capture_output = True , text = True , timeout = 300 , check = True
321+ )
322+
323+ return result .stdout .strip ()
324+
325+
326+ def run_json_checker (
327+ test_type : str ,
328+ rule_path : str ,
329+ rule_id : str ,
330+ test_data : Dict ,
331+ json_checker_path : str ,
332+ ) -> tuple [bool , str ]:
333+ """Compile the rule to an expr query and run json_checker against the events."""
334+ try :
335+ expr_query = compile_rule_to_expr (
336+ rule_path , test_data .get ("pipelines" , []), test_data .get ("filters" , [])
337+ )
338+ except subprocess .CalledProcessError as e :
339+ print (f" Error compiling rule { rule_id } with sigma: { e .stderr or e } " )
340+ return False , ""
341+ except subprocess .TimeoutExpired :
342+ print (f" Timeout compiling rule { rule_id } with sigma" )
343+ return False , ""
344+
345+ if not expr_query :
346+ print (f" Error: sigma produced an empty expr query for { rule_id } " )
347+ return False , ""
348+
349+ cmd = [json_checker_path , "--event" , test_data ["path" ], "--expr" , expr_query , "--test-type" , test_type ]
350+ try :
351+ result = subprocess .run (
352+ cmd , capture_output = True , text = True , timeout = 300 , check = True
353+ )
354+ except subprocess .TimeoutExpired :
355+ print (" Timeout: json_checker timed out" )
356+ return False , ""
357+ except subprocess .CalledProcessError as e :
358+ print (f" Error running json_checker: { e .stderr or e } " )
359+ return False , ""
360+
361+ match_lines = [ln for ln in result .stdout .splitlines () if ln .endswith ("MATCH" )]
362+ match_count = len (match_lines )
363+ all_output = "\n " .join (match_lines )
364+
365+ expected_count = test_data .get ("match_count" )
366+ if expected_count is not None :
367+ if match_count < expected_count :
368+ print (
369+ f" Error: { rule_id } : Match count too low: expected { expected_count } , got { match_count } "
370+ )
371+ return False , all_output
372+ if match_count > expected_count :
373+ print (
374+ f" Warning: { rule_id } : Got { match_count } matches but only { expected_count } expected - consider updating match_count in info.yml"
375+ )
376+ return True , all_output
377+
378+ return match_count > 0 , all_output
379+
380+
296381def run_test (
297382 rule_path : str ,
298383 rule_id : str ,
299384 test_data : Dict ,
300385 evtx_checker_path : str ,
301386 thor_config : str ,
387+ json_checker_path : str ,
302388) -> tuple [bool , str ]:
303389 """Run a test based on its type."""
304390 test_type = test_data .get ("type" , "unknown" )
@@ -307,6 +393,12 @@ def run_test(
307393 return run_evtx_checker (
308394 rule_path , rule_id , test_data , evtx_checker_path , thor_config
309395 )
396+
397+ if test_type == "jsonl" or test_type == "ndjson" or test_type == "json" :
398+ if not json_checker_path :
399+ print (" Error: --json-checker is required for 'ndjson/json' tests" )
400+ return False , ""
401+ return run_json_checker (test_type , rule_path , rule_id , test_data , json_checker_path )
310402 print (f" Warning: Unknown test type '{ test_type } ', skipping" )
311403 return False , ""
312404
@@ -335,6 +427,11 @@ def parse_arguments() -> argparse.Namespace:
335427 help = "Path to thor.yml configuration file (required unless using --validate-only)" ,
336428 )
337429
430+ parser .add_argument (
431+ "--json-checker" ,
432+ help = "Path to json_checker binary (required for 'ndjsonjson' tests)" ,
433+ )
434+
338435 parser .add_argument (
339436 "--validate-only" ,
340437 action = "store_true" ,
@@ -379,6 +476,11 @@ def init_checks(args: argparse.Namespace) -> None:
379476 if not os .path .exists (args .thor_config ):
380477 print (f"Error: Thor config not found at { args .thor_config } " )
381478 sys .exit (1 )
479+
480+ # json_checker is optional; only needed for 'ndjson/json' tests
481+ if args .json_checker and not os .path .exists (args .json_checker ):
482+ print (f"Error: json_checker not found at { args .json_checker } " )
483+ sys .exit (1 )
382484 print (f"Rules paths: { args .rules_paths } " )
383485
384486 if not args .validate_only :
@@ -414,7 +516,12 @@ def run_tests(
414516 total_tests += 1
415517
416518 success , output = run_test (
417- rule_path , rule_id , test_data , args .evtx_checker , args .thor_config
519+ rule_path ,
520+ rule_id ,
521+ test_data ,
522+ args .evtx_checker ,
523+ args .thor_config ,
524+ args .json_checker ,
418525 )
419526
420527 if success :
0 commit comments