Skip to main content

django-risk-guardian

PyPI Python 3.11+ Django 4.2+ CI License: MIT

Middleware de score de risco comportamental para Django.

Behavior-based risk scoring middleware for Django.

Rate limiting detecta volume. Risk Guardian detecta comportamento.

Rate limiting detects volume. Risk Guardian detects behavior.


Português | English


Português

O que é

Um middleware que analisa cada requisição HTTP e atribui um score de risco (0–100) a partir de múltiplos sinais comportamentais: taxa de requisições, user-agent, sessão, padrões de navegação e timing. Sinais fracos isolados não bloqueiam nada — combinados, revelam comportamento automatizado ou abusivo.

Detecção de bots é a aplicação mais óbvia do mecanismo, não o limite dele. O que o middleware entrega é um score que sua aplicação usa para decidir.

Como funciona

Request → Analyzers → Risk Score → Policy → Allow / Monitor / Challenge / Block

Cada analyzer contribui com um delta e uma razão. O score composto é comparado com os thresholds configurados e a decisão é aplicada antes da view.

Faixas de decisão

Score Decisão O que acontece
0–19 Allow Requisição segue normalmente, sem log
20–49 Monitor Segue normalmente, mas emite evento risk_assessed para observabilidade
50–79 Challenge request.risk.challenged = True — a view decide (2FA, CAPTCHA, confirmação)
80+ Block HTTP 429 e IP bloqueado por BLOCK_TTL_SECONDS

Os limiares de Challenge e Block são configuráveis via SCORE_THRESHOLD_CHALLENGE e SCORE_THRESHOLD_BLOCK.

Why Risk Guardian?

Cada camada de defesa enxerga uma dimensão diferente do tráfego:

Técnica Detecta
Rate limiting Volume
CAPTCHA Automação
IP blocking Origem
WAF Padrões conhecidos
Risk Guardian Comportamento composto

Risk Guardian complementa essas camadas — não substitui nenhuma delas. Continue usando WAF, rate limiting e CAPTCHA. O que falta nesse conjunto é a leitura de comportamento: um IP com taxa média + UA desatualizado + sem sessão em path autenticado passa por todos os filtros acima individualmente, mas é muito mais suspeito do que qualquer um desses sinais isolado. O score composto captura exatamente isso.

Casos de uso

  • Bots e scrapers que respeitam limites de taxa
  • Brute force distribuído
  • Credential stuffing
  • Scanners de vulnerabilidade (.env, wp-admin, paths de probe)
  • Comportamento anômalo de navegação (timing robótico, rotação de UA)
  • Abuso de endpoints caros ou sensíveis

Instalação

pip install django-risk-guardian
# settings.py (3 linhas)
INSTALLED_APPS += ["risk_guardian"]

MIDDLEWARE = [
    "risk_guardian.middleware.RiskGuardianMiddleware",
    # ... demais middlewares
]

Requisitos

  • Python 3.11+
  • Django 4.2+
  • Redis (via django-redis)

Configuração

Todos os parâmetros têm defaults funcionais. Sobrescreva apenas o necessário:

RISK_GUARDIAN = {
    # Comportamento geral
    "ENABLED": True,
    "CACHE_BACKEND": "default",           # backend do Django cache (deve ser Redis)
    "CACHE_PREFIX": "rg",
    "LOG_ALL_SCORES": False,

    # Thresholds (0–100)
    "SCORE_THRESHOLD_BLOCK": 80,          # bloqueia a requisição
    "SCORE_THRESHOLD_CHALLENGE": 50,      # sinaliza para a view (ex: exigir 2FA)

    # Bloqueio
    "BLOCK_RESPONSE_CODE": 429,
    "BLOCK_TTL_SECONDS": 3600,

    # Histórico deslizante
    "HISTORY_WINDOW_SECONDS": 300,        # janela de 5 min
    "HISTORY_MAX_REQUESTS": 100,

    # Paths ignorados
    "IGNORE_PATHS": ["/health/", "/metrics/", "/__debug__/", "/favicon.ico"],

    # Analyzers ativos (ordem importa)
    "ANALYZERS": [
        "risk_guardian.analyzers.RateAnalyzer",
        "risk_guardian.analyzers.UserAgentAnalyzer",
        "risk_guardian.analyzers.SessionAnalyzer",
        "risk_guardian.analyzers.PatternAnalyzer",
        "risk_guardian.analyzers.TimingAnalyzer",
    ],
}

