Skip to content

Commit d36ee93

Browse files
committed
Add test-events command to run detections on JSON inputs
1 parent 6f4567a commit d36ee93

4 files changed

Lines changed: 500 additions & 2 deletions

File tree

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,15 @@ Show available commands and their options:
4141

4242
```bash
4343
$ panther_analysis_tool -h
44-
usage: panther_analysis_tool [-h] [--version] [--debug] [--skip-version-check] {release,test,publish,upload,delete,update-custom-schemas,test-lookup-table,validate,zip,check-connection,benchmark,enrich-test-data} ...
44+
usage: panther_analysis_tool [-h] [--version] [--debug] [--skip-version-check] {release,test,test-events,publish,upload,delete,update-custom-schemas,test-lookup-table,validate,zip,check-connection,benchmark,enrich-test-data} ...
4545

4646
Panther Analysis Tool: A command line tool for managing Panther policies and rules.
4747

4848
positional arguments:
49-
{release,test,publish,upload,delete,update-custom-schemas,test-lookup-table,validate,zip,check-connection,benchmark,enrich-test-data}
49+
{release,test,test-events,publish,upload,delete,update-custom-schemas,test-lookup-table,validate,zip,check-connection,benchmark,enrich-test-data}
5050
release Create release assets for repository containing panther detections. Generates a file called panther-analysis-all.zip and optionally generates panther-analysis-all.sig
5151
test Validate analysis specifications and run policy and rule tests.
52+
test-events Run selected rules/policies against raw event JSON files. Each EVENT_FILE may contain a single JSON object, a JSON array of objects, or NDJSON/JSONL.
5253
debug Run a single rule test in a debug environment, which allows you to see print statements and use breakpoints.
5354
publish Publishes a new release, generates the release assets, and uploads them. Generates a file called panther-analysis-all.zip and optionally generates panther-analysis-all.sig
5455
upload Upload specified policies and rules to a Panther deployment.
@@ -105,6 +106,16 @@ $ panther_analysis_tool test --filter RuleID=AWS.CloudTrail.Stopped --test-names
105106
[PASS] [rule] false
106107
```
107108
109+
### Test Events
110+
111+
Run a rule/policy against one-or-more raw event JSON files (JSON object, JSON array, or NDJSON/JSONL):
112+
113+
```bash
114+
$ panther_analysis_tool test-events --path path/to/analysis --filter RuleID=My.RuleID --log-type AWS.CloudTrail ./event.json ./more_events.jsonl
115+
Event 1 (p_log_type=AWS.CloudTrail)
116+
[ALERT] My.RuleID
117+
```
118+
108119
### Debug
109120
110121
Run a specific unit test in debug mode:
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import json
2+
import sys
3+
from dataclasses import dataclass
4+
from typing import Any, Dict, List, Optional
5+
6+
7+
@dataclass(frozen=True)
8+
class EventSource:
9+
"""Identifies where events were loaded from (for error reporting)."""
10+
11+
path: str
12+
13+
14+
class EventInputError(ValueError):
15+
pass
16+
17+
18+
def _ensure_event_dict(obj: Any, source: EventSource) -> Dict[str, Any]:
19+
if not isinstance(obj, dict):
20+
raise EventInputError(f"{source.path}: expected JSON object, got {type(obj).__name__}")
21+
return obj
22+
23+
24+
def load_events_from_text(text: str, source: EventSource) -> List[Dict[str, Any]]:
25+
"""
26+
Load one-or-more events from JSON text.
27+
28+
Supported formats:
29+
- A single JSON object: {"k": "v"}
30+
- A JSON array of objects: [{"k": "v"}, ...]
31+
- NDJSON / JSON Lines: one JSON object per line
32+
"""
33+
stripped = text.strip()
34+
if not stripped:
35+
return []
36+
37+
# First try "normal" JSON: object or array.
38+
try:
39+
parsed = json.loads(stripped)
40+
if isinstance(parsed, list):
41+
return [_ensure_event_dict(item, source) for item in parsed]
42+
return [_ensure_event_dict(parsed, source)]
43+
except json.JSONDecodeError:
44+
pass
45+
46+
# Fall back to NDJSON / JSON Lines
47+
events: List[Dict[str, Any]] = []
48+
for line_no, line in enumerate(text.splitlines(), start=1):
49+
if not line.strip():
50+
continue
51+
try:
52+
parsed_line = json.loads(line)
53+
except json.JSONDecodeError as exc:
54+
raise EventInputError(
55+
f"{source.path}:{line_no}: invalid JSON line ({exc.msg})"
56+
) from exc
57+
events.append(_ensure_event_dict(parsed_line, source))
58+
return events
59+
60+
61+
def load_events_from_path(path: str, encoding: str = "utf-8") -> List[Dict[str, Any]]:
62+
"""
63+
Load events from a path, or from stdin when path == "-".
64+
"""
65+
source = EventSource(path=path)
66+
if path == "-":
67+
return load_events_from_text(sys.stdin.read(), source)
68+
69+
try:
70+
with open(path, "r", encoding=encoding) as handle:
71+
return load_events_from_text(handle.read(), source)
72+
except OSError as exc:
73+
raise EventInputError(f"{path}: unable to read file ({exc})") from exc
74+
75+
76+
def apply_log_type(
77+
events: List[Dict[str, Any]],
78+
log_type: Optional[str],
79+
overwrite: bool,
80+
) -> List[Dict[str, Any]]:
81+
if not log_type:
82+
return events
83+
for event in events:
84+
if overwrite or "p_log_type" not in event:
85+
event["p_log_type"] = log_type
86+
return events
87+

0 commit comments

Comments
 (0)