191 lines
5.3 KiB
Python
191 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Malicious-pattern detection rules for the intrusion-detection system.
|
|
|
|
Defines compiled regex patterns and classifiers that identify
|
|
probing/scanning URLs (e.g. env files, wp-admin, webshells) and
|
|
suspicious file names. Imported by analyzers to flag IPs hitting
|
|
these patterns as blocklist candidates.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass, field
|
|
from typing import ClassVar, Pattern
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Pattern categories
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BadPattern:
|
|
"""A single detection rule.
|
|
|
|
Attributes:
|
|
name: Short identifier (e.g. "env_file", "wp_admin").
|
|
label: Human-readable description for reports.
|
|
regex: Compiled regex matched against the request path.
|
|
severity: 1 = moderate, 2 = high, 3 = critical (auto-block).
|
|
"""
|
|
|
|
name: str
|
|
label: str
|
|
regex: Pattern[str]
|
|
severity: int = 2
|
|
|
|
def matches(self, path: str) -> bool:
|
|
return bool(self.regex.search(path))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Compiled pattern bank
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# -- env / config file probes -------------------------------------------------
|
|
|
|
ENV_FILE = BadPattern(
|
|
"env_file",
|
|
"env/config file probe (.env, .aws/credentials, config.json)",
|
|
re.compile(
|
|
r"/\.(?:env|config)(?:\.\w+)*|"
|
|
r"/\.aws/credentials|"
|
|
r"/config\.json",
|
|
re.IGNORECASE,
|
|
),
|
|
severity=3,
|
|
)
|
|
|
|
# -- version control probes ---------------------------------------------------
|
|
|
|
VCS_PROBE = BadPattern(
|
|
"vcs_probe",
|
|
"version-control probe (.git, .svn)",
|
|
re.compile(
|
|
r"/\.(?:git|svn)(?:/|$)",
|
|
re.IGNORECASE,
|
|
),
|
|
severity=3,
|
|
)
|
|
|
|
# -- WordPress / CMS probes ---------------------------------------------------
|
|
|
|
WP_PROBE = BadPattern(
|
|
"wp_probe",
|
|
"WordPress/CMS probe (wp-admin, wp-login, wp-content, wp-includes)",
|
|
re.compile(
|
|
r"/wp-(?:admin|login|content|includes|load|cron|xmlrpc)(?:/|\.php)",
|
|
re.IGNORECASE,
|
|
),
|
|
severity=2,
|
|
)
|
|
|
|
# -- well-known / common probe paths ------------------------------------------
|
|
|
|
WELL_KNOWN_PROBE = BadPattern(
|
|
"well_known_probe",
|
|
".well-known probe",
|
|
re.compile(
|
|
r"/\.well-known/",
|
|
re.IGNORECASE,
|
|
),
|
|
severity=1,
|
|
)
|
|
|
|
# -- webshell / backdoor probes -----------------------------------------------
|
|
|
|
WEBSHELL_PROBE = BadPattern(
|
|
"webshell_probe",
|
|
"webshell/backdoor probe (known shell filenames, phpinfo, file managers)",
|
|
re.compile(
|
|
r"/(?:"
|
|
r"phpinfo|info|shell|cmd|upload|"
|
|
r"wp_filemanager|filemanager|"
|
|
r"xstelth|adminfuns|hellopress|"
|
|
r"fpwch|crgio|coffexium|"
|
|
r"ccou|biufile|"
|
|
r"ms-edit|classwithtostring|"
|
|
r"this_is_a_new_hello_world|"
|
|
r"chosen|ops|simple|inputs"
|
|
r")\.php",
|
|
re.IGNORECASE,
|
|
),
|
|
severity=3,
|
|
)
|
|
|
|
# -- suspicious generic PHP file requests -------------------------------------
|
|
|
|
GENERIC_PHP_PROBE = BadPattern(
|
|
"generic_php_probe",
|
|
"generic/random PHP file probe (single-char, numeric names)",
|
|
re.compile(
|
|
r"/(?:"
|
|
r"[a-z]\.php|" # single letter: a.php, k.php, x.php
|
|
r"\d{1,3}\.php|" # numeric: 1.php, 222.php, 155.php
|
|
r"[a-z]{2}\.php|" # two-letter: aa.php, dx.php
|
|
r"a\d\.php|" # a1.php
|
|
r"\w+\.php" # generic: anything.php
|
|
r")",
|
|
re.IGNORECASE,
|
|
),
|
|
severity=1,
|
|
)
|
|
|
|
# -- sensitive file probes ----------------------------------------------------
|
|
|
|
SENSITIVE_FILE = BadPattern(
|
|
"sensitive_file",
|
|
"sensitive file probe (id_rsa, id_dsa, .pem, .key)",
|
|
re.compile(
|
|
r"/(?:id_rsa|id_dsa|id_ecdsa|id_ed25519|.*\.(?:pem|key|ppk|pfx|p12))",
|
|
re.IGNORECASE,
|
|
),
|
|
severity=3,
|
|
)
|
|
|
|
# -- suspicious user-agent (optional, path-based only for now) -----------------
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# All active patterns (ordered by severity, highest first)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
ALL_PATTERNS: list[BadPattern] = [
|
|
ENV_FILE,
|
|
VCS_PROBE,
|
|
SENSITIVE_FILE,
|
|
WEBSHELL_PROBE,
|
|
WP_PROBE,
|
|
GENERIC_PHP_PROBE,
|
|
WELL_KNOWN_PROBE,
|
|
]
|
|
|
|
# Quick lookup by name
|
|
PATTERNS_BY_NAME: dict[str, BadPattern] = {p.name: p for p in ALL_PATTERNS}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Classification API
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def classify_path(path: str) -> list[BadPattern]:
|
|
"""Return all patterns that match a given request path."""
|
|
return [p for p in ALL_PATTERNS if p.matches(path)]
|
|
|
|
|
|
def is_malicious(path: str) -> bool:
|
|
"""Return True if any pattern matches the path."""
|
|
return any(p.matches(path) for p in ALL_PATTERNS)
|
|
|
|
|
|
def malicious_severity(path: str) -> int:
|
|
"""Return the highest severity among matching patterns, or 0 if none."""
|
|
matched = classify_path(path)
|
|
return max((p.severity for p in matched), default=0)
|
|
|
|
|
|
def pattern_names(path: str) -> list[str]:
|
|
"""Return names of all patterns matching the path."""
|
|
return [p.name for p in classify_path(path)]
|