Skip to content

Instantly share code, notes, and snippets.

@zr0n
Created January 16, 2026 21:54
Show Gist options
  • Select an option

  • Save zr0n/d488b4ae9c15881c16670b3670b191ec to your computer and use it in GitHub Desktop.

Select an option

Save zr0n/d488b4ae9c15881c16670b3670b191ec to your computer and use it in GitHub Desktop.
"""
CODE CHALLENGE (prova em branco)
Implemente a função detect_failed_logins(lines, window_seconds=60, threshold=5)
Retorno esperado:
- dict no formato:
{
"IP": {"count": <int>},
...
}
Regras:
- Considerar "login inválido" quando:
PATH in {"/login", "/auth/login"} AND STATUS in {401, 403}
- Para cada IP, se ocorrerem >= threshold falhas dentro de uma janela de window_seconds,
o IP deve aparecer no retorno.
- "Dentro da janela" significa: se um evento ocorre em T, contam os eventos com timestamp >= T - window_seconds.
- Ignore linhas mal formatadas (não quebre).
- Comportamento padrão: se NÃO houver "pico" (sem padrão de ddos/bruteforce),
o retorno deve ser {}.
Observação didática:
- Aqui a gente chama de "ddos" no sentido de "pico de requisições suspeitas",
mas o detector é especificamente de *falhas de login* (bruteforce/rate abuse).
"""
# ========= O ALUNO IMPLEMENTA AQUI =========
def detect_failed_logins(lines, window_seconds=60, threshold=5):
"""
TODO: implementar.
Dica: parse do timestamp, path e status.
"""
raise NotImplementedError
# ===========================================
# --------- DADOS DE TESTE ---------
# 0) Comportamento padrão: acessos normais, sem pico de falhas de login -> {}
LOGS_0_NORMAL_BEHAVIOR = [
'200.200.200.1 - - [10/Jan/2026:10:00:00 -0300] "GET / HTTP/1.1" 200 1200 "Mozilla/5.0"',
'200.200.200.1 - - [10/Jan/2026:10:00:03 -0300] "GET /products HTTP/1.1" 200 980 "Mozilla/5.0"',
'201.201.201.2 - - [10/Jan/2026:10:00:10 -0300] "GET /about HTTP/1.1" 200 640 "Mozilla/5.0"',
'201.201.201.2 - - [10/Jan/2026:10:00:20 -0300] "POST /login HTTP/1.1" 200 256 "Mozilla/5.0"', # login OK
'200.200.200.1 - - [10/Jan/2026:10:00:25 -0300] "GET /profile HTTP/1.1" 200 777 "Mozilla/5.0"',
]
# 0b) "Não é ddos": tem algumas falhas de login, mas poucas e espaçadas (nunca cruza threshold na janela) -> {}
LOGS_0B_SPARSE_FAILS_NO_SPIKE = [
'10.10.10.10 - - [10/Jan/2026:10:00:00 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"',
'10.10.10.10 - - [10/Jan/2026:10:02:10 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"',
'10.10.10.10 - - [10/Jan/2026:10:04:30 -0300] "POST /login HTTP/1.1" 403 256 "Mozilla/5.0"',
'10.10.10.10 - - [10/Jan/2026:10:08:00 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"',
'10.10.10.10 - - [10/Jan/2026:10:12:00 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"',
]
# 1) Simples: 5 falhas em 60s => alerta
LOGS_1_SIMPLE_TRIGGER = [
'192.168.0.10 - - [10/Jan/2026:10:01:00 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"',
'192.168.0.10 - - [10/Jan/2026:10:01:10 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"',
'192.168.0.10 - - [10/Jan/2026:10:01:20 -0300] "POST /login HTTP/1.1" 403 256 "Mozilla/5.0"',
'192.168.0.10 - - [10/Jan/2026:10:01:30 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"',
'192.168.0.10 - - [10/Jan/2026:10:01:40 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"',
]
# 2) Abaixo do threshold => não alerta
LOGS_2_BELOW_THRESHOLD = LOGS_1_SIMPLE_TRIGGER[:-1] # só 4 falhas
# 3) Janela estourada: 5 falhas, mas espalhadas >60s => não alerta
LOGS_3_WINDOW_EXCEEDED = [
'10.0.0.5 - - [10/Jan/2026:10:00:00 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"',
'10.0.0.5 - - [10/Jan/2026:10:00:30 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"',
'10.0.0.5 - - [10/Jan/2026:10:01:01 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"',
'10.0.0.5 - - [10/Jan/2026:10:01:32 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"',
'10.0.0.5 - - [10/Jan/2026:10:02:05 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"',
]
# 4) Misturado: várias rotas/status; só /login 401/403 conta
LOGS_4_MIXED_ONLY_VALID_COUNT = [
'8.8.8.8 - - [10/Jan/2026:10:01:00 -0300] "GET / HTTP/1.1" 200 999 "Mozilla/5.0"', # ignora
'8.8.8.8 - - [10/Jan/2026:10:01:05 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"', # conta
'8.8.8.8 - - [10/Jan/2026:10:01:10 -0300] "POST /login HTTP/1.1" 500 256 "Mozilla/5.0"', # ignora (status)
'8.8.8.8 - - [10/Jan/2026:10:01:15 -0300] "POST /admin HTTP/1.1" 401 256 "Mozilla/5.0"', # ignora (path)
'8.8.8.8 - - [10/Jan/2026:10:01:20 -0300] "POST /login HTTP/1.1" 403 256 "Mozilla/5.0"', # conta
'8.8.8.8 - - [10/Jan/2026:10:01:25 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"', # conta
'8.8.8.8 - - [10/Jan/2026:10:01:30 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"', # conta
'8.8.8.8 - - [10/Jan/2026:10:01:35 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"', # conta (5ª)
]
# 5) Complexo: múltiplos IPs, intercalados; só um cruza threshold
LOGS_5_MULTI_IP_INTERLEAVED = [
'1.1.1.1 - - [10/Jan/2026:10:01:00 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"',
'2.2.2.2 - - [10/Jan/2026:10:01:01 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"',
'1.1.1.1 - - [10/Jan/2026:10:01:10 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"',
'2.2.2.2 - - [10/Jan/2026:10:01:11 -0300] "POST /login HTTP/1.1" 200 256 "Mozilla/5.0"', # ignora
'1.1.1.1 - - [10/Jan/2026:10:01:20 -0300] "POST /login HTTP/1.1" 403 256 "Mozilla/5.0"',
'1.1.1.1 - - [10/Jan/2026:10:01:30 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"',
'2.2.2.2 - - [10/Jan/2026:10:01:31 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"',
'1.1.1.1 - - [10/Jan/2026:10:01:40 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"', # 5ª do 1.1.1.1
]
# 6) Path alternativo /auth/login
LOGS_6_ALT_LOGIN_PATH = [
'9.9.9.9 - - [10/Jan/2026:10:01:00 -0300] "POST /auth/login HTTP/1.1" 401 256 "Mozilla/5.0"',
'9.9.9.9 - - [10/Jan/2026:10:01:10 -0300] "POST /auth/login HTTP/1.1" 401 256 "Mozilla/5.0"',
'9.9.9.9 - - [10/Jan/2026:10:01:20 -0300] "POST /auth/login HTTP/1.1" 401 256 "Mozilla/5.0"',
'9.9.9.9 - - [10/Jan/2026:10:01:30 -0300] "POST /auth/login HTTP/1.1" 401 256 "Mozilla/5.0"',
'9.9.9.9 - - [10/Jan/2026:10:01:40 -0300] "POST /auth/login HTTP/1.1" 403 256 "Mozilla/5.0"',
]
# 7) Linhas ruins no meio (não pode quebrar); ainda assim cruza threshold
LOGS_7_HAS_BAD_LINES = [
'THIS IS NOT A LOG LINE',
'3.3.3.3 - - [10/Jan/2026:10:01:00 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"',
'MALFORMED [10/Jan/2026:10:01:10 -0300] "POST /login HTTP/1.1" 401',
'3.3.3.3 - - [10/Jan/2026:10:01:20 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"',
'3.3.3.3 - - [10/Jan/2026:10:01:30 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"',
'3.3.3.3 - - [10/Jan/2026:10:01:40 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"',
'3.3.3.3 - - [10/Jan/2026:10:01:50 -0300] "POST /login HTTP/1.1" 401 256 "Mozilla/5.0"',
]
# --------- ASSERTS ---------
def run_asserts():
# Caso 0: comportamento normal (sem pico / "não ddos")
out = detect_failed_logins(LOGS_0_NORMAL_BEHAVIOR, window_seconds=60, threshold=5)
assert out == {}, "Sem padrão suspeito, retorno deve ser {}"
# Caso 0b: algumas falhas, mas espaçadas (sem pico)
out = detect_failed_logins(LOGS_0B_SPARSE_FAILS_NO_SPIKE, window_seconds=60, threshold=5)
assert out == {}, "Falhas espaçadas não devem gerar alerta"
# Caso 1: dispara
out = detect_failed_logins(LOGS_1_SIMPLE_TRIGGER, window_seconds=60, threshold=5)
assert isinstance(out, dict), "Retorno deve ser dict"
assert "192.168.0.10" in out, "Deveria alertar: 5 falhas em 60s"
assert out["192.168.0.10"]["count"] == 5, "Count deve ser 5 no disparo"
# Caso 2: não dispara (abaixo do threshold)
out = detect_failed_logins(LOGS_2_BELOW_THRESHOLD, window_seconds=60, threshold=5)
assert out == {}, "Não deveria alertar com 4 falhas (threshold 5)"
# Caso 3: não dispara (fora da janela)
out = detect_failed_logins(LOGS_3_WINDOW_EXCEEDED, window_seconds=60, threshold=5)
assert "10.0.0.5" not in out, "Não deveria alertar: falhas espalhadas >60s"
# Caso 4: mixed (só conta /login com 401/403)
out = detect_failed_logins(LOGS_4_MIXED_ONLY_VALID_COUNT, window_seconds=60, threshold=5)
assert "8.8.8.8" in out, "Deveria alertar contando apenas eventos válidos"
assert out["8.8.8.8"]["count"] == 5, "Deveria contar exatamente 5 falhas válidas"
# Caso 5: múltiplos IPs intercalados
out = detect_failed_logins(LOGS_5_MULTI_IP_INTERLEAVED, window_seconds=60, threshold=5)
assert "1.1.1.1" in out, "1.1.1.1 deveria alertar"
assert "2.2.2.2" not in out, "2.2.2.2 não deveria alertar"
assert out["1.1.1.1"]["count"] == 5
# Caso 6: /auth/login também vale
out = detect_failed_logins(LOGS_6_ALT_LOGIN_PATH, window_seconds=60, threshold=5)
assert "9.9.9.9" in out, "Deveria alertar em /auth/login também"
assert out["9.9.9.9"]["count"] == 5
# Caso 7: linhas ruins não podem quebrar
out = detect_failed_logins(LOGS_7_HAS_BAD_LINES, window_seconds=60, threshold=5)
assert "3.3.3.3" in out, "Mesmo com linhas ruins, deveria alertar"
assert out["3.3.3.3"]["count"] == 5
print("✅ Passou em todos os asserts!")
if __name__ == "__main__":
run_asserts()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment