Loglayzr — flag the noise, find the attacks.
A zero-dependency Python toolkit for scanning Apache/Nginx combined-format access logs for suspicious activity. Uses signature-based pattern matching, GeoIP enrichment, and interactive browsing to surface reconnaissance, exploitation attempts, and brute-force attacks hiding in your logs.
- 57 built-in detection patterns covering SQL injection, command injection, path traversal, web shells, CVE probes, WordPress attacks, credential stuffing, config scraping, XSS, scanner user-agents, and more
- GeoIP enrichment — tags matches with country codes; whitelist your own countries to suppress noise
- IP whitelist — ignore known-safe IPs (internal servers, monitoring)
- Static asset noise filter — skips legitimate CSS/JS/image loads from real page views
- Directory mode — pass a directory instead of a file to scan all logs recursively (handles
.gztransparently) - Two interfaces — summary report (
analyze.py) and interactive browser (browse.py) - Pipeline-friendly — JSONL output, pipe to
jq,grep, or any Unix tool - All configuration in one file — log format, whitelists, GeoIP command, filter settings live in
config.json
- Python 3.12+
geoiplookupfrom thegeoip-binpackage (for GeoIP lookups)
sudo apt install geoip-bin geoip-databaseNo Python packages outside the standard library.
# Summary of all suspicious activity in one file
python3 analyze.py logs/access.log
# Or process an entire directory (recursive, .gz supported)
python3 analyze.py logs/
# Save to a report file
python3 analyze.py logs/access.log > reports/site.report.txt
# Only critical hits
python3 analyze.py logs/ | grep '"sev":"CRITICAL"'
# Browse matches interactively (select category → pattern → browse)
python3 browse.py logs/access.log
# Aggregate by country across all logs in a directory
python3 analyze.py logs/ | grep '^{' | \
jq -s 'group_by(.cc) | map({cc: .[0].cc, count: length}) | sort_by(-.count)'python3 analyze.py <file|directory>
Accepts a single log file or a directory (walked recursively, .gz files decompressed on the fly, hidden files and known binary extensions skipped). All matches are aggregated into a single combined summary.
Single-file summary:
=== SUMMARY: access.log ===
Multi-file summary:
=== SUMMARY: logs/ (42 files) ===
Full example:
=== SUMMARY: access.log ===
Lines parsed: 9,277
Suspicious hits: 1,613
By severity:
CRITICAL: 80
HIGH: 388
MEDIUM: 230
LOW: 915
By category (top 10):
config_scraping 534
wordpress 407
...
Top attacking IPs:
136.109.155.86 58 [US] (env_file_probe×28, ...)
213.209.159.175 58 [US] (env_file_probe×58)
By country (non-whitelisted):
US 1234
CN 456
...
python3 browse.py <file|directory>
Also accepts a file or directory. Parses all logs, then presents menus to choose a category and pattern, then lets you browse matches one at a time.
| Key | Action |
|---|---|
N / Enter / Space |
Next match |
P / Backspace |
Previous match |
B |
Back to category/pattern menu |
Q / Ctrl+C |
Quit (works from menus too) |
Each match shows severity (color-coded), pattern description, full log entry, and country code for non-whitelisted IPs.
Detection rules live in patterns.json. Each pattern has:
| Field | Description |
|---|---|
name |
Unique identifier (e.g. env_file_probe) |
category |
Grouping label (config_scraping, sqli, rce, …) |
severity |
CRITICAL, HIGH, MEDIUM, or LOW |
regex |
Python regex applied to the target field (or null for length checks) |
field |
Which log field to match: url, user_agent, status, method, referrer |
type |
"length_check" (optional) — instead of regex, flags entries where the field exceeds threshold characters |
description |
Human-readable explanation shown in browse mode |
57 patterns across 13 categories: config_scraping (10), recon (8), wordpress (6), cve_exploit (6), sqli (5), rce (5), anomaly (4), xss (3), admin_panel (3), evasion (3), path_traversal (2), backdoor (1), php_exploit (1).
Add new patterns by editing patterns.json — no code changes needed.
All configurable settings live in config.json:
{
"log_format": {
"regex": "^(\\S+) \\S+ \\S+ \\[([^\\]]+)\\] \"(\\S+) (\\S+) (\\S+)\" (\\d{3}) (\\S+) \"([^\"]*)\" \"([^\"]*)\"",
"timestamp_format": "%d/%b/%Y:%H:%M:%S %z"
},
"patterns_file": "patterns.json",
"whitelists": {
"countries": ["CN", "IN", "LK"],
"ips": ["192.168.1.2"]
},
"geoip": {
"command": "geoiplookup",
"regex_search": "GeoIP Country Edition:\\s*([A-Z]{2})"
},
"filters": {
"static_extensions": [
".js", ".css", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp",
".ico", ".woff", ".woff2", ".ttf", ".eot", ".mp4", ".mp3", ".webm",
".pdf", ".zip", ".gz", ".tar"
]
}
}| Section | Setting | Default | Description |
|---|---|---|---|
log_format |
regex |
(combined log format) | Python regex to parse log lines. Must have 9 capture groups for IP, timestamp, method, URL, protocol, status, size, referrer, user-agent. |
log_format |
timestamp_format |
%d/%b/%Y:%H:%M:%S %z |
strptime format for the timestamp in log entries. |
patterns_file |
— | patterns.json |
Path to the pattern definitions file. Relative paths resolve from the config file's directory. |
whitelists |
countries |
["CN", "IN", "LK"] |
Two-letter country codes to ignore. Matches from these IPs are silently dropped. |
whitelists |
ips |
["192.168.1.2"] |
Specific IP addresses to ignore (e.g. internal hosts, monitoring services). |
geoip |
command |
geoiplookup |
Command to run for GeoIP lookups. Swap to mmdblookup, geoiplookup6, or any tool that outputs the country code on stdout. |
geoip |
regex_search |
GeoIP Country Edition:\s*([A-Z]{2}) |
Regex to extract the two-letter country code from the GeoIP command's output. First capture group must yield the code. |
filters |
static_extensions |
(see above) | File extensions that trigger the static-load noise filter when combined with a GET method and non-empty referrer. |
Missing or misconfigured settings fall back to sensible defaults — config.json can be sparse.
Each log line passes through filters in order before pattern matching:
- Log file resolution — if a directory was passed, walk it recursively. Skip hidden files/dirs and known binary extensions. Decompress
.gzon the fly. - Static load — GET + static extension (
.js,.css,.png, …) + non-empty referrer → skip - IP whitelist — IP in
IP_WHITELIST→ skip - Country whitelist — IP resolves to a country in
COUNTRY_WHITELIST→ skip - Pattern matching — remaining entries checked against all 57 patterns
Filters are defined in common.py — add new ones in should_skip().
loglayzr/
├── analyze.py # Summary report script
├── browse.py # Interactive match browser
├── common.py # Shared filtering, match construction, file resolution
├── config.py # Configuration loader (reads config.json)
├── config.json # User-facing configuration file
├── geoip.py # GeoIP lookups with caching
├── patterns.json # Suspicious pattern definitions (57 rules)
├── LICENSE # GPLv3
└── README.md
GNU General Public License v3.0 — see LICENSE.