Tools · Python and Bash for security automation · 06 / 20
Analyzing JSON lines logs in Python: CloudTrail, Suricata, and app logs without a SIEM
Stream large JSON lines files, filter and group events with plain Python, handle nested fields safely, and write results for a timeline.
JSON lines
Many tools write one JSON object per line: Suricata eve.json, Zeek in JSON mode, application logs, and exported cloud logs. Reading line by line keeps memory low even for multi-gigabyte files. CloudTrail delivers files containing a Records array, which you can flatten into lines first with jq.
Filter and group
A small helper reads nested fields with a dotted path and a default, so missing keys do not crash the script.
import gzip, json, sys, collections
def read(path):
opener = gzip.open if path.endswith('.gz') else open
with opener(path, 'rt') as f:
for line in f:
if line.strip():
yield json.loads(line)
def get(obj, dotted, default=None):
for part in dotted.split('.'):
if not isinstance(obj, dict) or part not in obj:
return default
obj = obj[part]
return obj
counts = collections.Counter()
for ev in read(sys.argv[1]):
if get(ev, 'errorCode') in ('AccessDenied', 'UnauthorizedOperation'):
counts[(get(ev, 'userIdentity.arn', '?'), get(ev, 'eventName'))] += 1
for (who, action), n in counts.most_common(20):
print(f'{n:5} {action:35} {who}')Lab: CloudTrail denied calls
Export a day of CloudTrail logs from a sandbox account, flatten them to JSON lines with jq, and run the script to list identities with the most AccessDenied errors by action. Many denials from one role often indicate a misconfigured application or someone probing permissions with stolen credentials. Then adapt the filter for Suricata alerts by changing the field names.
aws s3 sync s3://my-trail-bucket/AWSLogs/111122223333/CloudTrail/us-east-1/2026/11/02/ ./ct for f in ct/*.json.gz; do gzip -dc "$f" | jq -c '.Records[]'; done > ct.jsonl python3 denied.py ct.jsonl
When to use pandas or DuckDB
For ad hoc questions on a few million rows, DuckDB can query JSON lines files directly with SQL, and pandas.read_json(lines=True) loads them into a DataFrame. Plain Python streaming is best when files are huge or when you need custom parsing logic.
duckdb -c "SELECT eventName, count(*) n FROM read_json_auto('ct.jsonl') WHERE errorCode='AccessDenied' GROUP BY 1 ORDER BY n DESC LIMIT 10"Diagram
Plain Python streaming versus DuckDB
Choose by size and question.
Memory. One line at a time
Best for. Huge files, custom logic
Effort. Write the grouping yourself
Memory. Efficient columnar engine
Best for. Ad hoc questions with GROUP BY
Effort. One SQL query
Check yourself
1. Why read JSON lines files line by line?
2. Why use a helper with defaults to read nested fields?
3. Many AccessDenied errors from one role may indicate what?