Voltar ao portfólio

SIEM com Splunk – SOC case

Construção de um mini-SOC em casa: ingestão de logs, dashboards, alertas e resposta automática (UFW).

PT EN

Resumo do projeto

Este projeto transforma o meu servidor doméstico num pequeno SOC: o Splunk recebe os logs do Linux (auth.log, syslog, UFW, Apache, MySQL), deteta tentativas de força bruta em SSH e responde automaticamente com bloqueio no ufw, registando tudo para auditoria.

O foco está em três pilares: visibilidade (dashboards), deteção (SPL + Threat Intel) e reação (scripts e alertas).

Splunk Enterprise 9.1.4 Ubuntu Server UFW Threat Intelligence Automação de resposta

Arquitetura resumida

  • Inputs: linux_secure (SSH), syslog, access_combined (Apache), mysql_error_log.
  • Dashboards:
    • Main Dashboard: panorama de ataques (SSH/HTTP), IPs banidos, Threat Intel.
    • Centro Avançado: saúde do Splunk, erros MySQL, top sourcetypes, auditoria.
  • Resposta automática: alerta de brute force → lookup → script ban_ip.sh → regra UFW + log no syslog.
  • Threat Intel: lookup threatintel_by_ip.csv cruza IPs externos com feeds maliciosos.

Deteção de força bruta SSH (SPL do alerta)

Pesquisa guardada que alimenta a “caixa-forte” de IPs a banir:

| search index=* sourcetype=linux_secure "sshd" (Failed OR failure OR "Invalid user")
| rex field=_raw "from (?<src_ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"
| where isnotnull(src_ip)
| streamstats time_window=1m count as consecutive_failures by src_ip
| where consecutive_failures >= 5
| fields src_ip
| dedup src_ip
| outputlookup ips_a_banir.csv

Esta pesquisa corre a cada minuto como um alerta agendado. Em vez de chamar o script diretamente com parâmetros frágeis, escreve os IPs num lookup (ips_a_banir.csv) que serve de caixa-forte.

BAN automático via UFW (ban_ip.sh)

Script chamado pelo alerta Run a script, que lê a caixa-forte, aplica whitelist, evita duplicados e regista o histórico:

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

DEBUG_LOG="/opt/splunk/var/log/splunk/ban_script.log"
LOOKUP_FILE="/opt/splunk/etc/apps/search/lookups/ips_a_banir.csv"
WHITELIST="/opt/splunk/etc/apps/search/lookups/ip_whitelist.csv"
HISTORY="/opt/splunk/etc/apps/search/lookups/banned_history.csv"
UFW="/usr/sbin/ufw"
LOGGER="/usr/bin/logger"

exec 2>>"$DEBUG_LOG"

echo "----------------------------------------------------" >> "$DEBUG_LOG"
echo "$(date): Início ban_ip.sh. Lendo: $LOOKUP_FILE" >> "$DEBUG_LOG"

mapfile -t WL < <(awk -F',' 'NR>1{print $1}' "$WHITELIST" | tr -d '\r"')

awk -F',' 'NR>1{print $1}' "$LOOKUP_FILE" | tr -d '\r"' | while read -r IP; do
  IP="$(echo "$IP" | xargs)"
  [[ -z "${IP:-}" ]] && continue
  [[ ! "$IP" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && {
    echo "$(date): Ignorado (IPv4 inválido): '$IP'" >> "$DEBUG_LOG"; continue; }

  for W in "${WL[@]:-}"; do
    if [[ "$IP" == "$W" ]]; then
      echo "$(date): Ignorado (whitelist): $IP" >> "$DEBUG_LOG"
      continue 2
    fi
  done

  if /usr/bin/sudo $UFW status numbered | grep -qE "DENY IN +$IP"; then
    echo "$(date): Já está banido: $IP" >> "$DEBUG_LOG"
    continue
  fi

  echo "$(date): A banir $IP via ufw..." >> "$DEBUG_LOG"
  /usr/bin/sudo $UFW insert 1 deny from "$IP" to any comment 'Splunk-Auto-Ban-SSH-Brute-Force' || {
    echo "$(date): Falha ao banir $IP" >> "$DEBUG_LOG"
    continue
  }

  $LOGGER -t BAN_SCRIPT "action=ip_banned ip=$IP reason=ssh_bruteforce"
  echo "$(date): $IP banido." >> "$DEBUG_LOG"

  echo "$(date +%F\ %T),$IP,ban,ban_script" >> "$HISTORY"
done

printf 'src_ip\n' > "$LOOKUP_FILE"
chown splunk:splunk "$LOOKUP_FILE" "$HISTORY"

echo "$(date): Fim ban_ip.sh. Lookup limpo." >> "$DEBUG_LOG"
echo "----------------------------------------------------" >> "$DEBUG_LOG"
exit 0

A whitelist (ip_whitelist.csv) protege IPs de administração; o banned_history.csv guarda um log simples para auditoria.

Configuração de sudoers (segura)

Permite ao utilizador splunk chamar apenas o que o script precisa, sem senha:

# /etc/sudoers.d/splunk_ufw
splunk ALL=(root) NOPASSWD:/usr/sbin/ufw insert 1 deny from * to any comment Splunk-Auto-Ban-SSH-Brute-Force
splunk ALL=(root) NOPASSWD:/usr/sbin/ufw status numbered

Score de risco + Threat Intel (SPL)

Pesquisa utilizada no painel “IPs Banidos – Geo + Reputação + Score”:

| multisearch
    [ search index=* sourcetype=ban_script
      | rex max_match=1 "(?<banned_ip>\d{1,3}(?:\.\d{1,3}){3})"
      | eval via="ban_script" ]
    [ search index=* sourcetype=syslog BAN_SCRIPT
      | rex max_match=1 "ip=(?<banned_ip>\d{1,3}(?:\.\d{1,3}){3})"
      | eval via="syslog" ]
| where isnotnull(banned_ip)
| stats latest(_time) as when values(via) as via by banned_ip
| iplocation banned_ip
| eval country=coalesce(Country,"Unknown"), city=coalesce(City,"")
| lookup threatintel_by_ip ip as banned_ip OUTPUTNEW ti_desc ti_feed threat_key
| eval rep_flag = if(isnotnull(ti_feed),1,0)
| eval geo_flag = if(country!="PT",1,0)
| eval base=30, rep=rep_flag*50, geo=geo_flag*10
| eval score=base+rep+geo
| convert ctime(when) as when timeformat="%Y-%m-%d %H:%M:%S"
| sort - score - when
| table when banned_ip country city ti_feed ti_desc threat_key via score

Com isto, consigo priorizar IPs banidos que também aparecem em feeds de reputação e que vêm de países inesperados.