Chaves no cache

Todas as chaves usam o CACHE_PREFIX configurado (default rg):

Chave TTL Conteúdo
{prefix}:hist:ip:{ip} HISTORY_WINDOW_SECONDS + 60 Histórico deslizante do IP
{prefix}:hist:sess:{session_key} HISTORY_WINDOW_SECONDS + 60 Histórico deslizante da sessão
{prefix}:blocked:{ip} BLOCK_TTL_SECONDS Flag de IP bloqueado (checada antes de qualquer análise)
{prefix}:sess_set:{ip} 300 Conjunto de sessões distintas vistas no IP

Cada entrada de histórico guarda ts, path, method, status, ua (truncado em 100 chars) e duration_ms. A lista é limitada a HISTORY_MAX_REQUESTS entradas.

Uso nas views

Após o middleware processar, toda view tem acesso a request.risk:

def minha_view(request):
    if request.risk.challenged:
        return redirect("verificacao_2fa")

    print(request.risk.score)      # int (0–100)
    print(request.risk.reasons)    # ["high_rate", "outdated_browser"]
    print(request.risk.blocked)    # bool

Decorators

from risk_guardian.decorators import require_risk_below, require_no_challenge

@require_risk_below(50)
def endpoint_sensivel(request):
    ...

@require_no_challenge
def area_restrita(request):
    ...

Signals

from django.dispatch import receiver
from risk_guardian.signals import ip_blocked

@receiver(ip_blocked)
def notificar_bloqueio(sender, ip, score, reasons, **kwargs):
    SlackNotifier.send(f"IP bloqueado: {ip} (score={score})")

Signals disponíveis: ip_blocked, risk_assessed, challenge_required, email_risk_assessed.

Analyzers

Analyzer Detecta Score máximo Reasons emitidos
RateAnalyzer Volume anormal de requisições por IP +50 critical_rate, high_rate, medium_rate
UserAgentAnalyzer UAs de bots, browsers desatualizados, UA vazio +40 bot_ua:curl, missing_ua, outdated_browser
SessionAnalyzer Sessão ausente, rotação de UA, sessões excessivas por IP +35 no_session_on_auth_path, session_ua_rotation, excessive_sessions_per_ip
PatternAnalyzer Paths de scan (.env, wp-admin), taxa de erro alta, diversidade de paths +60 scan_attempt:/.env, high_error_rate, excessive_path_diversity
TimingAnalyzer Intervalos artificialmente regulares entre requisições +30 robotic_timing

EmailAnalyzer (login e cadastro)

O EmailAnalyzer não é um analyzer de middleware — não roda por requisição e não entra na lista ANALYZERS. Ele avalia o endereço de e-mail nos signals de autenticação do Django (user_logged_in e user_login_failed) e é registrado automaticamente quando risk_guardian está em INSTALLED_APPS. Nenhuma configuração necessária.

Diferente dos analyzers de middleware, que retornam um único sinal, ele pode retornar vários de uma vez — todos somados ao request.risk:

Reason Delta Detecta
disposable_email +40 Domínio descartável (mailinator, guerrillamail, yopmail, 10minutemail, ...)
suspicious_email_hex_suffix +30 Sufixo hexadecimal longo no local part (ex: user4f3a9b2c1d@...)
suspicious_email_entropy +30 Entropia de Shannon ≥ 3.5 — local part aparentemente gerado por máquina
suspicious_email_digits +25 Proporção de dígitos ≥ 50% no local part

