Skip to content

Commit edc7623

Browse files
author
Paul Hager
committed
chore: add json/ndjson/jsonl to regression test
1 parent 2dbc894 commit edc7623

6 files changed

Lines changed: 157 additions & 3 deletions

File tree

.github/workflows/regression-tests.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ on: [push, pull_request, workflow_dispatch]
44

55
env:
66
EVTX_BASELINE_VERSION: v0.8.4
7+
JSON_MATCHER_VERSION: v0.0.2
78

89
jobs:
910
true-positive-tests:
@@ -19,13 +20,18 @@ jobs:
1920
- name: Install Python dependencies
2021
run: |
2122
python -m pip install --upgrade pip
22-
pip install pyyaml
23+
pip install pyyaml sigma-cli pySigma-backend-golangexpr
2324
2425
- name: Download evtx-sigma-checker
2526
run: |
2627
wget --no-verbose https://github.com/NextronSystems/evtx-baseline/releases/download/$EVTX_BASELINE_VERSION/evtx-sigma-checker
2728
chmod +x evtx-sigma-checker
29+
30+
- name: Download json-checker
31+
run: |
32+
wget --no-verbose https://github.com/pH-T/json_matcher/releases/download/$JSON_MATCHER_VERSION/json_matcher_v0.0.1 -O json_checker
33+
chmod +x json_checker
2834
2935
- name: Run regression tests
3036
run: |
31-
python tests/regression_tests_runner.py --rules-paths rules rules-emerging-threats rules-threat-hunting --evtx-checker ./evtx-sigma-checker --thor-config tests/thor.yml --ignore-validation
37+
python tests/regression_tests_runner.py --rules-paths rules rules-emerging-threats rules-threat-hunting --evtx-checker ./evtx-sigma-checker --thor-config tests/thor.yml --ignore-validation --json-checker ./json_checker
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: process_creation_fieldmapping
2+
priority: 100
3+
transformations:
4+
- id: field_mapping_process_creation
5+
type: field_name_mapping
6+
mapping:
7+
CommandLine: Event.EventData.CommandLine
8+
Image: Event.EventData.Image
9+
OriginalFileName: Event.EventData.OriginalFileName
10+
rule_conditions:
11+
- type: logsource
12+
category: process_creation
13+
product: windows
14+
- id: field_mapping_registry_set
15+
type: field_name_mapping
16+
mapping:
17+
TargetObject: Event.EventData.TargetObject
18+
Details: Event.EventData.Details
19+
Image: Event.EventData.Image
20+
rule_conditions:
21+
- type: logsource
22+
category: registry_set
23+
product: windows

regression_data/rules/windows/process_creation/proc_creation_win_amsi_registry_tampering/info.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,9 @@ regression_tests_info:
1111
provider: Microsoft-Windows-Sysmon
1212
match_count: 1
1313
path: regression_data/rules/windows/process_creation/proc_creation_win_amsi_registry_tampering/7dbbcac2-57a0-45ac-b306-ff30a8bd2981.evtx
14+
- name: Positive Detection Test
15+
type: json
16+
match_count: 1
17+
pipelines:
18+
- regression_data/pipelines/process_creation_fieldmapping.yml
19+
path: regression_data/rules/windows/process_creation/proc_creation_win_amsi_registry_tampering/7dbbcac2-57a0-45ac-b306-ff30a8bd2981.json

regression_data/rules/windows/process_creation/proc_creation_win_autologger_session_registry_modification/info.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,9 @@ regression_tests_info:
1111
provider: Microsoft-Windows-Sysmon
1212
match_count: 1
1313
path: regression_data/rules/windows/process_creation/proc_creation_win_autologger_session_registry_modification/d7b81144-b866-48a4-9bcc-275dc69d870e.evtx
14+
- name: Positive Detection Test
15+
type: json
16+
match_count: 1
17+
pipelines:
18+
- regression_data/pipelines/process_creation_fieldmapping.yml
19+
path: regression_data/rules/windows/process_creation/proc_creation_win_autologger_session_registry_modification/d7b81144-b866-48a4-9bcc-275dc69d870e.json

regression_data/rules/windows/registry/registry_set/registry_set_add_load_service_in_safe_mode/info.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,9 @@ regression_tests_info:
1111
provider: Microsoft-Windows-Sysmon
1212
match_count: 1
1313
path: regression_data/rules/windows/registry/registry_set/registry_set_add_load_service_in_safe_mode/1547e27c-3974-43e2-a7d7-7f484fb928ec.evtx
14+
- name: Positive Detection Test
15+
type: json
16+
match_count: 1
17+
pipelines:
18+
- regression_data/pipelines/process_creation_fieldmapping.yml
19+
path: regression_data/rules/windows/registry/registry_set/registry_set_add_load_service_in_safe_mode/1547e27c-3974-43e2-a7d7-7f484fb928ec.json

tests/regression_tests_runner.py

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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+
296381
def 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

Comments
 (0)