Created
January 16, 2026 21:54
-
-
Save zr0n/d488b4ae9c15881c16670b3670b191ec to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| 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