Ordem de execução importa. O login acontece dentro da view, ou seja, depois que o middleware já decidiu bloquear ou não. Os deltas de e-mail elevam request.risk.score durante a requisição, mas não disparam bloqueio retroativo. Cabe à view reler o score após o login:

from django.contrib.auth import login

def view_de_login(request):
    login(request, user)   # dispara user_logged_in → EmailAnalyzer

    if request.risk.score >= 80:
        return redirect("verificacao_manual")

Para reagir ao evento, use o signal email_risk_assessed:

from django.dispatch import receiver
from risk_guardian.signals import email_risk_assessed

@receiver(email_risk_assessed)
def alertar_cadastro_suspeito(sender, request, user, email, score, reasons, **kwargs):
    SlackNotifier.send(f"Login suspeito: {user.pk} (score={score}, {reasons})")

Os receivers automáticos usam os limiares padrão. Para ajustá-los, instancie o analyzer diretamente no seu próprio handler:

from risk_guardian.analyzers import EmailAnalyzer

analyzer = EmailAnalyzer(
    digit_ratio_threshold=0.5,     # proporção de dígitos no local part
    entropy_threshold=3.5,         # entropia de Shannon
    min_length_for_entropy=8,      # tamanho mínimo para avaliar entropia
    hex_suffix_threshold=10,       # tamanho do sufixo hex
)

analyzer.evaluate("user4f3a9b2c1d@mailinator.com")
# [(40, "disposable_email"), (30, "suspicious_email_hex_suffix")]

Auditoria da base existente

O comando audit_emails aplica o EmailAnalyzer em todos os usuários já cadastrados — útil para encontrar contas criadas antes de o middleware entrar em produção:

python manage.py audit_emails
python manage.py audit_emails --format json --min-score 40
PK       Email                                         Score  Reasons
------------------------------------------------------------------------------
1042     user4f3a9b2c1d@mailinator.com                   100  disposable_email, suspicious_email_hex_suffix, suspicious_email_entropy
876      x7k2m9q4w1@guerrillamail.com                     65  disposable_email, suspicious_email_digits

Audited 12043 users, 2 flagged as suspicious.

Opções: --format (table ou json, padrão table) e --min-score (padrão 1).

Logs estruturados

O middleware emite JSON estruturado via logger risk_guardian:

{
  "event": "ip_blocked",
  "ip": "1.2.3.4",
  "score": 85,
  "reasons": ["high_rate", "missing_ua"],
  "request_id": "abc-123"
}

Eventos emitidos: risk_assessed, ip_blocked, challenge_required, analyzer_error, email_risk_assessed.

O evento email_risk_assessed loga apenas o domínio do e-mail, nunca o endereço completo.

Decisões de design

Nada assíncrono no caminho crítico. O middleware roda de forma síncrona no ciclo da requisição e toda análise usa apenas o cache — nenhuma consulta ao banco, nenhuma chamada de rede externa, nenhuma task. A decisão de bloquear nunca depende de infraestrutura que possa estar lenta ou fora do ar. Persistência e notificação ficam a cargo dos signals, fora do caminho da decisão.

Falha de analyzer nunca derruba a requisição. Se um analyzer lança exceção, o middleware loga analyzer_error, ignora aquele analyzer e segue com os demais. Um bug no scoring degrada a detecção, não a disponibilidade da aplicação.

Mecanismo público, configuração privada. O algoritmo é aberto, mas os valores operacionais não. Seguindo o modelo do fail2ban e do ModSecurity: quem leu este repositório ainda não sabe quais são os seus thresholds reais, quais analyzers você deixou ativos, nem o histórico já acumulado do IP dele. O segredo operacional está na configuração do seu projeto, não no algoritmo — por isso todos os parâmetros são sobrescrevíveis via RISK_GUARDIAN e os defaults são apenas um ponto de partida razoável.

Testes

pip install -e ".[dev]"
pytest tests/ -v

Com relatório de cobertura (o CI exige no mínimo 90%):

pytest tests/ --cov --cov-report=term-missing --cov-fail-under=90

English

What is it

A middleware that analyzes each HTTP request and assigns a risk score (0–100) from multiple behavioral signals: request rate, user-agent, session, navigation patterns, and timing. Weak signals alone block nothing — combined, they reveal automated or abusive behavior.

Bot detection is the most obvious application of the mechanism, not its limit. What the middleware delivers is a score your application uses to decide.

How it works

Request → Analyzers → Risk Score → Policy → Allow / Monitor / Challenge / Block

Each analyzer contributes a delta and a reason. The composite score is compared against the configured thresholds and the decision is applied before the view runs.

Decision bands

Score Decision What happens
0–19 Allow Request proceeds normally, no logging
20–49 Monitor Proceeds normally, but emits a risk_assessed event for observability
50–79 Challenge request.risk.challenged = True — the view decides (2FA, CAPTCHA, confirmation)
80+ Block HTTP 429 and the IP is blocked for BLOCK_TTL_SECONDS

Challenge and Block thresholds are configurable via SCORE_THRESHOLD_CHALLENGE and SCORE_THRESHOLD_BLOCK.

Why Risk Guardian?

Each defense layer sees a different dimension of traffic:

Technique Detects
Rate limiting Volume
CAPTCHA Automation
IP blocking Origin
WAF Known patterns
Risk Guardian Composite behavior

Risk Guardian complements these layers — it replaces none of them. Keep your WAF, rate limiting, and CAPTCHA. What that stack is missing is a read on behavior: an IP with a medium rate + an outdated UA + no session on an authenticated path slips past each of those filters individually, yet is far more suspicious than any single one of those signals alone. Composite scoring captures exactly that.

Use cases

  • Bots and scrapers that stay within rate limits
  • Distributed brute force
  • Credential stuffing
  • Vulnerability scanners (.env, wp-admin, probe paths)
  • Anomalous browsing behavior (robotic timing, UA rotation)
  • Abuse of expensive or sensitive endpoints

Installation

pip install django-risk-guardian
# settings.py (3 lines)
INSTALLED_APPS += ["risk_guardian"]

MIDDLEWARE = [
    "risk_guardian.middleware.RiskGuardianMiddleware",
    # ... other middlewares
]

Requirements

  • Python 3.11+
  • Django 4.2+
  • Redis (via django-redis)

Configuration

All parameters have functional defaults. Override only what you need:

RISK_GUARDIAN = {
    # General behavior
    "ENABLED": True,
    "CACHE_BACKEND": "default",           # Django cache backend (should be Redis)
    "CACHE_PREFIX": "rg",
    "LOG_ALL_SCORES": False,

    # Thresholds (0–100)
    "SCORE_THRESHOLD_BLOCK": 80,          # blocks the request
    "SCORE_THRESHOLD_CHALLENGE": 50,      # flags for the view (e.g., require 2FA)

    # Blocking
    "BLOCK_RESPONSE_CODE": 429,
    "BLOCK_TTL_SECONDS": 3600,

    # Sliding history
    "HISTORY_WINDOW_SECONDS": 300,        # 5-minute window
    "HISTORY_MAX_REQUESTS": 100,

    # Ignored paths
    "IGNORE_PATHS": ["/health/", "/metrics/", "/__debug__/", "/favicon.ico"],

    # Active analyzers (order matters)
    "ANALYZERS": [
        "risk_guardian.analyzers.RateAnalyzer",
        "risk_guardian.analyzers.UserAgentAnalyzer",
        "risk_guardian.analyzers.SessionAnalyzer",
        "risk_guardian.analyzers.PatternAnalyzer",
        "risk_guardian.analyzers.TimingAnalyzer",
    ],
}

Cache keys

All keys use the configured CACHE_PREFIX (default rg):

Key TTL Contents
{prefix}:hist:ip:{ip} HISTORY_WINDOW_SECONDS + 60 Sliding history for the IP
{prefix}:hist:sess:{session_key} HISTORY_WINDOW_SECONDS + 60 Sliding history for the session
{prefix}:blocked:{ip} BLOCK_TTL_SECONDS Blocked-IP flag (checked before any analysis)
{prefix}:sess_set:{ip} 300 Set of distinct sessions seen for the IP

Each history entry stores ts, path, method, status, ua (truncated to 100 chars) and duration_ms. The list is capped at HISTORY_MAX_REQUESTS entries.

Usage in views

After the middleware processes a request, every view has access to request.risk:

def my_view(request):
    if request.risk.challenged:
        return redirect("2fa_verification")

    print(request.risk.score)      # int (0–100)
    print(request.risk.reasons)    # ["high_rate", "outdated_browser"]
    print(request.risk.blocked)    # bool

Decorators

from risk_guardian.decorators import require_risk_below, require_no_challenge

@require_risk_below(50)
def sensitive_endpoint(request):
    ...

@require_no_challenge
def restricted_area(request):
    ...

Signals

from django.dispatch import receiver
from risk_guardian.signals import ip_blocked

@receiver(ip_blocked)
def notify_block(sender, ip, score, reasons, **kwargs):
    SlackNotifier.send(f"IP blocked: {ip} (score={score})")

Available signals: ip_blocked, risk_assessed, challenge_required, email_risk_assessed.

Analyzers

Analyzer Detects Max score Emitted reasons
RateAnalyzer Abnormal request volume per IP +50 critical_rate, high_rate, medium_rate
UserAgentAnalyzer Bot UAs, outdated browsers, missing UA +40 bot_ua:curl, missing_ua, outdated_browser
SessionAnalyzer Missing session, UA rotation, excessive sessions per IP +35 no_session_on_auth_path, session_ua_rotation, excessive_sessions_per_ip
PatternAnalyzer Scan paths (.env, wp-admin), high error rate, path diversity +60 scan_attempt:/.env, high_error_rate, excessive_path_diversity
TimingAnalyzer Artificially regular intervals between requests +30 robotic_timing

EmailAnalyzer (login and signup)

EmailAnalyzer is not a middleware analyzer — it doesn't run per request and doesn't belong in the ANALYZERS list. It evaluates the email address on Django's authentication signals (user_logged_in and user_login_failed) and is registered automatically when risk_guardian is in INSTALLED_APPS. No configuration required.

Unlike middleware analyzers, which return a single signal, it can return several at once — all added to request.risk:

Reason Delta Detects
disposable_email +40 Disposable domain (mailinator, guerrillamail, yopmail, 10minutemail, ...)
suspicious_email_hex_suffix +30 Long hexadecimal suffix in the local part (e.g. user4f3a9b2c1d@...)
suspicious_email_entropy +30 Shannon entropy ≥ 3.5 — machine-generated-looking local part
suspicious_email_digits +25 Digit ratio ≥ 50% in the local part

Execution order matters. Login happens inside the view — that is, after the middleware has already decided whether to block. Email deltas raise request.risk.score during the request, but do not trigger a retroactive block. It's up to the view to re-read the score after login:

from django.contrib.auth import login

def login_view(request):
    login(request, user)   # fires user_logged_in → EmailAnalyzer

    if request.risk.score >= 80:
        return redirect("manual_verification")

To react to the event, use the email_risk_assessed signal:

from django.dispatch import receiver
from risk_guardian.signals import email_risk_assessed

@receiver(email_risk_assessed)
def alert_suspicious_signup(sender, request, user, email, score, reasons, **kwargs):
    SlackNotifier.send(f"Suspicious login: {user.pk} (score={score}, {reasons})")

The auto-registered receivers use the default thresholds. To tune them, instantiate the analyzer directly in your own handler:

from risk_guardian.analyzers import EmailAnalyzer

analyzer = EmailAnalyzer(
    digit_ratio_threshold=0.5,     # digit ratio in the local part
    entropy_threshold=3.5,         # Shannon entropy
    min_length_for_entropy=8,      # minimum length to evaluate entropy
    hex_suffix_threshold=10,       # hex suffix length
)

analyzer.evaluate("user4f3a9b2c1d@mailinator.com")
# [(40, "disposable_email"), (30, "suspicious_email_hex_suffix")]

Auditing an existing user base

The audit_emails command applies EmailAnalyzer to all existing users — useful for finding accounts created before the middleware went to production:

python manage.py audit_emails
python manage.py audit_emails --format json --min-score 40
PK       Email                                         Score  Reasons
------------------------------------------------------------------------------
1042     user4f3a9b2c1d@mailinator.com                   100  disposable_email, suspicious_email_hex_suffix, suspicious_email_entropy
876      x7k2m9q4w1@guerrillamail.com                     65  disposable_email, suspicious_email_digits

Audited 12043 users, 2 flagged as suspicious.

Options: --format (table or json, default table) and --min-score (default 1).

Structured logs

The middleware emits structured JSON via the risk_guardian logger:

{
  "event": "ip_blocked",
  "ip": "1.2.3.4",
  "score": 85,
  "reasons": ["high_rate", "missing_ua"],
  "request_id": "abc-123"
}

Emitted events: risk_assessed, ip_blocked, challenge_required, analyzer_error, email_risk_assessed.

The email_risk_assessed event logs only the email domain, never the full address.

Design decisions

Nothing asynchronous on the critical path. The middleware runs synchronously within the request cycle, and all analysis hits the cache only — no database queries, no external network calls, no tasks. The decision to block never depends on infrastructure that might be slow or down. Persistence and notification are left to signals, outside the decision path.

An analyzer failure never takes down the request. If an analyzer raises, the middleware logs analyzer_error, skips that analyzer and continues with the rest. A bug in scoring degrades detection, not your application's availability.

Public mechanism, private configuration. The algorithm is open; the operational values are not. Following the fail2ban and ModSecurity model: someone who has read this repository still doesn't know your actual thresholds, which analyzers you left enabled, or the history already accumulated for their IP. The operational secret lives in your project's configuration, not in the algorithm — which is why every parameter is overridable via RISK_GUARDIAN and the defaults are only a reasonable starting point.

Tests

pip install -e ".[dev]"
pytest tests/ -v

With a coverage report (CI enforces a minimum of 90%):

pytest tests/ --cov --cov-report=term-missing --cov-fail-under=90

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_risk_guardian-0.1.1.tar.gz (27.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_risk_guardian-0.1.1-py3-none-any.whl (22.7 kB view details)

Uploaded Python 3

File details

Details for the file django_risk_guardian-0.1.1.tar.gz.

File metadata

  • Download URL: django_risk_guardian-0.1.1.tar.gz
  • Upload date:
  • Size: 27.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.4

File hashes

Hashes for django_risk_guardian-0.1.1.tar.gz
Algorithm Hash digest
SHA256 2f1bb8f4e57b2801e8790ecfebdea35decfc4e8f24b5a57454672889f0f8ff80
MD5 d1f00484ee02b0c8fd0bb9a2982eae55
BLAKE2b-256 1076aa73bd82ab06dcf11e6e199c3e9c7cfc26cef008568a3c542561e9757879

See more details on using hashes here.

File details

Details for the file django_risk_guardian-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for django_risk_guardian-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 d6e9bd3e7a1e5e0cdcdf4c8b7ccd0092ed0b6ef69e538c1e3966633453b5379c
MD5 c1aba9843beaf3d064accec2094b5011
BLAKE2b-256 9038b89be6587dcb53205268071a2546c068944452f7a5384d8870b158a9b2b5

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page