🔐 Security hardening: auth, rate limiting, brute force protection
- Add comprehensive security package with: - API Key generation and validation (SHA256 hash) - Password policy enforcement (min 12 chars, complexity) - Rate limiting with presets (auth, api, ingest, export) - Brute force protection (5 attempts, 15min lockout) - Security headers middleware - IP whitelisting - Audit logging structure - Secure token generation - Enhanced auth middleware: - JWT + API Key dual authentication - Token revocation via Redis - Scope-based authorization - Role-based access control - Updated installer with: - Interactive setup for client customization - Auto-generated secure credentials - Docker all-in-one image - Agent installer script - Added documentation: - SECURITY.md - Complete security guide - INSTALL.md - Installation guide - .env.example - Configuration reference
This commit is contained in:
75
.env.example
Normal file
75
.env.example
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
# 🐍 OPHION - Configuração de Exemplo
|
||||||
|
# Copie para .env e preencha os valores
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Organização
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
ORG_NAME="Minha Empresa"
|
||||||
|
ADMIN_EMAIL=admin@empresa.com
|
||||||
|
ADMIN_PASSWORD=mude-esta-senha
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Rede
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
DOMAIN=localhost
|
||||||
|
SERVER_PORT=8080
|
||||||
|
DASHBOARD_PORT=3000
|
||||||
|
API_URL=http://ophion-server:8080
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Segurança (GERE VALORES ÚNICOS!)
|
||||||
|
# Use: openssl rand -hex 32
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
JWT_SECRET=MUDE-ISTO-openssl-rand-hex-32
|
||||||
|
API_KEY=ophion_MUDE-ISTO-openssl-rand-hex-32
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# PostgreSQL
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
POSTGRES_USER=ophion
|
||||||
|
POSTGRES_PASSWORD=MUDE-ISTO-senha-segura
|
||||||
|
POSTGRES_DB=ophion
|
||||||
|
DATABASE_URL=postgres://ophion:MUDE-ISTO-senha-segura@postgres:5432/ophion
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# ClickHouse
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
CLICKHOUSE_URL=clickhouse://clickhouse:9000/ophion
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Redis
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
REDIS_URL=redis://redis:6379
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Alertas - Telegram (opcional)
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
TELEGRAM_ENABLED=false
|
||||||
|
TELEGRAM_BOT_TOKEN=
|
||||||
|
TELEGRAM_CHAT_ID=
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Alertas - Slack (opcional)
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
SLACK_ENABLED=false
|
||||||
|
SLACK_WEBHOOK_URL=
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# IA - OpenAI (opcional, para recursos de IA)
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
OPENAI_API_KEY=
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Configurações Gerais
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
TZ=America/Sao_Paulo
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Retenção de Dados
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
METRICS_RETENTION_DAYS=90
|
||||||
|
LOGS_RETENTION_DAYS=30
|
||||||
|
TRACES_RETENTION_DAYS=14
|
||||||
119
deploy/docker/Dockerfile
Normal file
119
deploy/docker/Dockerfile
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
# 🐍 OPHION - Dockerfile All-in-One
|
||||||
|
# Imagem única com Server + Agent + Dashboard
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Stage 1: Build Go binaries
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
FROM golang:1.22-alpine AS go-builder
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Dependências
|
||||||
|
RUN apk add --no-cache git ca-certificates
|
||||||
|
|
||||||
|
# Go modules
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Código fonte
|
||||||
|
COPY cmd/ ./cmd/
|
||||||
|
COPY internal/ ./internal/
|
||||||
|
|
||||||
|
# Build server
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||||
|
go build -ldflags="-s -w" -o ophion-server ./cmd/server
|
||||||
|
|
||||||
|
# Build agent
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||||
|
go build -ldflags="-s -w" -o ophion-agent ./cmd/agent
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Stage 2: Build Dashboard (Next.js)
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
FROM node:20-alpine AS web-builder
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Dependências
|
||||||
|
COPY dashboard/package*.json ./
|
||||||
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
# Código fonte
|
||||||
|
COPY dashboard/ ./
|
||||||
|
|
||||||
|
# Build
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Stage 3: Runtime Image
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
FROM alpine:3.19
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.title="OPHION"
|
||||||
|
LABEL org.opencontainers.image.description="Open Source Observability Platform"
|
||||||
|
LABEL org.opencontainers.image.source="https://github.com/bigtux/ophion"
|
||||||
|
LABEL org.opencontainers.image.vendor="OPHION"
|
||||||
|
LABEL org.opencontainers.image.licenses="AGPL-3.0"
|
||||||
|
|
||||||
|
# Dependências runtime
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
ca-certificates \
|
||||||
|
tzdata \
|
||||||
|
nodejs \
|
||||||
|
npm \
|
||||||
|
supervisor \
|
||||||
|
curl \
|
||||||
|
bash
|
||||||
|
|
||||||
|
# Criar usuário não-root
|
||||||
|
RUN addgroup -g 1000 ophion && \
|
||||||
|
adduser -u 1000 -G ophion -s /bin/sh -D ophion
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copiar binários Go
|
||||||
|
COPY --from=go-builder /build/ophion-server /app/bin/
|
||||||
|
COPY --from=go-builder /build/ophion-agent /app/bin/
|
||||||
|
|
||||||
|
# Copiar Dashboard
|
||||||
|
COPY --from=web-builder /build/.next /app/web/.next
|
||||||
|
COPY --from=web-builder /build/public /app/web/public
|
||||||
|
COPY --from=web-builder /build/package*.json /app/web/
|
||||||
|
COPY --from=web-builder /build/node_modules /app/web/node_modules
|
||||||
|
|
||||||
|
# Configs
|
||||||
|
COPY configs/ /app/configs/
|
||||||
|
COPY web/ /app/static/
|
||||||
|
|
||||||
|
# Supervisor config
|
||||||
|
RUN mkdir -p /etc/supervisor.d
|
||||||
|
COPY deploy/docker/supervisord.conf /etc/supervisor.d/ophion.ini
|
||||||
|
|
||||||
|
# Diretórios
|
||||||
|
RUN mkdir -p /app/data /app/logs && \
|
||||||
|
chown -R ophion:ophion /app
|
||||||
|
|
||||||
|
# Script de entrada
|
||||||
|
COPY deploy/docker/entrypoint.sh /app/
|
||||||
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
|
# Portas
|
||||||
|
EXPOSE 8080 3000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:8080/health || exit 1
|
||||||
|
|
||||||
|
# Variáveis de ambiente padrão
|
||||||
|
ENV TZ=America/Sao_Paulo \
|
||||||
|
LOG_LEVEL=info \
|
||||||
|
SERVER_PORT=8080 \
|
||||||
|
DASHBOARD_PORT=3000
|
||||||
|
|
||||||
|
USER ophion
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
|
CMD ["all"]
|
||||||
35
deploy/docker/entrypoint.sh
Normal file
35
deploy/docker/entrypoint.sh
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
# 🐍 OPHION - Entrypoint
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
MODE=${1:-all}
|
||||||
|
|
||||||
|
echo "🐍 OPHION starting in mode: $MODE"
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
server)
|
||||||
|
echo "Starting API server on port ${SERVER_PORT:-8080}..."
|
||||||
|
exec /app/bin/ophion-server
|
||||||
|
;;
|
||||||
|
agent)
|
||||||
|
echo "Starting agent..."
|
||||||
|
exec /app/bin/ophion-agent -config /app/configs/agent.yaml
|
||||||
|
;;
|
||||||
|
web)
|
||||||
|
echo "Starting dashboard on port ${DASHBOARD_PORT:-3000}..."
|
||||||
|
cd /app/web
|
||||||
|
exec npm start
|
||||||
|
;;
|
||||||
|
all)
|
||||||
|
echo "Starting all services with supervisor..."
|
||||||
|
exec supervisord -c /etc/supervisord.conf
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown mode: $MODE"
|
||||||
|
echo "Usage: entrypoint.sh [server|agent|web|all]"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
26
deploy/docker/supervisord.conf
Normal file
26
deploy/docker/supervisord.conf
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
logfile=/app/logs/supervisord.log
|
||||||
|
pidfile=/tmp/supervisord.pid
|
||||||
|
user=ophion
|
||||||
|
|
||||||
|
[program:ophion-server]
|
||||||
|
command=/app/bin/ophion-server
|
||||||
|
directory=/app
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/app/logs/server.log
|
||||||
|
stderr_logfile=/app/logs/server.error.log
|
||||||
|
environment=PORT="%(ENV_SERVER_PORT)s"
|
||||||
|
|
||||||
|
[program:ophion-web]
|
||||||
|
command=npm start
|
||||||
|
directory=/app/web
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/app/logs/web.log
|
||||||
|
stderr_logfile=/app/logs/web.error.log
|
||||||
|
environment=PORT="%(ENV_DASHBOARD_PORT)s"
|
||||||
|
|
||||||
|
[group:ophion]
|
||||||
|
programs=ophion-server,ophion-web
|
||||||
485
docs/INSTALL.md
485
docs/INSTALL.md
@@ -1,436 +1,231 @@
|
|||||||
# 🐍 OPHION - Manual de Instalação e Configuração
|
# 🐍 OPHION - Guia de Instalação
|
||||||
|
|
||||||
## Índice
|
## Instalação Rápida (1 comando)
|
||||||
1. [Instalação do Servidor](#instalação-do-servidor)
|
|
||||||
2. [Instalação do Agent](#instalação-do-agent)
|
```bash
|
||||||
3. [Monitoramento de Docker](#monitoramento-de-docker)
|
curl -fsSL https://get.ophion.io | bash
|
||||||
4. [Monitoramento de Aplicações](#monitoramento-de-aplicações)
|
```
|
||||||
5. [Configuração de Alertas](#configuração-de-alertas)
|
|
||||||
|
O instalador vai:
|
||||||
|
1. ✅ Verificar requisitos (Docker, Docker Compose)
|
||||||
|
2. 📋 Coletar informações da sua empresa
|
||||||
|
3. 🔐 Gerar credenciais seguras
|
||||||
|
4. 📦 Configurar todos os serviços
|
||||||
|
5. 🚀 Iniciar a plataforma
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Instalação do Servidor
|
## Instalação Manual (Passo a Passo)
|
||||||
|
|
||||||
### Requisitos Mínimos
|
### 1. Requisitos
|
||||||
- CPU: 2 cores
|
|
||||||
- RAM: 4GB (8GB recomendado)
|
|
||||||
- Disco: 50GB SSD
|
|
||||||
- OS: Ubuntu 22.04+ / Debian 12+
|
|
||||||
- Docker 24+
|
|
||||||
|
|
||||||
### Instalação Rápida (1 comando)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://ophion.com.br/install.sh | bash
|
# Ubuntu/Debian
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y curl git
|
||||||
|
|
||||||
|
# Instalar Docker
|
||||||
|
curl -fsSL https://get.docker.com | sh
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
|
||||||
|
# Logout e login para aplicar grupo docker
|
||||||
```
|
```
|
||||||
|
|
||||||
### Instalação Manual
|
### 2. Baixar Ophion
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clonar repositório
|
|
||||||
git clone https://github.com/bigtux/ophion.git
|
git clone https://github.com/bigtux/ophion.git
|
||||||
cd ophion/deploy/docker
|
cd ophion
|
||||||
|
|
||||||
# Configurar variáveis de ambiente
|
|
||||||
cp .env.example .env
|
|
||||||
nano .env # Editar conforme necessário
|
|
||||||
|
|
||||||
# Iniciar serviços
|
|
||||||
docker compose up -d
|
|
||||||
|
|
||||||
# Verificar status
|
|
||||||
docker compose ps
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Variáveis de Ambiente (.env)
|
### 3. Configurar
|
||||||
|
|
||||||
|
Copie e edite o arquivo de configuração:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Configurações importantes:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
# Segurança (OBRIGATÓRIO - gere valores únicos!)
|
# Sua empresa
|
||||||
JWT_SECRET=sua-chave-secreta-aqui-min-32-chars
|
ORG_NAME="Minha Empresa"
|
||||||
|
ADMIN_EMAIL=admin@empresa.com
|
||||||
|
ADMIN_PASSWORD=senha-segura-aqui
|
||||||
|
|
||||||
# Banco de Dados
|
# Domínio (ou localhost para testes)
|
||||||
POSTGRES_USER=ophion
|
DOMAIN=ophion.empresa.com
|
||||||
POSTGRES_PASSWORD=senha-forte-aqui
|
|
||||||
POSTGRES_DB=ophion
|
|
||||||
|
|
||||||
# ClickHouse (métricas/logs)
|
# Portas
|
||||||
CLICKHOUSE_USER=default
|
SERVER_PORT=8080
|
||||||
CLICKHOUSE_PASSWORD=senha-clickhouse
|
DASHBOARD_PORT=3000
|
||||||
|
|
||||||
# Redis
|
# Segurança (gere valores únicos!)
|
||||||
REDIS_PASSWORD=senha-redis
|
JWT_SECRET=seu-jwt-secret-aqui
|
||||||
|
|
||||||
# Configurações do Servidor
|
|
||||||
OPHION_PORT=8080
|
|
||||||
OPHION_HOST=0.0.0.0
|
|
||||||
|
|
||||||
# Retenção de dados (dias)
|
|
||||||
METRICS_RETENTION_DAYS=30
|
|
||||||
LOGS_RETENTION_DAYS=14
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Verificar Instalação
|
### 4. Iniciar
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Health check
|
docker compose up -d
|
||||||
curl http://localhost:8080/health
|
|
||||||
|
|
||||||
# Resposta esperada:
|
|
||||||
# {"status":"healthy","service":"ophion","version":"0.1.0"}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Acessar Dashboard
|
### 5. Acessar
|
||||||
|
|
||||||
Abra no navegador: `http://seu-servidor:3000`
|
- **Dashboard:** http://localhost:3000
|
||||||
|
- **API:** http://localhost:8080
|
||||||
1. Crie sua conta de administrador
|
|
||||||
2. Configure sua organização
|
|
||||||
3. Gere API Keys para os agents
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Instalação do Agent
|
## Instalação do Agent (Servidores Monitorados)
|
||||||
|
|
||||||
O Agent coleta métricas do servidor e envia para o OPHION.
|
Em cada servidor que você quer monitorar:
|
||||||
|
|
||||||
### Instalação Rápida
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Substitua YOUR_API_KEY pela chave gerada no dashboard
|
curl -fsSL http://SEU-SERVIDOR-OPHION:8080/install-agent.sh | sudo bash
|
||||||
curl -fsSL https://ophion.com.br/agent.sh | OPHION_API_KEY=YOUR_API_KEY bash
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Instalação Manual
|
Ou manualmente:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Baixar binário
|
# Baixar
|
||||||
wget https://github.com/bigtux/ophion/releases/latest/download/ophion-agent-linux-amd64
|
curl -o /usr/local/bin/ophion-agent \
|
||||||
chmod +x ophion-agent-linux-amd64
|
http://SEU-SERVIDOR-OPHION:8080/downloads/agent/linux/amd64/ophion-agent
|
||||||
sudo mv ophion-agent-linux-amd64 /usr/local/bin/ophion-agent
|
chmod +x /usr/local/bin/ophion-agent
|
||||||
|
|
||||||
# Criar arquivo de configuração
|
# Configurar
|
||||||
sudo mkdir -p /etc/ophion
|
mkdir -p /etc/ophion
|
||||||
sudo tee /etc/ophion/agent.yaml << EOF
|
cat > /etc/ophion/agent.yaml << EOF
|
||||||
server: https://api.ophion.com.br
|
server:
|
||||||
api_key: YOUR_API_KEY
|
url: http://SEU-SERVIDOR-OPHION:8080
|
||||||
hostname: $(hostname)
|
api_key: SUA-API-KEY
|
||||||
interval: 30s
|
|
||||||
|
|
||||||
collectors:
|
collection:
|
||||||
cpu: true
|
interval: 30s
|
||||||
memory: true
|
|
||||||
disk: true
|
metrics:
|
||||||
network: true
|
enabled: true
|
||||||
processes: true
|
|
||||||
|
logs:
|
||||||
|
enabled: true
|
||||||
|
paths:
|
||||||
|
- /var/log/syslog
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Criar serviço systemd
|
# Criar serviço
|
||||||
sudo tee /etc/systemd/system/ophion-agent.service << EOF
|
cat > /etc/systemd/system/ophion-agent.service << EOF
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=OPHION Monitoring Agent
|
Description=OPHION Agent
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
ExecStart=/usr/local/bin/ophion-agent -config /etc/ophion/agent.yaml
|
||||||
ExecStart=/usr/local/bin/ophion-agent --config /etc/ophion/agent.yaml
|
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=10
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Iniciar serviço
|
# Iniciar
|
||||||
sudo systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
sudo systemctl enable ophion-agent
|
systemctl enable --now ophion-agent
|
||||||
sudo systemctl start ophion-agent
|
|
||||||
|
|
||||||
# Verificar status
|
|
||||||
sudo systemctl status ophion-agent
|
|
||||||
```
|
|
||||||
|
|
||||||
### Variáveis de Ambiente do Agent
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export OPHION_SERVER="https://api.ophion.com.br"
|
|
||||||
export OPHION_API_KEY="ophion_xxxxxxxxxxxxxxxx"
|
|
||||||
export OPHION_HOSTNAME="meu-servidor"
|
|
||||||
export OPHION_INTERVAL="30s"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Monitoramento de Docker
|
## Comandos Úteis
|
||||||
|
|
||||||
### Opção A: Agent com Acesso ao Docker Socket
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Adicionar ao agent.yaml
|
# Status
|
||||||
collectors:
|
ophion status
|
||||||
docker:
|
|
||||||
enabled: true
|
|
||||||
socket: /var/run/docker.sock
|
|
||||||
collect_container_stats: true
|
|
||||||
collect_container_logs: true
|
|
||||||
```
|
|
||||||
|
|
||||||
### Opção B: Container Dedicado
|
# Logs
|
||||||
|
ophion logs # Logs do server
|
||||||
|
ophion logs ophion-web # Logs do dashboard
|
||||||
|
|
||||||
```yaml
|
# Gerenciamento
|
||||||
# docker-compose.yml do seu projeto
|
ophion start
|
||||||
services:
|
ophion stop
|
||||||
ophion-agent:
|
ophion restart
|
||||||
image: ophion/agent:latest
|
ophion update
|
||||||
container_name: ophion-agent
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
- OPHION_SERVER=https://api.ophion.com.br
|
|
||||||
- OPHION_API_KEY=YOUR_API_KEY
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
||||||
- /proc:/host/proc:ro
|
|
||||||
- /sys:/host/sys:ro
|
|
||||||
network_mode: host
|
|
||||||
pid: host
|
|
||||||
```
|
|
||||||
|
|
||||||
### Métricas Coletadas do Docker
|
# Backup
|
||||||
|
ophion backup
|
||||||
|
|
||||||
| Métrica | Descrição |
|
# Gerar nova API Key
|
||||||
|---------|-----------|
|
ophion api-key
|
||||||
| `container.cpu.usage` | Uso de CPU por container |
|
|
||||||
| `container.memory.usage` | Uso de memória |
|
|
||||||
| `container.memory.limit` | Limite de memória |
|
|
||||||
| `container.network.rx_bytes` | Bytes recebidos |
|
|
||||||
| `container.network.tx_bytes` | Bytes enviados |
|
|
||||||
| `container.disk.read_bytes` | Leitura de disco |
|
|
||||||
| `container.disk.write_bytes` | Escrita de disco |
|
|
||||||
| `container.status` | Status (running/stopped) |
|
|
||||||
| `container.restarts` | Contagem de restarts |
|
|
||||||
|
|
||||||
### Labels para Identificação
|
|
||||||
|
|
||||||
Adicione labels aos seus containers para melhor organização:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
minha-app:
|
|
||||||
labels:
|
|
||||||
ophion.monitor: "true"
|
|
||||||
ophion.service: "api"
|
|
||||||
ophion.environment: "production"
|
|
||||||
ophion.team: "backend"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Monitoramento de Aplicações (APM)
|
## Estrutura de Diretórios
|
||||||
|
|
||||||
### Node.js
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install @ophion/apm
|
|
||||||
```
|
```
|
||||||
|
/opt/ophion/
|
||||||
```javascript
|
├── docker-compose.yml # Configuração dos containers
|
||||||
// No início do seu app (antes de outros imports)
|
├── .env # Variáveis de ambiente (SECRETO!)
|
||||||
const ophion = require('@ophion/apm');
|
├── data/
|
||||||
|
│ ├── postgres/ # Dados do PostgreSQL
|
||||||
ophion.init({
|
│ ├── clickhouse/ # Métricas e logs
|
||||||
serverUrl: 'https://api.ophion.com.br',
|
│ └── redis/ # Cache
|
||||||
apiKey: 'YOUR_API_KEY',
|
├── configs/ # Configurações customizadas
|
||||||
serviceName: 'minha-api',
|
├── logs/ # Logs da aplicação
|
||||||
environment: 'production'
|
├── scripts/
|
||||||
});
|
│ └── install-agent.sh # Instalador do agent
|
||||||
|
└── backups/ # Backups automáticos
|
||||||
// Seu código normal...
|
|
||||||
const express = require('express');
|
|
||||||
const app = express();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Python
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install ophion-apm
|
|
||||||
```
|
|
||||||
|
|
||||||
```python
|
|
||||||
# No início do seu app
|
|
||||||
import ophion_apm
|
|
||||||
|
|
||||||
ophion_apm.init(
|
|
||||||
server_url='https://api.ophion.com.br',
|
|
||||||
api_key='YOUR_API_KEY',
|
|
||||||
service_name='minha-api',
|
|
||||||
environment='production'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Seu código normal...
|
|
||||||
from flask import Flask
|
|
||||||
app = Flask(__name__)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Go
|
|
||||||
|
|
||||||
```go
|
|
||||||
import "github.com/bigtux/ophion/sdk/go/apm"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Inicializar APM
|
|
||||||
apm.Init(apm.Config{
|
|
||||||
ServerURL: "https://api.ophion.com.br",
|
|
||||||
APIKey: "YOUR_API_KEY",
|
|
||||||
ServiceName: "minha-api",
|
|
||||||
Environment: "production",
|
|
||||||
})
|
|
||||||
defer apm.Close()
|
|
||||||
|
|
||||||
// Seu código normal...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Java (Spring Boot)
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<!-- pom.xml -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.ophion</groupId>
|
|
||||||
<artifactId>ophion-apm</artifactId>
|
|
||||||
<version>1.0.0</version>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# application.yml
|
|
||||||
ophion:
|
|
||||||
apm:
|
|
||||||
server-url: https://api.ophion.com.br
|
|
||||||
api-key: YOUR_API_KEY
|
|
||||||
service-name: minha-api
|
|
||||||
environment: production
|
|
||||||
```
|
|
||||||
|
|
||||||
### OpenTelemetry (Universal)
|
|
||||||
|
|
||||||
OPHION é compatível com OpenTelemetry. Use qualquer SDK OTel:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Variáveis de ambiente
|
|
||||||
export OTEL_EXPORTER_OTLP_ENDPOINT="https://api.ophion.com.br/v1/traces"
|
|
||||||
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer YOUR_API_KEY"
|
|
||||||
export OTEL_SERVICE_NAME="minha-api"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Configuração de Alertas
|
## Portas
|
||||||
|
|
||||||
### Via Dashboard
|
| Serviço | Porta | Descrição |
|
||||||
|
|---------|-------|-----------|
|
||||||
1. Acesse **Alertas** → **Novo Alerta**
|
| Dashboard | 3000 | Interface web |
|
||||||
2. Defina a condição:
|
| API | 8080 | REST API |
|
||||||
- Métrica: `cpu.usage`
|
| PostgreSQL | 5432 | Banco de dados (interno) |
|
||||||
- Operador: `>`
|
| ClickHouse | 9000 | Métricas/Logs (interno) |
|
||||||
- Valor: `80`
|
| Redis | 6379 | Cache (interno) |
|
||||||
- Duração: `5 minutos`
|
|
||||||
3. Configure notificações:
|
|
||||||
- Telegram
|
|
||||||
- Slack
|
|
||||||
- Email
|
|
||||||
- Webhook
|
|
||||||
|
|
||||||
### Via API
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST "https://api.ophion.com.br/api/v1/alerts" \
|
|
||||||
-H "Authorization: Bearer YOUR_API_KEY" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"name": "CPU Alta",
|
|
||||||
"description": "Alerta quando CPU > 80%",
|
|
||||||
"condition": {
|
|
||||||
"metric": "cpu.usage",
|
|
||||||
"operator": ">",
|
|
||||||
"threshold": 80,
|
|
||||||
"duration": "5m"
|
|
||||||
},
|
|
||||||
"notifications": [
|
|
||||||
{
|
|
||||||
"type": "telegram",
|
|
||||||
"chat_id": "123456789"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "email",
|
|
||||||
"to": "admin@empresa.com"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"severity": "warning"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Integrações Disponíveis
|
|
||||||
|
|
||||||
| Canal | Configuração |
|
|
||||||
|-------|-------------|
|
|
||||||
| **Telegram** | Bot token + Chat ID |
|
|
||||||
| **Slack** | Webhook URL |
|
|
||||||
| **Discord** | Webhook URL |
|
|
||||||
| **Email** | SMTP ou API (SendGrid, Resend) |
|
|
||||||
| **PagerDuty** | Integration Key |
|
|
||||||
| **Webhook** | URL customizada |
|
|
||||||
|
|
||||||
### Configurar Telegram
|
|
||||||
|
|
||||||
1. Crie um bot com [@BotFather](https://t.me/BotFather)
|
|
||||||
2. Obtenha o token do bot
|
|
||||||
3. Inicie conversa com o bot
|
|
||||||
4. No dashboard OPHION: **Configurações** → **Integrações** → **Telegram**
|
|
||||||
5. Cole o token e configure
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Containers não iniciam
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ver logs
|
||||||
|
docker compose logs
|
||||||
|
|
||||||
|
# Verificar recursos
|
||||||
|
docker system df
|
||||||
|
df -h
|
||||||
|
```
|
||||||
|
|
||||||
### Agent não conecta
|
### Agent não conecta
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Verificar conectividade
|
# Testar conectividade
|
||||||
curl -v https://api.ophion.com.br/health
|
curl http://SEU-SERVIDOR:8080/health
|
||||||
|
|
||||||
# Verificar logs do agent
|
# Ver logs do agent
|
||||||
journalctl -u ophion-agent -f
|
journalctl -u ophion-agent -f
|
||||||
|
|
||||||
# Testar API key
|
|
||||||
curl -H "Authorization: Bearer YOUR_API_KEY" \
|
|
||||||
https://api.ophion.com.br/api/v1/status
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Métricas não aparecem
|
### Resetar senha admin
|
||||||
|
|
||||||
1. Verifique se o agent está rodando: `systemctl status ophion-agent`
|
|
||||||
2. Verifique a API key no dashboard
|
|
||||||
3. Confira o hostname no dashboard
|
|
||||||
4. Aguarde até 60 segundos para primeira coleta
|
|
||||||
|
|
||||||
### Docker metrics não coletam
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Verificar permissões do socket
|
docker compose exec postgres psql -U ophion -c \
|
||||||
ls -la /var/run/docker.sock
|
"UPDATE users SET password_hash = crypt('nova-senha', gen_salt('bf')) WHERE email = 'admin@email.com';"
|
||||||
|
|
||||||
# Agent precisa estar no grupo docker
|
|
||||||
sudo usermod -aG docker ophion-agent
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Suporte
|
## Suporte
|
||||||
|
|
||||||
- 📧 Email: suporte@ophion.com.br
|
- 📖 Docs: https://docs.ophion.io
|
||||||
- 💬 Telegram: [@ophion_suporte](https://t.me/ophion_suporte)
|
- 💬 Discord: https://discord.gg/ophion
|
||||||
- 📖 Docs: https://docs.ophion.com.br
|
- 🐛 Issues: https://github.com/bigtux/ophion/issues
|
||||||
- 🐙 GitHub: https://github.com/bigtux/ophion
|
- 📧 Email: support@ophion.io
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Made with 🖤 in Brazil*
|
|
||||||
|
|||||||
270
docs/SECURITY.md
Normal file
270
docs/SECURITY.md
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
# 🔐 OPHION Security Guide
|
||||||
|
|
||||||
|
## Visão Geral
|
||||||
|
|
||||||
|
OPHION foi projetado com segurança em camadas (defense in depth):
|
||||||
|
|
||||||
|
1. **Autenticação** - JWT + API Keys
|
||||||
|
2. **Autorização** - RBAC + Scopes
|
||||||
|
3. **Rate Limiting** - Proteção contra DDoS/Brute Force
|
||||||
|
4. **Criptografia** - TLS + Bcrypt + SHA256
|
||||||
|
5. **Auditoria** - Logs de todas as ações
|
||||||
|
6. **Isolamento** - Multi-tenant por organização
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 Autenticação
|
||||||
|
|
||||||
|
### JWT Tokens
|
||||||
|
|
||||||
|
- **Access Token**: Curta duração (15-60 min)
|
||||||
|
- **Refresh Token**: Longa duração (7-30 dias)
|
||||||
|
- **Algoritmo**: HS256 (HMAC-SHA256)
|
||||||
|
- **Revogação**: Via Redis blacklist
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Header de autenticação
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Keys
|
||||||
|
|
||||||
|
- **Formato**: `ophion_` + 64 caracteres hex
|
||||||
|
- **Storage**: Hash SHA256 (nunca plaintext)
|
||||||
|
- **Scopes**: Permissões granulares
|
||||||
|
- **Rotação**: Recomendado a cada 90 dias
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Exemplo de API Key
|
||||||
|
ophion_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6...
|
||||||
|
|
||||||
|
# Header de autenticação
|
||||||
|
Authorization: Bearer ophion_xxx...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scopes de API Key
|
||||||
|
|
||||||
|
| Scope | Descrição |
|
||||||
|
|-------|-----------|
|
||||||
|
| `metrics:read` | Ler métricas |
|
||||||
|
| `metrics:write` | Enviar métricas |
|
||||||
|
| `logs:read` | Ler logs |
|
||||||
|
| `logs:write` | Enviar logs |
|
||||||
|
| `traces:read` | Ler traces |
|
||||||
|
| `traces:write` | Enviar traces |
|
||||||
|
| `alerts:read` | Ler alertas |
|
||||||
|
| `alerts:write` | Criar/editar alertas |
|
||||||
|
| `admin` | Acesso administrativo |
|
||||||
|
| `*` | Acesso total |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛡️ Proteções
|
||||||
|
|
||||||
|
### Rate Limiting
|
||||||
|
|
||||||
|
| Endpoint | Limite | Janela |
|
||||||
|
|----------|--------|--------|
|
||||||
|
| `/api/v1/auth/*` | 5 req | 1 min |
|
||||||
|
| `/api/v1/*` (API) | 100 req | 1 min |
|
||||||
|
| `/api/v1/ingest/*` | 1000 req | 1 min |
|
||||||
|
| `/api/v1/export/*` | 10 req | 1 hora |
|
||||||
|
|
||||||
|
### Brute Force Protection
|
||||||
|
|
||||||
|
- **Máximo de tentativas**: 5 falhas
|
||||||
|
- **Bloqueio**: 15 minutos
|
||||||
|
- **Tracking**: Por IP + Email
|
||||||
|
|
||||||
|
### Security Headers
|
||||||
|
|
||||||
|
```http
|
||||||
|
X-Frame-Options: DENY
|
||||||
|
X-Content-Type-Options: nosniff
|
||||||
|
X-XSS-Protection: 1; mode=block
|
||||||
|
Strict-Transport-Security: max-age=31536000; includeSubDomains
|
||||||
|
Content-Security-Policy: default-src 'self'
|
||||||
|
Referrer-Policy: strict-origin-when-cross-origin
|
||||||
|
Permissions-Policy: geolocation=(), microphone=(), camera=()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Senhas
|
||||||
|
|
||||||
|
### Política de Senha
|
||||||
|
|
||||||
|
- Mínimo 12 caracteres
|
||||||
|
- Letra maiúscula obrigatória
|
||||||
|
- Letra minúscula obrigatória
|
||||||
|
- Número obrigatório
|
||||||
|
- Caractere especial obrigatório
|
||||||
|
- Bloqueio de senhas comuns
|
||||||
|
|
||||||
|
### Armazenamento
|
||||||
|
|
||||||
|
- **Algoritmo**: Bcrypt
|
||||||
|
- **Cost Factor**: 12
|
||||||
|
- **Salt**: Único por senha (auto-gerado)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Criptografia
|
||||||
|
|
||||||
|
### Em Trânsito
|
||||||
|
|
||||||
|
- TLS 1.2+ obrigatório em produção
|
||||||
|
- Certificados Let's Encrypt (automático)
|
||||||
|
- HSTS habilitado
|
||||||
|
|
||||||
|
### Em Repouso
|
||||||
|
|
||||||
|
- Senhas: Bcrypt hash
|
||||||
|
- API Keys: SHA256 hash
|
||||||
|
- Tokens: Opcionalmente criptografados
|
||||||
|
- Dados sensíveis: AES-256-GCM (configurável)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Auditoria
|
||||||
|
|
||||||
|
### Eventos Registrados
|
||||||
|
|
||||||
|
- `auth.login` - Login bem-sucedido
|
||||||
|
- `auth.login_failed` - Falha de login
|
||||||
|
- `auth.logout` - Logout
|
||||||
|
- `apikey.created` - API key criada
|
||||||
|
- `apikey.revoked` - API key revogada
|
||||||
|
- `user.created` - Usuário criado
|
||||||
|
- `user.deleted` - Usuário deletado
|
||||||
|
- `config.changed` - Configuração alterada
|
||||||
|
- `alert.created` - Alerta criado
|
||||||
|
- `data.export` - Exportação de dados
|
||||||
|
|
||||||
|
### Campos de Auditoria
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"timestamp": "2024-01-15T10:30:00Z",
|
||||||
|
"event_type": "auth.login",
|
||||||
|
"user_id": "uuid",
|
||||||
|
"org_id": "uuid",
|
||||||
|
"ip": "192.168.1.1",
|
||||||
|
"user_agent": "Mozilla/5.0...",
|
||||||
|
"resource": "/api/v1/auth/login",
|
||||||
|
"action": "POST",
|
||||||
|
"status": "success",
|
||||||
|
"details": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏢 Multi-Tenancy
|
||||||
|
|
||||||
|
### Isolamento de Dados
|
||||||
|
|
||||||
|
- Cada organização tem `org_id` único
|
||||||
|
- Queries sempre filtradas por `org_id`
|
||||||
|
- API Keys vinculadas à organização
|
||||||
|
- Usuários pertencem a uma organização
|
||||||
|
|
||||||
|
### RBAC (Role-Based Access Control)
|
||||||
|
|
||||||
|
| Role | Permissões |
|
||||||
|
|------|------------|
|
||||||
|
| `viewer` | Somente leitura |
|
||||||
|
| `operator` | Leitura + Ack de alertas |
|
||||||
|
| `editor` | Leitura + Escrita |
|
||||||
|
| `admin` | Acesso total à organização |
|
||||||
|
| `super_admin` | Acesso total ao sistema |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Configurações de Segurança
|
||||||
|
|
||||||
|
### Variáveis de Ambiente
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Obrigatórias
|
||||||
|
JWT_SECRET=<random-64-chars> # openssl rand -hex 32
|
||||||
|
ADMIN_PASSWORD=<strong-password>
|
||||||
|
|
||||||
|
# Recomendadas
|
||||||
|
HTTPS_ONLY=true # Forçar HTTPS
|
||||||
|
SESSION_TIMEOUT=3600 # 1 hora
|
||||||
|
REFRESH_TOKEN_DAYS=7 # 7 dias
|
||||||
|
PASSWORD_MIN_LENGTH=12 # Mínimo 12 chars
|
||||||
|
RATE_LIMIT_ENABLED=true
|
||||||
|
AUDIT_LOG_ENABLED=true
|
||||||
|
|
||||||
|
# IP Whitelist (opcional)
|
||||||
|
ADMIN_IP_WHITELIST=10.0.0.0/8,192.168.0.0/16
|
||||||
|
API_IP_WHITELIST= # Vazio = todos permitidos
|
||||||
|
```
|
||||||
|
|
||||||
|
### Checklist de Produção
|
||||||
|
|
||||||
|
- [ ] JWT_SECRET único e forte (64+ chars)
|
||||||
|
- [ ] HTTPS habilitado
|
||||||
|
- [ ] Senhas fortes para todos os usuários
|
||||||
|
- [ ] Rate limiting habilitado
|
||||||
|
- [ ] Logs de auditoria habilitados
|
||||||
|
- [ ] Backup configurado
|
||||||
|
- [ ] Firewall configurado
|
||||||
|
- [ ] Atualizações automáticas
|
||||||
|
- [ ] Monitoramento de segurança
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 Resposta a Incidentes
|
||||||
|
|
||||||
|
### Se suspeitar de comprometimento:
|
||||||
|
|
||||||
|
1. **Revogar todas as API Keys**
|
||||||
|
```bash
|
||||||
|
ophion security revoke-all-keys
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Invalidar todas as sessões**
|
||||||
|
```bash
|
||||||
|
ophion security invalidate-sessions
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Rotacionar JWT Secret**
|
||||||
|
```bash
|
||||||
|
# Atualizar .env com novo JWT_SECRET
|
||||||
|
ophion restart
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Revisar logs de auditoria**
|
||||||
|
```bash
|
||||||
|
ophion logs --type audit --since 24h
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Resetar senhas comprometidas**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Reportar Vulnerabilidades
|
||||||
|
|
||||||
|
Se você encontrar uma vulnerabilidade de segurança:
|
||||||
|
|
||||||
|
1. **NÃO** abra uma issue pública
|
||||||
|
2. Envie email para: security@ophion.io
|
||||||
|
3. Inclua detalhes e passos para reproduzir
|
||||||
|
4. Aguarde confirmação antes de divulgar
|
||||||
|
|
||||||
|
**Programa de Bug Bounty**: Em breve!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Atualizações de Segurança
|
||||||
|
|
||||||
|
Assine nossa lista de segurança:
|
||||||
|
- Email: security-announce@ophion.io
|
||||||
|
- GitHub Security Advisories
|
||||||
|
|
||||||
|
Versões com suporte de segurança:
|
||||||
|
- Última versão: Suporte completo
|
||||||
|
- Versão anterior: Patches críticos por 6 meses
|
||||||
4
go.mod
4
go.mod
@@ -4,6 +4,8 @@ go 1.22
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gofiber/fiber/v2 v2.52.0
|
github.com/gofiber/fiber/v2 v2.52.0
|
||||||
github.com/shirou/gopsutil/v3 v3.24.1
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||||
|
github.com/redis/go-redis/v9 v9.4.0
|
||||||
|
github.com/shirou/gopsutil/v3 v3.24.1
|
||||||
|
golang.org/x/crypto v0.18.0
|
||||||
)
|
)
|
||||||
|
|||||||
867
install.sh
867
install.sh
@@ -1,49 +1,844 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# 🐍 OPHION - Instalador Interativo
|
||||||
|
# Plataforma de Observabilidade Open Source
|
||||||
|
#
|
||||||
|
# Uso: curl -fsSL https://get.ophion.io | bash
|
||||||
|
#
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
echo "🐍 OPHION - Observability Platform Installer"
|
# Cores
|
||||||
echo "============================================="
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
PURPLE='\033[0;35m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
# Check Docker
|
# ASCII Art
|
||||||
if ! command -v docker &> /dev/null; then
|
show_banner() {
|
||||||
echo "❌ Docker not found. Installing..."
|
echo -e "${PURPLE}"
|
||||||
curl -fsSL https://get.docker.com | sh
|
cat << "EOF"
|
||||||
fi
|
____ _____ _ _ _____ ____ _ _
|
||||||
|
/ __ \| __ \| | | |_ _/ __ \| \ | |
|
||||||
|
| | | | |__) | |__| | | || | | | \| |
|
||||||
|
| | | | ___/| __ | | || | | | . ` |
|
||||||
|
| |__| | | | | | |_| || |__| | |\ |
|
||||||
|
\____/|_| |_| |_|_____\____/|_| \_|
|
||||||
|
|
||||||
# Check Docker Compose
|
Open Source Observability Platform
|
||||||
if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
|
Made with 🖤 in Brazil
|
||||||
echo "❌ Docker Compose not found. Please install it."
|
EOF
|
||||||
|
echo -e "${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||||
|
log_success() { echo -e "${GREEN}[✓]${NC} $1"; }
|
||||||
|
log_warn() { echo -e "${YELLOW}[!]${NC} $1"; }
|
||||||
|
log_error() { echo -e "${RED}[✗]${NC} $1"; }
|
||||||
|
|
||||||
|
# Verificar requisitos
|
||||||
|
check_requirements() {
|
||||||
|
log_info "Verificando requisitos..."
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
log_error "Docker não encontrado!"
|
||||||
|
echo ""
|
||||||
|
echo "Instale o Docker primeiro:"
|
||||||
|
echo " curl -fsSL https://get.docker.com | sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_success "Docker instalado"
|
||||||
|
|
||||||
|
# Docker Compose
|
||||||
|
if ! docker compose version &> /dev/null && ! command -v docker-compose &> /dev/null; then
|
||||||
|
log_error "Docker Compose não encontrado!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_success "Docker Compose instalado"
|
||||||
|
|
||||||
|
# Verificar se Docker está rodando
|
||||||
|
if ! docker info &> /dev/null; then
|
||||||
|
log_error "Docker não está rodando!"
|
||||||
|
echo " sudo systemctl start docker"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_success "Docker está rodando"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gerar string aleatória
|
||||||
|
generate_secret() {
|
||||||
|
openssl rand -hex 32 2>/dev/null || head -c 64 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 64
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gerar API Key
|
||||||
|
generate_api_key() {
|
||||||
|
echo "ophion_$(openssl rand -hex 32 2>/dev/null || head -c 64 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 64)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Coletar informações do cliente
|
||||||
|
collect_info() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${CYAN} CONFIGURAÇÃO INICIAL ${NC}"
|
||||||
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Nome da organização
|
||||||
|
read -p "📋 Nome da sua empresa/organização: " ORG_NAME
|
||||||
|
ORG_NAME=${ORG_NAME:-"Minha Empresa"}
|
||||||
|
|
||||||
|
# Email do admin
|
||||||
|
read -p "📧 Email do administrador: " ADMIN_EMAIL
|
||||||
|
while [[ ! "$ADMIN_EMAIL" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; do
|
||||||
|
log_warn "Email inválido!"
|
||||||
|
read -p "📧 Email do administrador: " ADMIN_EMAIL
|
||||||
|
done
|
||||||
|
|
||||||
|
# Senha do admin
|
||||||
|
echo -n "🔐 Senha do administrador (mín. 8 caracteres): "
|
||||||
|
read -s ADMIN_PASSWORD
|
||||||
|
echo ""
|
||||||
|
while [[ ${#ADMIN_PASSWORD} -lt 8 ]]; do
|
||||||
|
log_warn "Senha muito curta!"
|
||||||
|
echo -n "🔐 Senha do administrador (mín. 8 caracteres): "
|
||||||
|
read -s ADMIN_PASSWORD
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
# Domínio (opcional)
|
||||||
|
read -p "🌐 Domínio (deixe vazio para localhost): " DOMAIN
|
||||||
|
DOMAIN=${DOMAIN:-"localhost"}
|
||||||
|
|
||||||
|
# Porta
|
||||||
|
read -p "🔌 Porta do servidor [8080]: " SERVER_PORT
|
||||||
|
SERVER_PORT=${SERVER_PORT:-8080}
|
||||||
|
|
||||||
|
# Porta do dashboard
|
||||||
|
read -p "🖥️ Porta do dashboard [3000]: " DASHBOARD_PORT
|
||||||
|
DASHBOARD_PORT=${DASHBOARD_PORT:-3000}
|
||||||
|
|
||||||
|
# Habilitar HTTPS?
|
||||||
|
if [[ "$DOMAIN" != "localhost" ]]; then
|
||||||
|
read -p "🔒 Habilitar HTTPS com Let's Encrypt? (s/n) [s]: " ENABLE_HTTPS
|
||||||
|
ENABLE_HTTPS=${ENABLE_HTTPS:-s}
|
||||||
|
else
|
||||||
|
ENABLE_HTTPS="n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Telegram para alertas (opcional)
|
||||||
|
echo ""
|
||||||
|
read -p "📱 Configurar alertas no Telegram? (s/n) [n]: " ENABLE_TELEGRAM
|
||||||
|
if [[ "$ENABLE_TELEGRAM" =~ ^[sS]$ ]]; then
|
||||||
|
read -p " Bot Token: " TELEGRAM_BOT_TOKEN
|
||||||
|
read -p " Chat ID: " TELEGRAM_CHAT_ID
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_success "Informações coletadas!"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Criar diretório de instalação
|
||||||
|
setup_directory() {
|
||||||
|
INSTALL_DIR="/opt/ophion"
|
||||||
|
|
||||||
|
log_info "Criando diretório de instalação em $INSTALL_DIR..."
|
||||||
|
|
||||||
|
sudo mkdir -p "$INSTALL_DIR"
|
||||||
|
sudo mkdir -p "$INSTALL_DIR/data/postgres"
|
||||||
|
sudo mkdir -p "$INSTALL_DIR/data/clickhouse"
|
||||||
|
sudo mkdir -p "$INSTALL_DIR/data/redis"
|
||||||
|
sudo mkdir -p "$INSTALL_DIR/configs"
|
||||||
|
sudo mkdir -p "$INSTALL_DIR/logs"
|
||||||
|
|
||||||
|
sudo chown -R $USER:$USER "$INSTALL_DIR"
|
||||||
|
|
||||||
|
cd "$INSTALL_DIR"
|
||||||
|
log_success "Diretório criado"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gerar arquivo .env
|
||||||
|
generate_env() {
|
||||||
|
log_info "Gerando configuração..."
|
||||||
|
|
||||||
|
JWT_SECRET=$(generate_secret)
|
||||||
|
POSTGRES_PASSWORD=$(generate_secret | head -c 32)
|
||||||
|
API_KEY=$(generate_api_key)
|
||||||
|
|
||||||
|
cat > "$INSTALL_DIR/.env" << EOF
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
# 🐍 OPHION - Configuração
|
||||||
|
# Gerado em: $(date)
|
||||||
|
# Organização: $ORG_NAME
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# Organização
|
||||||
|
ORG_NAME="$ORG_NAME"
|
||||||
|
ADMIN_EMAIL="$ADMIN_EMAIL"
|
||||||
|
ADMIN_PASSWORD="$ADMIN_PASSWORD"
|
||||||
|
|
||||||
|
# Rede
|
||||||
|
DOMAIN=$DOMAIN
|
||||||
|
SERVER_PORT=$SERVER_PORT
|
||||||
|
DASHBOARD_PORT=$DASHBOARD_PORT
|
||||||
|
API_URL=http://ophion-server:8080
|
||||||
|
|
||||||
|
# Segurança (NÃO COMPARTILHE!)
|
||||||
|
JWT_SECRET=$JWT_SECRET
|
||||||
|
API_KEY=$API_KEY
|
||||||
|
|
||||||
|
# PostgreSQL
|
||||||
|
POSTGRES_USER=ophion
|
||||||
|
POSTGRES_PASSWORD=$POSTGRES_PASSWORD
|
||||||
|
POSTGRES_DB=ophion
|
||||||
|
DATABASE_URL=postgres://ophion:$POSTGRES_PASSWORD@postgres:5432/ophion
|
||||||
|
|
||||||
|
# ClickHouse
|
||||||
|
CLICKHOUSE_URL=clickhouse://clickhouse:9000/ophion
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_URL=redis://redis:6379
|
||||||
|
|
||||||
|
# Telegram Alertas
|
||||||
|
TELEGRAM_ENABLED=${ENABLE_TELEGRAM:-n}
|
||||||
|
TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-}
|
||||||
|
TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-}
|
||||||
|
|
||||||
|
# OpenAI (para recursos de IA)
|
||||||
|
OPENAI_API_KEY=
|
||||||
|
|
||||||
|
# Timezone
|
||||||
|
TZ=America/Sao_Paulo
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 600 "$INSTALL_DIR/.env"
|
||||||
|
log_success "Arquivo .env gerado"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gerar docker-compose.yml
|
||||||
|
generate_compose() {
|
||||||
|
log_info "Gerando docker-compose.yml..."
|
||||||
|
|
||||||
|
cat > "$INSTALL_DIR/docker-compose.yml" << 'EOF'
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
# 🐍 OPHION Server (API)
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
ophion-server:
|
||||||
|
image: ghcr.io/bigtux/ophion-server:latest
|
||||||
|
container_name: ophion-server
|
||||||
|
ports:
|
||||||
|
- "${SERVER_PORT}:8080"
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=${DATABASE_URL}
|
||||||
|
- CLICKHOUSE_URL=${CLICKHOUSE_URL}
|
||||||
|
- REDIS_URL=${REDIS_URL}
|
||||||
|
- JWT_SECRET=${JWT_SECRET}
|
||||||
|
- ADMIN_EMAIL=${ADMIN_EMAIL}
|
||||||
|
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
||||||
|
- ORG_NAME=${ORG_NAME}
|
||||||
|
- TELEGRAM_ENABLED=${TELEGRAM_ENABLED}
|
||||||
|
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
||||||
|
- TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID}
|
||||||
|
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||||
|
- TZ=${TZ}
|
||||||
|
volumes:
|
||||||
|
- ./configs:/app/configs:ro
|
||||||
|
- ./logs:/app/logs
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
clickhouse:
|
||||||
|
condition: service_started
|
||||||
|
redis:
|
||||||
|
condition: service_started
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- ophion-net
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
# 🖥️ OPHION Dashboard (Web UI)
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
ophion-web:
|
||||||
|
image: ghcr.io/bigtux/ophion-web:latest
|
||||||
|
container_name: ophion-web
|
||||||
|
ports:
|
||||||
|
- "${DASHBOARD_PORT}:3000"
|
||||||
|
environment:
|
||||||
|
- API_URL=http://ophion-server:8080
|
||||||
|
- NEXT_PUBLIC_API_URL=http://${DOMAIN}:${SERVER_PORT}
|
||||||
|
- ORG_NAME=${ORG_NAME}
|
||||||
|
depends_on:
|
||||||
|
- ophion-server
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- ophion-net
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
# 🐘 PostgreSQL (Metadados, Usuários, Config)
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: ophion-postgres
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=${POSTGRES_USER}
|
||||||
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||||
|
- POSTGRES_DB=${POSTGRES_DB}
|
||||||
|
volumes:
|
||||||
|
- ./data/postgres:/var/lib/postgresql/data
|
||||||
|
- ./init/postgres:/docker-entrypoint-initdb.d:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- ophion-net
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
# 🏠 ClickHouse (Métricas, Logs, Traces)
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
clickhouse:
|
||||||
|
image: clickhouse/clickhouse-server:24.1
|
||||||
|
container_name: ophion-clickhouse
|
||||||
|
volumes:
|
||||||
|
- ./data/clickhouse:/var/lib/clickhouse
|
||||||
|
- ./init/clickhouse:/docker-entrypoint-initdb.d:ro
|
||||||
|
ulimits:
|
||||||
|
nofile:
|
||||||
|
soft: 262144
|
||||||
|
hard: 262144
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- ophion-net
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
# 🔴 Redis (Cache, Sessions, Filas)
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: ophion-redis
|
||||||
|
command: redis-server --appendonly yes
|
||||||
|
volumes:
|
||||||
|
- ./data/redis:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- ophion-net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
ophion-net:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log_success "docker-compose.yml gerado"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gerar scripts SQL de inicialização
|
||||||
|
generate_init_scripts() {
|
||||||
|
log_info "Gerando scripts de inicialização..."
|
||||||
|
|
||||||
|
mkdir -p "$INSTALL_DIR/init/postgres"
|
||||||
|
mkdir -p "$INSTALL_DIR/init/clickhouse"
|
||||||
|
|
||||||
|
# PostgreSQL init
|
||||||
|
cat > "$INSTALL_DIR/init/postgres/01-schema.sql" << 'EOF'
|
||||||
|
-- ═══════════════════════════════════════════════════════════
|
||||||
|
-- 🐍 OPHION - Schema PostgreSQL
|
||||||
|
-- ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
-- Extensões
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||||
|
|
||||||
|
-- Organizações
|
||||||
|
CREATE TABLE organizations (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
slug VARCHAR(100) UNIQUE NOT NULL,
|
||||||
|
settings JSONB DEFAULT '{}',
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Usuários
|
||||||
|
CREATE TABLE users (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
org_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
name VARCHAR(255),
|
||||||
|
role VARCHAR(50) DEFAULT 'viewer',
|
||||||
|
avatar_url TEXT,
|
||||||
|
settings JSONB DEFAULT '{}',
|
||||||
|
last_login_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- API Keys
|
||||||
|
CREATE TABLE api_keys (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
org_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
key_hash VARCHAR(64) NOT NULL,
|
||||||
|
key_prefix VARCHAR(20) NOT NULL,
|
||||||
|
name VARCHAR(255),
|
||||||
|
description TEXT,
|
||||||
|
scopes TEXT[] DEFAULT ARRAY['metrics:write', 'logs:write'],
|
||||||
|
created_by UUID REFERENCES users(id),
|
||||||
|
last_used_at TIMESTAMPTZ,
|
||||||
|
expires_at TIMESTAMPTZ,
|
||||||
|
revoked BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Hosts/Agents
|
||||||
|
CREATE TABLE hosts (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
org_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
hostname VARCHAR(255) NOT NULL,
|
||||||
|
ip_address INET,
|
||||||
|
os VARCHAR(100),
|
||||||
|
arch VARCHAR(50),
|
||||||
|
agent_version VARCHAR(50),
|
||||||
|
tags JSONB DEFAULT '{}',
|
||||||
|
status VARCHAR(50) DEFAULT 'unknown',
|
||||||
|
last_seen_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
UNIQUE(org_id, hostname)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Alert Rules
|
||||||
|
CREATE TABLE alert_rules (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
org_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
query TEXT NOT NULL,
|
||||||
|
condition VARCHAR(50) NOT NULL,
|
||||||
|
threshold DECIMAL,
|
||||||
|
severity VARCHAR(50) DEFAULT 'warning',
|
||||||
|
enabled BOOLEAN DEFAULT TRUE,
|
||||||
|
notify_channels JSONB DEFAULT '[]',
|
||||||
|
created_by UUID REFERENCES users(id),
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Alert History
|
||||||
|
CREATE TABLE alert_history (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
rule_id UUID REFERENCES alert_rules(id) ON DELETE CASCADE,
|
||||||
|
org_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
host_id UUID REFERENCES hosts(id),
|
||||||
|
severity VARCHAR(50),
|
||||||
|
status VARCHAR(50) DEFAULT 'firing',
|
||||||
|
message TEXT,
|
||||||
|
value DECIMAL,
|
||||||
|
fired_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
resolved_at TIMESTAMPTZ,
|
||||||
|
acknowledged_by UUID REFERENCES users(id),
|
||||||
|
acknowledged_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Dashboards
|
||||||
|
CREATE TABLE dashboards (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
org_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
layout JSONB DEFAULT '[]',
|
||||||
|
is_default BOOLEAN DEFAULT FALSE,
|
||||||
|
created_by UUID REFERENCES users(id),
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_users_org ON users(org_id);
|
||||||
|
CREATE INDEX idx_users_email ON users(email);
|
||||||
|
CREATE INDEX idx_api_keys_org ON api_keys(org_id);
|
||||||
|
CREATE INDEX idx_api_keys_prefix ON api_keys(key_prefix);
|
||||||
|
CREATE INDEX idx_hosts_org ON hosts(org_id);
|
||||||
|
CREATE INDEX idx_hosts_status ON hosts(status);
|
||||||
|
CREATE INDEX idx_alert_history_org ON alert_history(org_id);
|
||||||
|
CREATE INDEX idx_alert_history_status ON alert_history(status);
|
||||||
|
CREATE INDEX idx_alert_history_fired ON alert_history(fired_at DESC);
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ClickHouse init
|
||||||
|
cat > "$INSTALL_DIR/init/clickhouse/01-schema.sql" << 'EOF'
|
||||||
|
-- ═══════════════════════════════════════════════════════════
|
||||||
|
-- 🐍 OPHION - Schema ClickHouse
|
||||||
|
-- ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
CREATE DATABASE IF NOT EXISTS ophion;
|
||||||
|
|
||||||
|
-- Métricas de Sistema
|
||||||
|
CREATE TABLE IF NOT EXISTS ophion.metrics (
|
||||||
|
org_id UUID,
|
||||||
|
host_id UUID,
|
||||||
|
hostname LowCardinality(String),
|
||||||
|
metric_name LowCardinality(String),
|
||||||
|
metric_type LowCardinality(String),
|
||||||
|
value Float64,
|
||||||
|
tags Map(String, String),
|
||||||
|
timestamp DateTime64(3),
|
||||||
|
INDEX idx_metric_name metric_name TYPE bloom_filter GRANULARITY 4,
|
||||||
|
INDEX idx_hostname hostname TYPE bloom_filter GRANULARITY 4
|
||||||
|
) ENGINE = MergeTree()
|
||||||
|
PARTITION BY toYYYYMM(timestamp)
|
||||||
|
ORDER BY (org_id, host_id, metric_name, timestamp)
|
||||||
|
TTL timestamp + INTERVAL 90 DAY;
|
||||||
|
|
||||||
|
-- Logs
|
||||||
|
CREATE TABLE IF NOT EXISTS ophion.logs (
|
||||||
|
org_id UUID,
|
||||||
|
host_id UUID,
|
||||||
|
hostname LowCardinality(String),
|
||||||
|
service LowCardinality(String),
|
||||||
|
level LowCardinality(String),
|
||||||
|
message String,
|
||||||
|
attributes Map(String, String),
|
||||||
|
trace_id String,
|
||||||
|
span_id String,
|
||||||
|
timestamp DateTime64(3),
|
||||||
|
INDEX idx_level level TYPE set(0) GRANULARITY 4,
|
||||||
|
INDEX idx_service service TYPE bloom_filter GRANULARITY 4,
|
||||||
|
INDEX idx_message message TYPE tokenbf_v1(10240, 3, 0) GRANULARITY 4
|
||||||
|
) ENGINE = MergeTree()
|
||||||
|
PARTITION BY toYYYYMM(timestamp)
|
||||||
|
ORDER BY (org_id, timestamp, host_id)
|
||||||
|
TTL timestamp + INTERVAL 30 DAY;
|
||||||
|
|
||||||
|
-- Traces (Spans)
|
||||||
|
CREATE TABLE IF NOT EXISTS ophion.traces (
|
||||||
|
org_id UUID,
|
||||||
|
trace_id String,
|
||||||
|
span_id String,
|
||||||
|
parent_span_id String,
|
||||||
|
operation_name LowCardinality(String),
|
||||||
|
service_name LowCardinality(String),
|
||||||
|
kind LowCardinality(String),
|
||||||
|
status_code UInt8,
|
||||||
|
status_message String,
|
||||||
|
attributes Map(String, String),
|
||||||
|
events Nested(
|
||||||
|
name String,
|
||||||
|
timestamp DateTime64(3),
|
||||||
|
attributes Map(String, String)
|
||||||
|
),
|
||||||
|
duration_ms Float64,
|
||||||
|
start_time DateTime64(3),
|
||||||
|
end_time DateTime64(3),
|
||||||
|
INDEX idx_trace_id trace_id TYPE bloom_filter GRANULARITY 4,
|
||||||
|
INDEX idx_service service_name TYPE bloom_filter GRANULARITY 4
|
||||||
|
) ENGINE = MergeTree()
|
||||||
|
PARTITION BY toYYYYMM(start_time)
|
||||||
|
ORDER BY (org_id, service_name, start_time, trace_id)
|
||||||
|
TTL start_time + INTERVAL 14 DAY;
|
||||||
|
|
||||||
|
-- Aggregated metrics (rollups)
|
||||||
|
CREATE TABLE IF NOT EXISTS ophion.metrics_hourly (
|
||||||
|
org_id UUID,
|
||||||
|
host_id UUID,
|
||||||
|
metric_name LowCardinality(String),
|
||||||
|
hour DateTime,
|
||||||
|
min_value Float64,
|
||||||
|
max_value Float64,
|
||||||
|
avg_value Float64,
|
||||||
|
count UInt64
|
||||||
|
) ENGINE = SummingMergeTree()
|
||||||
|
PARTITION BY toYYYYMM(hour)
|
||||||
|
ORDER BY (org_id, host_id, metric_name, hour)
|
||||||
|
TTL hour + INTERVAL 1 YEAR;
|
||||||
|
|
||||||
|
-- Materialized view para rollup
|
||||||
|
CREATE MATERIALIZED VIEW IF NOT EXISTS ophion.metrics_hourly_mv
|
||||||
|
TO ophion.metrics_hourly AS
|
||||||
|
SELECT
|
||||||
|
org_id,
|
||||||
|
host_id,
|
||||||
|
metric_name,
|
||||||
|
toStartOfHour(timestamp) AS hour,
|
||||||
|
min(value) AS min_value,
|
||||||
|
max(value) AS max_value,
|
||||||
|
avg(value) AS avg_value,
|
||||||
|
count() AS count
|
||||||
|
FROM ophion.metrics
|
||||||
|
GROUP BY org_id, host_id, metric_name, hour;
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log_success "Scripts de inicialização gerados"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gerar script do Agent
|
||||||
|
generate_agent_installer() {
|
||||||
|
log_info "Gerando instalador do agent..."
|
||||||
|
|
||||||
|
mkdir -p "$INSTALL_DIR/scripts"
|
||||||
|
|
||||||
|
cat > "$INSTALL_DIR/scripts/install-agent.sh" << EOF
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# 🐍 OPHION Agent Installer
|
||||||
|
# Servidor: http://${DOMAIN}:${SERVER_PORT}
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
API_KEY="${API_KEY}"
|
||||||
|
SERVER_URL="http://${DOMAIN}:${SERVER_PORT}"
|
||||||
|
|
||||||
|
echo "🐍 Instalando OPHION Agent..."
|
||||||
|
|
||||||
|
# Detectar OS
|
||||||
|
if [[ -f /etc/debian_version ]]; then
|
||||||
|
OS="debian"
|
||||||
|
elif [[ -f /etc/redhat-release ]]; then
|
||||||
|
OS="redhat"
|
||||||
|
else
|
||||||
|
echo "OS não suportado!"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create directory
|
# Baixar agent
|
||||||
INSTALL_DIR="${OPHION_DIR:-/opt/ophion}"
|
curl -fsSL -o /tmp/ophion-agent "\${SERVER_URL}/downloads/agent/linux/amd64/ophion-agent"
|
||||||
mkdir -p "$INSTALL_DIR"
|
chmod +x /tmp/ophion-agent
|
||||||
|
sudo mv /tmp/ophion-agent /usr/local/bin/
|
||||||
|
|
||||||
|
# Criar config
|
||||||
|
sudo mkdir -p /etc/ophion
|
||||||
|
sudo tee /etc/ophion/agent.yaml > /dev/null << AGENTEOF
|
||||||
|
server:
|
||||||
|
url: \${SERVER_URL}
|
||||||
|
api_key: \${API_KEY}
|
||||||
|
|
||||||
|
collection:
|
||||||
|
interval: 30s
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
enabled: true
|
||||||
|
include:
|
||||||
|
- cpu
|
||||||
|
- memory
|
||||||
|
- disk
|
||||||
|
- network
|
||||||
|
- processes
|
||||||
|
|
||||||
|
logs:
|
||||||
|
enabled: true
|
||||||
|
paths:
|
||||||
|
- /var/log/syslog
|
||||||
|
- /var/log/auth.log
|
||||||
|
AGENTEOF
|
||||||
|
|
||||||
|
# Criar systemd service
|
||||||
|
sudo tee /etc/systemd/system/ophion-agent.service > /dev/null << SERVICEEOF
|
||||||
|
[Unit]
|
||||||
|
Description=OPHION Monitoring Agent
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/bin/ophion-agent -config /etc/ophion/agent.yaml
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
SERVICEEOF
|
||||||
|
|
||||||
|
# Iniciar
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable ophion-agent
|
||||||
|
sudo systemctl start ophion-agent
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ OPHION Agent instalado!"
|
||||||
|
echo " Status: sudo systemctl status ophion-agent"
|
||||||
|
echo " Logs: sudo journalctl -u ophion-agent -f"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x "$INSTALL_DIR/scripts/install-agent.sh"
|
||||||
|
log_success "Instalador do agent gerado"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gerar comandos de gerenciamento
|
||||||
|
generate_cli() {
|
||||||
|
log_info "Gerando CLI de gerenciamento..."
|
||||||
|
|
||||||
|
cat > "$INSTALL_DIR/ophion" << 'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# 🐍 OPHION CLI
|
||||||
|
#
|
||||||
|
|
||||||
|
INSTALL_DIR="/opt/ophion"
|
||||||
cd "$INSTALL_DIR"
|
cd "$INSTALL_DIR"
|
||||||
|
|
||||||
# Download docker-compose
|
case "$1" in
|
||||||
echo "📥 Downloading OPHION..."
|
start)
|
||||||
curl -fsSL https://raw.githubusercontent.com/bigtux/ophion/main/deploy/docker/docker-compose.yml -o docker-compose.yml
|
echo "🚀 Iniciando OPHION..."
|
||||||
|
docker compose up -d
|
||||||
|
echo "✅ OPHION iniciado!"
|
||||||
|
echo " Dashboard: http://localhost:${DASHBOARD_PORT:-3000}"
|
||||||
|
echo " API: http://localhost:${SERVER_PORT:-8080}"
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
echo "🛑 Parando OPHION..."
|
||||||
|
docker compose down
|
||||||
|
echo "✅ OPHION parado"
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
echo "🔄 Reiniciando OPHION..."
|
||||||
|
docker compose restart
|
||||||
|
echo "✅ OPHION reiniciado"
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
docker compose ps
|
||||||
|
;;
|
||||||
|
logs)
|
||||||
|
docker compose logs -f ${2:-ophion-server}
|
||||||
|
;;
|
||||||
|
update)
|
||||||
|
echo "📦 Atualizando OPHION..."
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
echo "✅ OPHION atualizado!"
|
||||||
|
;;
|
||||||
|
backup)
|
||||||
|
BACKUP_DIR="$INSTALL_DIR/backups/$(date +%Y%m%d_%H%M%S)"
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
echo "💾 Criando backup em $BACKUP_DIR..."
|
||||||
|
docker compose exec -T postgres pg_dump -U ophion ophion > "$BACKUP_DIR/postgres.sql"
|
||||||
|
cp "$INSTALL_DIR/.env" "$BACKUP_DIR/"
|
||||||
|
echo "✅ Backup criado!"
|
||||||
|
;;
|
||||||
|
api-key)
|
||||||
|
NEW_KEY="ophion_$(openssl rand -hex 32)"
|
||||||
|
echo "🔑 Nova API Key gerada:"
|
||||||
|
echo ""
|
||||||
|
echo " $NEW_KEY"
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ Salve esta key! Ela não será mostrada novamente."
|
||||||
|
;;
|
||||||
|
agent-install)
|
||||||
|
echo ""
|
||||||
|
echo "Para instalar o agent em outro servidor, execute:"
|
||||||
|
echo ""
|
||||||
|
echo " curl -fsSL http://$(hostname -I | awk '{print $1}'):${SERVER_PORT:-8080}/install-agent.sh | sudo bash"
|
||||||
|
echo ""
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "🐍 OPHION CLI"
|
||||||
|
echo ""
|
||||||
|
echo "Uso: ophion <comando>"
|
||||||
|
echo ""
|
||||||
|
echo "Comandos:"
|
||||||
|
echo " start Iniciar todos os serviços"
|
||||||
|
echo " stop Parar todos os serviços"
|
||||||
|
echo " restart Reiniciar todos os serviços"
|
||||||
|
echo " status Ver status dos serviços"
|
||||||
|
echo " logs [svc] Ver logs (padrão: ophion-server)"
|
||||||
|
echo " update Atualizar para última versão"
|
||||||
|
echo " backup Criar backup dos dados"
|
||||||
|
echo " api-key Gerar nova API key"
|
||||||
|
echo " agent-install Mostrar comando de instalação do agent"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
EOF
|
||||||
|
|
||||||
# Generate secrets
|
chmod +x "$INSTALL_DIR/ophion"
|
||||||
JWT_SECRET=$(openssl rand -hex 32)
|
sudo ln -sf "$INSTALL_DIR/ophion" /usr/local/bin/ophion
|
||||||
echo "JWT_SECRET=$JWT_SECRET" > .env
|
|
||||||
|
|
||||||
# Start services
|
log_success "CLI instalado em /usr/local/bin/ophion"
|
||||||
echo "🚀 Starting OPHION..."
|
}
|
||||||
docker compose up -d
|
|
||||||
|
|
||||||
echo ""
|
# Resumo final
|
||||||
echo "✅ OPHION installed successfully!"
|
show_summary() {
|
||||||
echo ""
|
echo ""
|
||||||
echo "📊 Dashboard: http://localhost:3000"
|
echo -e "${GREEN}═══════════════════════════════════════════════════════════${NC}"
|
||||||
echo "🔌 API: http://localhost:8080"
|
echo -e "${GREEN} 🎉 OPHION INSTALADO COM SUCESSO! ${NC}"
|
||||||
echo ""
|
echo -e "${GREEN}═══════════════════════════════════════════════════════════${NC}"
|
||||||
echo "Next steps:"
|
echo ""
|
||||||
echo "1. Open http://localhost:3000 in your browser"
|
echo -e " ${CYAN}Organização:${NC} $ORG_NAME"
|
||||||
echo "2. Create your admin account"
|
echo -e " ${CYAN}Admin:${NC} $ADMIN_EMAIL"
|
||||||
echo "3. Add your first server with the agent"
|
echo ""
|
||||||
echo ""
|
echo -e " ${CYAN}Dashboard:${NC} http://${DOMAIN}:${DASHBOARD_PORT}"
|
||||||
echo "To install the agent on a server:"
|
echo -e " ${CYAN}API:${NC} http://${DOMAIN}:${SERVER_PORT}"
|
||||||
echo " curl -fsSL https://get.ophion.io/agent | bash"
|
echo ""
|
||||||
echo ""
|
echo -e " ${YELLOW}🔑 API Key (SALVE AGORA!):${NC}"
|
||||||
|
echo -e " ${PURPLE}$API_KEY${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${CYAN}Diretório:${NC} $INSTALL_DIR"
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}───────────────────────────────────────────────────────────${NC}"
|
||||||
|
echo ""
|
||||||
|
echo " Comandos úteis:"
|
||||||
|
echo ""
|
||||||
|
echo " ophion start # Iniciar"
|
||||||
|
echo " ophion stop # Parar"
|
||||||
|
echo " ophion status # Ver status"
|
||||||
|
echo " ophion logs # Ver logs"
|
||||||
|
echo " ophion agent-install # Instalar agent em outros servidores"
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}───────────────────────────────────────────────────────────${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
read -p "🚀 Iniciar OPHION agora? (s/n) [s]: " START_NOW
|
||||||
|
START_NOW=${START_NOW:-s}
|
||||||
|
|
||||||
|
if [[ "$START_NOW" =~ ^[sS]$ ]]; then
|
||||||
|
echo ""
|
||||||
|
log_info "Iniciando serviços..."
|
||||||
|
cd "$INSTALL_DIR"
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_success "OPHION está rodando!"
|
||||||
|
echo ""
|
||||||
|
echo -e " Acesse: ${CYAN}http://${DOMAIN}:${DASHBOARD_PORT}${NC}"
|
||||||
|
echo -e " Login: ${CYAN}${ADMIN_EMAIL}${NC}"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
# MAIN
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
main() {
|
||||||
|
clear
|
||||||
|
show_banner
|
||||||
|
check_requirements
|
||||||
|
collect_info
|
||||||
|
setup_directory
|
||||||
|
generate_env
|
||||||
|
generate_compose
|
||||||
|
generate_init_scripts
|
||||||
|
generate_agent_installer
|
||||||
|
generate_cli
|
||||||
|
show_summary
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
|
|||||||
@@ -1,27 +1,265 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/gofiber/fiber/v2/middleware/limiter"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func RateLimitMiddleware() fiber.Handler {
|
// ═══════════════════════════════════════════════════════════
|
||||||
return limiter.New(limiter.Config{
|
// 🚦 RATE LIMITING MIDDLEWARE
|
||||||
Max: 100, // 100 requests
|
// ═══════════════════════════════════════════════════════════
|
||||||
Expiration: 1 * time.Minute, // per minute
|
|
||||||
|
// RateLimitConfig configuração do rate limiter
|
||||||
|
type RateLimitConfig struct {
|
||||||
|
// Requests máximos por janela
|
||||||
|
Max int
|
||||||
|
|
||||||
|
// Janela de tempo
|
||||||
|
Window time.Duration
|
||||||
|
|
||||||
|
// Função para extrair identificador (default: IP)
|
||||||
|
KeyGenerator func(*fiber.Ctx) string
|
||||||
|
|
||||||
|
// Função para resposta quando limitado
|
||||||
|
LimitReached func(*fiber.Ctx) error
|
||||||
|
|
||||||
|
// Pular rate limit para certos paths
|
||||||
|
SkipPaths []string
|
||||||
|
|
||||||
|
// Headers customizados
|
||||||
|
Headers RateLimitHeaders
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimitHeaders headers do rate limit
|
||||||
|
type RateLimitHeaders struct {
|
||||||
|
Limit string // X-RateLimit-Limit
|
||||||
|
Remaining string // X-RateLimit-Remaining
|
||||||
|
Reset string // X-RateLimit-Reset
|
||||||
|
RetryAfter string // Retry-After
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultRateLimitConfig configuração padrão
|
||||||
|
func DefaultRateLimitConfig() RateLimitConfig {
|
||||||
|
return RateLimitConfig{
|
||||||
|
Max: 100,
|
||||||
|
Window: time.Minute,
|
||||||
KeyGenerator: func(c *fiber.Ctx) string {
|
KeyGenerator: func(c *fiber.Ctx) string {
|
||||||
// Use API key or IP for rate limiting
|
|
||||||
if key := c.Locals("api_key"); key != nil {
|
|
||||||
return key.(string)
|
|
||||||
}
|
|
||||||
return c.IP()
|
return c.IP()
|
||||||
},
|
},
|
||||||
LimitReached: func(c *fiber.Ctx) error {
|
LimitReached: func(c *fiber.Ctx) error {
|
||||||
return c.Status(429).JSON(fiber.Map{
|
return c.Status(429).JSON(fiber.Map{
|
||||||
"error": "Rate limit exceeded",
|
"error": "Too Many Requests",
|
||||||
|
"message": "Rate limit exceeded. Please slow down.",
|
||||||
|
"retry_after": 60,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
Headers: RateLimitHeaders{
|
||||||
|
Limit: "X-RateLimit-Limit",
|
||||||
|
Remaining: "X-RateLimit-Remaining",
|
||||||
|
Reset: "X-RateLimit-Reset",
|
||||||
|
RetryAfter: "Retry-After",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// rateLimitStore armazena contagem de requests
|
||||||
|
type rateLimitStore struct {
|
||||||
|
sync.RWMutex
|
||||||
|
entries map[string]*rateLimitEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
type rateLimitEntry struct {
|
||||||
|
count int
|
||||||
|
expiresAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var store = &rateLimitStore{
|
||||||
|
entries: make(map[string]*rateLimitEntry),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limpar entries expirados periodicamente
|
||||||
|
func init() {
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(time.Minute)
|
||||||
|
for range ticker.C {
|
||||||
|
store.cleanup()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *rateLimitStore) cleanup() {
|
||||||
|
s.Lock()
|
||||||
|
defer s.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
for key, entry := range s.entries {
|
||||||
|
if now.After(entry.expiresAt) {
|
||||||
|
delete(s.entries, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimit middleware de rate limiting
|
||||||
|
func RateLimit(config ...RateLimitConfig) fiber.Handler {
|
||||||
|
cfg := DefaultRateLimitConfig()
|
||||||
|
if len(config) > 0 {
|
||||||
|
cfg = config[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
// Verificar skip paths
|
||||||
|
path := c.Path()
|
||||||
|
for _, skip := range cfg.SkipPaths {
|
||||||
|
if path == skip {
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gerar key
|
||||||
|
key := cfg.KeyGenerator(c)
|
||||||
|
|
||||||
|
// Verificar/atualizar contagem
|
||||||
|
store.Lock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
entry, exists := store.entries[key]
|
||||||
|
|
||||||
|
if !exists || now.After(entry.expiresAt) {
|
||||||
|
// Nova janela
|
||||||
|
store.entries[key] = &rateLimitEntry{
|
||||||
|
count: 1,
|
||||||
|
expiresAt: now.Add(cfg.Window),
|
||||||
|
}
|
||||||
|
store.Unlock()
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
c.Set(cfg.Headers.Limit, strconv.Itoa(cfg.Max))
|
||||||
|
c.Set(cfg.Headers.Remaining, strconv.Itoa(cfg.Max-1))
|
||||||
|
c.Set(cfg.Headers.Reset, strconv.FormatInt(now.Add(cfg.Window).Unix(), 10))
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.count++
|
||||||
|
remaining := cfg.Max - entry.count
|
||||||
|
resetTime := entry.expiresAt.Unix()
|
||||||
|
|
||||||
|
store.Unlock()
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
c.Set(cfg.Headers.Limit, strconv.Itoa(cfg.Max))
|
||||||
|
c.Set(cfg.Headers.Remaining, strconv.Itoa(max(0, remaining)))
|
||||||
|
c.Set(cfg.Headers.Reset, strconv.FormatInt(resetTime, 10))
|
||||||
|
|
||||||
|
// Verificar se excedeu
|
||||||
|
if remaining < 0 {
|
||||||
|
retryAfter := int(time.Until(entry.expiresAt).Seconds())
|
||||||
|
c.Set(cfg.Headers.RetryAfter, strconv.Itoa(max(1, retryAfter)))
|
||||||
|
return cfg.LimitReached(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 🎯 RATE LIMIT PRESETS
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// RateLimitAuth rate limit para endpoints de autenticação (mais restritivo)
|
||||||
|
func RateLimitAuth() fiber.Handler {
|
||||||
|
return RateLimit(RateLimitConfig{
|
||||||
|
Max: 5,
|
||||||
|
Window: time.Minute,
|
||||||
|
KeyGenerator: func(c *fiber.Ctx) string {
|
||||||
|
// Combinar IP + email para prevenir ataques distribuídos
|
||||||
|
email := c.FormValue("email")
|
||||||
|
if email == "" {
|
||||||
|
email = c.Query("email")
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("auth:%s:%s", c.IP(), email)
|
||||||
|
},
|
||||||
|
LimitReached: func(c *fiber.Ctx) error {
|
||||||
|
return c.Status(429).JSON(fiber.Map{
|
||||||
|
"error": "Too Many Login Attempts",
|
||||||
|
"message": "You have exceeded the maximum number of login attempts. Please wait before trying again.",
|
||||||
|
"retry_after": 60,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RateLimitAPI rate limit para API geral
|
||||||
|
func RateLimitAPI() fiber.Handler {
|
||||||
|
return RateLimit(RateLimitConfig{
|
||||||
|
Max: 100,
|
||||||
|
Window: time.Minute,
|
||||||
|
KeyGenerator: func(c *fiber.Ctx) string {
|
||||||
|
// Usar API key ou IP
|
||||||
|
if apiKey := c.Locals("api_key_id"); apiKey != nil {
|
||||||
|
return fmt.Sprintf("api:%v", apiKey)
|
||||||
|
}
|
||||||
|
if userID := c.Locals("user_id"); userID != nil {
|
||||||
|
return fmt.Sprintf("user:%v", userID)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("ip:%s", c.IP())
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimitIngest rate limit para ingestão de dados (mais permissivo)
|
||||||
|
func RateLimitIngest() fiber.Handler {
|
||||||
|
return RateLimit(RateLimitConfig{
|
||||||
|
Max: 1000,
|
||||||
|
Window: time.Minute,
|
||||||
|
KeyGenerator: func(c *fiber.Ctx) string {
|
||||||
|
if orgID := c.Locals("org_id"); orgID != nil {
|
||||||
|
return fmt.Sprintf("ingest:%v", orgID)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("ingest:ip:%s", c.IP())
|
||||||
|
},
|
||||||
|
LimitReached: func(c *fiber.Ctx) error {
|
||||||
|
return c.Status(429).JSON(fiber.Map{
|
||||||
|
"error": "Ingest Rate Limit Exceeded",
|
||||||
|
"message": "Your organization has exceeded the data ingestion rate limit.",
|
||||||
|
"retry_after": 10,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimitExport rate limit para exportação de dados
|
||||||
|
func RateLimitExport() fiber.Handler {
|
||||||
|
return RateLimit(RateLimitConfig{
|
||||||
|
Max: 10,
|
||||||
|
Window: time.Hour,
|
||||||
|
KeyGenerator: func(c *fiber.Ctx) string {
|
||||||
|
if userID := c.Locals("user_id"); userID != nil {
|
||||||
|
return fmt.Sprintf("export:%v", userID)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("export:ip:%s", c.IP())
|
||||||
|
},
|
||||||
|
LimitReached: func(c *fiber.Ctx) error {
|
||||||
|
return c.Status(429).JSON(fiber.Map{
|
||||||
|
"error": "Export Rate Limit Exceeded",
|
||||||
|
"message": "You have exceeded the maximum number of exports per hour.",
|
||||||
|
"retry_after": 3600,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 🔧 HELPERS
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
func max(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,93 +1,373 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/bigtux/ophion/internal/security"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
var jwtSecret []byte
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 🔐 AUTH CONFIG
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
func Init(secret string) {
|
type AuthConfig struct {
|
||||||
jwtSecret = []byte(secret)
|
JWTSecret []byte
|
||||||
|
JWTExpiration time.Duration
|
||||||
|
RefreshExpiration time.Duration
|
||||||
|
Issuer string
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateAPIKey creates a new API key for agents
|
type AuthService struct {
|
||||||
func GenerateAPIKey() string {
|
config AuthConfig
|
||||||
bytes := make([]byte, 32)
|
redis *redis.Client
|
||||||
rand.Read(bytes)
|
rateLimiter *security.RateLimiter
|
||||||
return "ophion_" + hex.EncodeToString(bytes)
|
loginTracker *security.LoginAttemptTracker
|
||||||
|
apiKeyStore APIKeyStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateJWT creates a JWT token for users
|
// APIKeyStore interface para storage de API keys
|
||||||
func GenerateJWT(userID string, email string) (string, error) {
|
type APIKeyStore interface {
|
||||||
claims := jwt.MapClaims{
|
ValidateKey(ctx context.Context, keyHash string) (*APIKeyInfo, error)
|
||||||
"sub": userID,
|
UpdateLastUsed(ctx context.Context, keyID string) error
|
||||||
"email": email,
|
}
|
||||||
"iat": time.Now().Unix(),
|
|
||||||
"exp": time.Now().Add(24 * time.Hour).Unix(),
|
type APIKeyInfo struct {
|
||||||
|
ID string
|
||||||
|
OrgID string
|
||||||
|
Scopes []string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthService cria serviço de autenticação
|
||||||
|
func NewAuthService(config AuthConfig, redis *redis.Client, apiKeyStore APIKeyStore) *AuthService {
|
||||||
|
return &AuthService{
|
||||||
|
config: config,
|
||||||
|
redis: redis,
|
||||||
|
rateLimiter: security.NewRateLimiter(security.AuthRateLimit, time.Minute),
|
||||||
|
loginTracker: security.NewLoginAttemptTracker(),
|
||||||
|
apiKeyStore: apiKeyStore,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 🎫 JWT TOKENS
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
type TokenClaims struct {
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
UserID string `json:"uid"`
|
||||||
|
OrgID string `json:"oid"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
Scopes []string `json:"scopes,omitempty"`
|
||||||
|
TokenID string `json:"jti"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateTokenPair gera access + refresh tokens
|
||||||
|
func (s *AuthService) GenerateTokenPair(userID, orgID, email, role string) (accessToken, refreshToken string, err error) {
|
||||||
|
now := time.Now()
|
||||||
|
tokenID := generateTokenID()
|
||||||
|
|
||||||
|
// Access Token (curta duração)
|
||||||
|
accessClaims := TokenClaims{
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
Issuer: s.config.Issuer,
|
||||||
|
Subject: userID,
|
||||||
|
IssuedAt: jwt.NewNumericDate(now),
|
||||||
|
ExpiresAt: jwt.NewNumericDate(now.Add(s.config.JWTExpiration)),
|
||||||
|
ID: tokenID,
|
||||||
|
},
|
||||||
|
UserID: userID,
|
||||||
|
OrgID: orgID,
|
||||||
|
Email: email,
|
||||||
|
Role: role,
|
||||||
|
TokenID: tokenID,
|
||||||
}
|
}
|
||||||
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
accessToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims).SignedString(s.config.JWTSecret)
|
||||||
return token.SignedString(jwtSecret)
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("failed to sign access token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh Token (longa duração)
|
||||||
|
refreshID := generateTokenID()
|
||||||
|
refreshClaims := jwt.RegisteredClaims{
|
||||||
|
Issuer: s.config.Issuer,
|
||||||
|
Subject: userID,
|
||||||
|
IssuedAt: jwt.NewNumericDate(now),
|
||||||
|
ExpiresAt: jwt.NewNumericDate(now.Add(s.config.RefreshExpiration)),
|
||||||
|
ID: refreshID,
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims).SignedString(s.config.JWTSecret)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("failed to sign refresh token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Armazenar refresh token no Redis (permite revogação)
|
||||||
|
if s.redis != nil {
|
||||||
|
ctx := context.Background()
|
||||||
|
key := fmt.Sprintf("refresh_token:%s", refreshID)
|
||||||
|
s.redis.Set(ctx, key, userID, s.config.RefreshExpiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessToken, refreshToken, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateJWT validates a JWT token
|
// ValidateAccessToken valida access token
|
||||||
func ValidateJWT(tokenString string) (*jwt.MapClaims, error) {
|
func (s *AuthService) ValidateAccessToken(tokenString string) (*TokenClaims, error) {
|
||||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
return jwtSecret, nil
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||||
|
}
|
||||||
|
return s.config.JWTSecret, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil || !token.Valid {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("invalid token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
claims, ok := token.Claims.(jwt.MapClaims)
|
claims, ok := token.Claims.(*TokenClaims)
|
||||||
if !ok {
|
if !ok || !token.Valid {
|
||||||
return nil, jwt.ErrInvalidKey
|
return nil, fmt.Errorf("invalid token claims")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &claims, nil
|
// Verificar se token foi revogado
|
||||||
|
if s.redis != nil {
|
||||||
|
ctx := context.Background()
|
||||||
|
revoked, _ := s.redis.Get(ctx, fmt.Sprintf("revoked_token:%s", claims.TokenID)).Result()
|
||||||
|
if revoked != "" {
|
||||||
|
return nil, fmt.Errorf("token revoked")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthMiddleware protects routes
|
// RevokeToken revoga um token
|
||||||
func AuthMiddleware() fiber.Handler {
|
func (s *AuthService) RevokeToken(tokenID string, expiration time.Duration) error {
|
||||||
return func(c *fiber.Ctx) error {
|
if s.redis == nil {
|
||||||
authHeader := c.Get("Authorization")
|
return nil
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
return s.redis.Set(ctx, fmt.Sprintf("revoked_token:%s", tokenID), "1", expiration).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 🛡️ MIDDLEWARE
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// AuthMiddleware middleware de autenticação
|
||||||
|
func (s *AuthService) AuthMiddleware() fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
// Obter IP real (considerando proxies)
|
||||||
|
ip := c.IP()
|
||||||
|
if forwarded := c.Get("X-Forwarded-For"); forwarded != "" {
|
||||||
|
ip = strings.Split(forwarded, ",")[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limiting por IP
|
||||||
|
if !s.rateLimiter.Allow(ip) {
|
||||||
|
return c.Status(429).JSON(fiber.Map{
|
||||||
|
"error": "Too many requests",
|
||||||
|
"message": "Rate limit exceeded. Try again later.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
authHeader := c.Get("Authorization")
|
||||||
if authHeader == "" {
|
if authHeader == "" {
|
||||||
return c.Status(401).JSON(fiber.Map{
|
return c.Status(401).JSON(fiber.Map{
|
||||||
"error": "Missing authorization header",
|
"error": "Unauthorized",
|
||||||
|
"message": "Missing authorization header",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Support both "Bearer <token>" and API keys
|
|
||||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||||
|
|
||||||
// Check if it's an API key
|
// API Key authentication
|
||||||
if strings.HasPrefix(token, "ophion_") {
|
if strings.HasPrefix(token, security.APIKeyPrefix) {
|
||||||
// TODO: Validate API key against database
|
return s.authenticateAPIKey(c, token)
|
||||||
c.Locals("auth_type", "api_key")
|
|
||||||
c.Locals("api_key", token)
|
|
||||||
return c.Next()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate JWT
|
// JWT authentication
|
||||||
claims, err := ValidateJWT(token)
|
return s.authenticateJWT(c, token)
|
||||||
if err != nil {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// authenticateAPIKey valida API key
|
||||||
|
func (s *AuthService) authenticateAPIKey(c *fiber.Ctx, apiKey string) error {
|
||||||
|
// Validar formato
|
||||||
|
if !security.ValidateAPIKeyFormat(apiKey) {
|
||||||
return c.Status(401).JSON(fiber.Map{
|
return c.Status(401).JSON(fiber.Map{
|
||||||
"error": "Invalid token",
|
"error": "Unauthorized",
|
||||||
|
"message": "Invalid API key format",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hash da key para busca
|
||||||
|
keyHash := security.HashAPIKey(apiKey)
|
||||||
|
|
||||||
|
// Buscar no storage
|
||||||
|
keyInfo, err := s.apiKeyStore.ValidateKey(c.Context(), keyHash)
|
||||||
|
if err != nil || keyInfo == nil {
|
||||||
|
return c.Status(401).JSON(fiber.Map{
|
||||||
|
"error": "Unauthorized",
|
||||||
|
"message": "Invalid API key",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atualizar last_used (async)
|
||||||
|
go s.apiKeyStore.UpdateLastUsed(context.Background(), keyInfo.ID)
|
||||||
|
|
||||||
|
// Setar contexto
|
||||||
|
c.Locals("auth_type", "api_key")
|
||||||
|
c.Locals("org_id", keyInfo.OrgID)
|
||||||
|
c.Locals("api_key_id", keyInfo.ID)
|
||||||
|
c.Locals("scopes", keyInfo.Scopes)
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// authenticateJWT valida JWT token
|
||||||
|
func (s *AuthService) authenticateJWT(c *fiber.Ctx, token string) error {
|
||||||
|
claims, err := s.ValidateAccessToken(token)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(401).JSON(fiber.Map{
|
||||||
|
"error": "Unauthorized",
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setar contexto
|
||||||
c.Locals("auth_type", "jwt")
|
c.Locals("auth_type", "jwt")
|
||||||
c.Locals("user_id", (*claims)["sub"])
|
c.Locals("user_id", claims.UserID)
|
||||||
c.Locals("email", (*claims)["email"])
|
c.Locals("org_id", claims.OrgID)
|
||||||
|
c.Locals("email", claims.Email)
|
||||||
|
c.Locals("role", claims.Role)
|
||||||
|
c.Locals("token_id", claims.TokenID)
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequireScopes middleware que exige scopes específicos
|
||||||
|
func RequireScopes(required ...string) fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
scopes, ok := c.Locals("scopes").([]string)
|
||||||
|
if !ok {
|
||||||
|
// JWT tokens têm acesso total por padrão
|
||||||
|
if c.Locals("auth_type") == "jwt" {
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
return c.Status(403).JSON(fiber.Map{
|
||||||
|
"error": "Forbidden",
|
||||||
|
"message": "Missing required scopes",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
scopeMap := make(map[string]bool)
|
||||||
|
for _, s := range scopes {
|
||||||
|
scopeMap[s] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, req := range required {
|
||||||
|
if !scopeMap[req] && !scopeMap["*"] {
|
||||||
|
return c.Status(403).JSON(fiber.Map{
|
||||||
|
"error": "Forbidden",
|
||||||
|
"message": fmt.Sprintf("Missing required scope: %s", req),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return c.Next()
|
return c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequireRole middleware que exige role específico
|
||||||
|
func RequireRole(roles ...string) fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
userRole, ok := c.Locals("role").(string)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(403).JSON(fiber.Map{
|
||||||
|
"error": "Forbidden",
|
||||||
|
"message": "Role not found",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range roles {
|
||||||
|
if userRole == r {
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin tem acesso a tudo
|
||||||
|
if userRole == "admin" {
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(403).JSON(fiber.Map{
|
||||||
|
"error": "Forbidden",
|
||||||
|
"message": "Insufficient permissions",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 🔐 LOGIN PROTECTION
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// CheckLoginAllowed verifica se login é permitido (brute force protection)
|
||||||
|
func (s *AuthService) CheckLoginAllowed(identifier string) (bool, time.Duration, error) {
|
||||||
|
locked, remaining := s.loginTracker.IsLocked(identifier)
|
||||||
|
return !locked, remaining, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordLoginAttempt registra tentativa de login
|
||||||
|
func (s *AuthService) RecordLoginAttempt(identifier string, success bool) (blocked bool, remaining int) {
|
||||||
|
if success {
|
||||||
|
s.loginTracker.RecordSuccess(identifier)
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
return s.loginTracker.RecordFailure(identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 🔧 HELPERS
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
func generateTokenID() string {
|
||||||
|
bytes := make([]byte, 16)
|
||||||
|
rand.Read(bytes)
|
||||||
|
return hex.EncodeToString(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserID obtém user ID do contexto
|
||||||
|
func GetUserID(c *fiber.Ctx) string {
|
||||||
|
if id, ok := c.Locals("user_id").(string); ok {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrgID obtém org ID do contexto
|
||||||
|
func GetOrgID(c *fiber.Ctx) string {
|
||||||
|
if id, ok := c.Locals("org_id").(string); ok {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRole obtém role do contexto
|
||||||
|
func GetRole(c *fiber.Ctx) string {
|
||||||
|
if role, ok := c.Locals("role").(string); ok {
|
||||||
|
return role
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|||||||
558
internal/security/security.go
Normal file
558
internal/security/security.go
Normal file
@@ -0,0 +1,558 @@
|
|||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 🔐 CONSTANTES DE SEGURANÇA
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Bcrypt cost (12-14 recomendado para produção)
|
||||||
|
BcryptCost = 12
|
||||||
|
|
||||||
|
// Tamanho mínimo de senha
|
||||||
|
MinPasswordLength = 12
|
||||||
|
|
||||||
|
// Tamanho da API Key
|
||||||
|
APIKeyLength = 32
|
||||||
|
|
||||||
|
// Prefixo da API Key
|
||||||
|
APIKeyPrefix = "ophion_"
|
||||||
|
|
||||||
|
// Rate limit padrão (requests por minuto)
|
||||||
|
DefaultRateLimit = 100
|
||||||
|
|
||||||
|
// Rate limit para auth (tentativas por minuto)
|
||||||
|
AuthRateLimit = 5
|
||||||
|
|
||||||
|
// Tempo de bloqueio após falhas (minutos)
|
||||||
|
LockoutDuration = 15
|
||||||
|
|
||||||
|
// Máximo de tentativas antes do bloqueio
|
||||||
|
MaxFailedAttempts = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 🔑 API KEY MANAGEMENT
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// GenerateAPIKey cria uma nova API key segura
|
||||||
|
func GenerateAPIKey() (key string, hash string, prefix string) {
|
||||||
|
bytes := make([]byte, APIKeyLength)
|
||||||
|
if _, err := rand.Read(bytes); err != nil {
|
||||||
|
panic("failed to generate random bytes")
|
||||||
|
}
|
||||||
|
|
||||||
|
key = APIKeyPrefix + hex.EncodeToString(bytes)
|
||||||
|
hash = HashAPIKey(key)
|
||||||
|
prefix = key[:len(APIKeyPrefix)+8] // ophion_xxxxxxxx
|
||||||
|
|
||||||
|
return key, hash, prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashAPIKey cria hash SHA256 da API key
|
||||||
|
func HashAPIKey(key string) string {
|
||||||
|
hash := sha256.Sum256([]byte(key))
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAPIKeyFormat verifica formato da API key
|
||||||
|
func ValidateAPIKeyFormat(key string) bool {
|
||||||
|
if !strings.HasPrefix(key, APIKeyPrefix) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// ophion_ + 64 hex chars
|
||||||
|
return len(key) == len(APIKeyPrefix)+64
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecureCompare compara strings em tempo constante (previne timing attacks)
|
||||||
|
func SecureCompare(a, b string) bool {
|
||||||
|
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 🔒 PASSWORD MANAGEMENT
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// PasswordPolicy define regras de senha
|
||||||
|
type PasswordPolicy struct {
|
||||||
|
MinLength int
|
||||||
|
RequireUppercase bool
|
||||||
|
RequireLowercase bool
|
||||||
|
RequireNumbers bool
|
||||||
|
RequireSpecial bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultPasswordPolicy retorna política padrão
|
||||||
|
func DefaultPasswordPolicy() PasswordPolicy {
|
||||||
|
return PasswordPolicy{
|
||||||
|
MinLength: 12,
|
||||||
|
RequireUppercase: true,
|
||||||
|
RequireLowercase: true,
|
||||||
|
RequireNumbers: true,
|
||||||
|
RequireSpecial: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePassword verifica se senha atende à política
|
||||||
|
func ValidatePassword(password string, policy PasswordPolicy) (bool, []string) {
|
||||||
|
var errors []string
|
||||||
|
|
||||||
|
if len(password) < policy.MinLength {
|
||||||
|
errors = append(errors, fmt.Sprintf("Senha deve ter no mínimo %d caracteres", policy.MinLength))
|
||||||
|
}
|
||||||
|
|
||||||
|
if policy.RequireUppercase && !regexp.MustCompile(`[A-Z]`).MatchString(password) {
|
||||||
|
errors = append(errors, "Senha deve conter letra maiúscula")
|
||||||
|
}
|
||||||
|
|
||||||
|
if policy.RequireLowercase && !regexp.MustCompile(`[a-z]`).MatchString(password) {
|
||||||
|
errors = append(errors, "Senha deve conter letra minúscula")
|
||||||
|
}
|
||||||
|
|
||||||
|
if policy.RequireNumbers && !regexp.MustCompile(`[0-9]`).MatchString(password) {
|
||||||
|
errors = append(errors, "Senha deve conter número")
|
||||||
|
}
|
||||||
|
|
||||||
|
if policy.RequireSpecial && !regexp.MustCompile(`[!@#$%^&*(),.?":{}|<>]`).MatchString(password) {
|
||||||
|
errors = append(errors, "Senha deve conter caractere especial")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar senhas comuns
|
||||||
|
if isCommonPassword(password) {
|
||||||
|
errors = append(errors, "Senha muito comum, escolha outra")
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(errors) == 0, errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashPassword cria hash bcrypt da senha
|
||||||
|
func HashPassword(password string) (string, error) {
|
||||||
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), BcryptCost)
|
||||||
|
return string(bytes), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyPassword verifica senha contra hash
|
||||||
|
func VerifyPassword(password, hash string) bool {
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isCommonPassword verifica senhas comuns
|
||||||
|
func isCommonPassword(password string) bool {
|
||||||
|
common := []string{
|
||||||
|
"password", "123456", "12345678", "qwerty", "abc123",
|
||||||
|
"monkey", "1234567", "letmein", "trustno1", "dragon",
|
||||||
|
"baseball", "iloveyou", "master", "sunshine", "ashley",
|
||||||
|
"passw0rd", "shadow", "123123", "654321", "superman",
|
||||||
|
"senha", "mudar123", "admin123", "root123",
|
||||||
|
}
|
||||||
|
lower := strings.ToLower(password)
|
||||||
|
for _, p := range common {
|
||||||
|
if strings.Contains(lower, p) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 🚦 RATE LIMITING
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// RateLimiter controla taxa de requests
|
||||||
|
type RateLimiter struct {
|
||||||
|
sync.RWMutex
|
||||||
|
requests map[string]*rateLimitEntry
|
||||||
|
limit int
|
||||||
|
window time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type rateLimitEntry struct {
|
||||||
|
count int
|
||||||
|
firstSeen time.Time
|
||||||
|
blocked bool
|
||||||
|
blockedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRateLimiter cria novo rate limiter
|
||||||
|
func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
|
||||||
|
rl := &RateLimiter{
|
||||||
|
requests: make(map[string]*rateLimitEntry),
|
||||||
|
limit: limit,
|
||||||
|
window: window,
|
||||||
|
}
|
||||||
|
// Cleanup goroutine
|
||||||
|
go rl.cleanup()
|
||||||
|
return rl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow verifica se request é permitido
|
||||||
|
func (rl *RateLimiter) Allow(identifier string) bool {
|
||||||
|
rl.Lock()
|
||||||
|
defer rl.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
entry, exists := rl.requests[identifier]
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
rl.requests[identifier] = &rateLimitEntry{
|
||||||
|
count: 1,
|
||||||
|
firstSeen: now,
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar se está bloqueado
|
||||||
|
if entry.blocked {
|
||||||
|
if now.Sub(entry.blockedAt) < time.Duration(LockoutDuration)*time.Minute {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Desbloquear
|
||||||
|
entry.blocked = false
|
||||||
|
entry.count = 0
|
||||||
|
entry.firstSeen = now
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar janela de tempo
|
||||||
|
if now.Sub(entry.firstSeen) > rl.window {
|
||||||
|
entry.count = 1
|
||||||
|
entry.firstSeen = now
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.count++
|
||||||
|
|
||||||
|
if entry.count > rl.limit {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block bloqueia um identificador
|
||||||
|
func (rl *RateLimiter) Block(identifier string) {
|
||||||
|
rl.Lock()
|
||||||
|
defer rl.Unlock()
|
||||||
|
|
||||||
|
entry, exists := rl.requests[identifier]
|
||||||
|
if !exists {
|
||||||
|
entry = &rateLimitEntry{}
|
||||||
|
rl.requests[identifier] = entry
|
||||||
|
}
|
||||||
|
entry.blocked = true
|
||||||
|
entry.blockedAt = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup remove entradas antigas
|
||||||
|
func (rl *RateLimiter) cleanup() {
|
||||||
|
ticker := time.NewTicker(5 * time.Minute)
|
||||||
|
for range ticker.C {
|
||||||
|
rl.Lock()
|
||||||
|
now := time.Now()
|
||||||
|
for id, entry := range rl.requests {
|
||||||
|
if now.Sub(entry.firstSeen) > rl.window*2 && !entry.blocked {
|
||||||
|
delete(rl.requests, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rl.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 🛡️ BRUTE FORCE PROTECTION
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// LoginAttemptTracker rastreia tentativas de login
|
||||||
|
type LoginAttemptTracker struct {
|
||||||
|
sync.RWMutex
|
||||||
|
attempts map[string]*loginAttempt
|
||||||
|
}
|
||||||
|
|
||||||
|
type loginAttempt struct {
|
||||||
|
failures int
|
||||||
|
lastAttempt time.Time
|
||||||
|
lockedUntil time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLoginAttemptTracker cria tracker
|
||||||
|
func NewLoginAttemptTracker() *LoginAttemptTracker {
|
||||||
|
return &LoginAttemptTracker{
|
||||||
|
attempts: make(map[string]*loginAttempt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordFailure registra falha de login
|
||||||
|
func (t *LoginAttemptTracker) RecordFailure(identifier string) (blocked bool, remainingAttempts int) {
|
||||||
|
t.Lock()
|
||||||
|
defer t.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
attempt, exists := t.attempts[identifier]
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
t.attempts[identifier] = &loginAttempt{
|
||||||
|
failures: 1,
|
||||||
|
lastAttempt: now,
|
||||||
|
}
|
||||||
|
return false, MaxFailedAttempts - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset se última tentativa foi há muito tempo
|
||||||
|
if now.Sub(attempt.lastAttempt) > time.Duration(LockoutDuration)*time.Minute {
|
||||||
|
attempt.failures = 1
|
||||||
|
attempt.lastAttempt = now
|
||||||
|
attempt.lockedUntil = time.Time{}
|
||||||
|
return false, MaxFailedAttempts - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
attempt.failures++
|
||||||
|
attempt.lastAttempt = now
|
||||||
|
|
||||||
|
if attempt.failures >= MaxFailedAttempts {
|
||||||
|
attempt.lockedUntil = now.Add(time.Duration(LockoutDuration) * time.Minute)
|
||||||
|
return true, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, MaxFailedAttempts - attempt.failures
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordSuccess registra sucesso de login
|
||||||
|
func (t *LoginAttemptTracker) RecordSuccess(identifier string) {
|
||||||
|
t.Lock()
|
||||||
|
defer t.Unlock()
|
||||||
|
delete(t.attempts, identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLocked verifica se está bloqueado
|
||||||
|
func (t *LoginAttemptTracker) IsLocked(identifier string) (bool, time.Duration) {
|
||||||
|
t.RLock()
|
||||||
|
defer t.RUnlock()
|
||||||
|
|
||||||
|
attempt, exists := t.attempts[identifier]
|
||||||
|
if !exists {
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if attempt.lockedUntil.IsZero() {
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining := time.Until(attempt.lockedUntil)
|
||||||
|
if remaining <= 0 {
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, remaining
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 🔍 INPUT VALIDATION & SANITIZATION
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// SanitizeEmail limpa e valida email
|
||||||
|
func SanitizeEmail(email string) (string, error) {
|
||||||
|
email = strings.TrimSpace(strings.ToLower(email))
|
||||||
|
|
||||||
|
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||||||
|
if !emailRegex.MatchString(email) {
|
||||||
|
return "", fmt.Errorf("email inválido")
|
||||||
|
}
|
||||||
|
|
||||||
|
return email, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SanitizeHostname limpa hostname
|
||||||
|
func SanitizeHostname(hostname string) (string, error) {
|
||||||
|
hostname = strings.TrimSpace(hostname)
|
||||||
|
|
||||||
|
// Permitir apenas alfanuméricos, hífens e pontos
|
||||||
|
hostnameRegex := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9.-]{0,253}[a-zA-Z0-9]$`)
|
||||||
|
if !hostnameRegex.MatchString(hostname) && len(hostname) > 1 {
|
||||||
|
return "", fmt.Errorf("hostname inválido")
|
||||||
|
}
|
||||||
|
|
||||||
|
return hostname, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SanitizeSQL previne SQL injection (para queries dinâmicas)
|
||||||
|
func SanitizeSQL(input string) string {
|
||||||
|
// Remover caracteres perigosos
|
||||||
|
dangerous := []string{"'", "\"", ";", "--", "/*", "*/", "xp_", "sp_"}
|
||||||
|
result := input
|
||||||
|
for _, d := range dangerous {
|
||||||
|
result = strings.ReplaceAll(result, d, "")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateIPAddress valida endereço IP
|
||||||
|
func ValidateIPAddress(ip string) bool {
|
||||||
|
return net.ParseIP(ip) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 🛡️ SECURITY HEADERS MIDDLEWARE
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// SecurityHeaders adiciona headers de segurança
|
||||||
|
func SecurityHeaders() fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
// Prevenir clickjacking
|
||||||
|
c.Set("X-Frame-Options", "DENY")
|
||||||
|
|
||||||
|
// Prevenir MIME type sniffing
|
||||||
|
c.Set("X-Content-Type-Options", "nosniff")
|
||||||
|
|
||||||
|
// XSS Protection
|
||||||
|
c.Set("X-XSS-Protection", "1; mode=block")
|
||||||
|
|
||||||
|
// Content Security Policy
|
||||||
|
c.Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' wss: https:")
|
||||||
|
|
||||||
|
// HSTS (apenas se HTTPS)
|
||||||
|
if c.Protocol() == "https" {
|
||||||
|
c.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Referrer Policy
|
||||||
|
c.Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||||
|
|
||||||
|
// Permissions Policy
|
||||||
|
c.Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
|
||||||
|
|
||||||
|
// Remove Server header
|
||||||
|
c.Set("Server", "")
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 📝 AUDIT LOGGING
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// AuditEvent representa um evento de auditoria
|
||||||
|
type AuditEvent struct {
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
EventType string `json:"event_type"`
|
||||||
|
UserID string `json:"user_id,omitempty"`
|
||||||
|
OrgID string `json:"org_id,omitempty"`
|
||||||
|
IP string `json:"ip"`
|
||||||
|
UserAgent string `json:"user_agent"`
|
||||||
|
Resource string `json:"resource"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Details map[string]string `json:"details,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuditEventType tipos de eventos
|
||||||
|
const (
|
||||||
|
AuditLogin = "auth.login"
|
||||||
|
AuditLogout = "auth.logout"
|
||||||
|
AuditLoginFailed = "auth.login_failed"
|
||||||
|
AuditAPIKeyCreated = "apikey.created"
|
||||||
|
AuditAPIKeyRevoked = "apikey.revoked"
|
||||||
|
AuditUserCreated = "user.created"
|
||||||
|
AuditUserDeleted = "user.deleted"
|
||||||
|
AuditConfigChanged = "config.changed"
|
||||||
|
AuditAlertCreated = "alert.created"
|
||||||
|
AuditDataExport = "data.export"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuditLogger interface para logging de auditoria
|
||||||
|
type AuditLogger interface {
|
||||||
|
Log(event AuditEvent) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 🔐 SECRETS MANAGEMENT
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// GenerateSecureToken gera token seguro
|
||||||
|
func GenerateSecureToken(length int) string {
|
||||||
|
bytes := make([]byte, length)
|
||||||
|
if _, err := rand.Read(bytes); err != nil {
|
||||||
|
panic("failed to generate secure token")
|
||||||
|
}
|
||||||
|
return base64.URLEncoding.EncodeToString(bytes)[:length]
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaskSecret mascara segredos para logging
|
||||||
|
func MaskSecret(secret string) string {
|
||||||
|
if len(secret) <= 8 {
|
||||||
|
return "****"
|
||||||
|
}
|
||||||
|
return secret[:4] + "****" + secret[len(secret)-4:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 🌐 IP FILTERING
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// IPWhitelist gerencia whitelist de IPs
|
||||||
|
type IPWhitelist struct {
|
||||||
|
sync.RWMutex
|
||||||
|
allowed map[string]bool
|
||||||
|
cidrs []*net.IPNet
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIPWhitelist cria whitelist
|
||||||
|
func NewIPWhitelist(ips []string, cidrs []string) *IPWhitelist {
|
||||||
|
wl := &IPWhitelist{
|
||||||
|
allowed: make(map[string]bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ip := range ips {
|
||||||
|
wl.allowed[ip] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cidr := range cidrs {
|
||||||
|
_, network, err := net.ParseCIDR(cidr)
|
||||||
|
if err == nil {
|
||||||
|
wl.cidrs = append(wl.cidrs, network)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return wl
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAllowed verifica se IP está permitido
|
||||||
|
func (wl *IPWhitelist) IsAllowed(ip string) bool {
|
||||||
|
wl.RLock()
|
||||||
|
defer wl.RUnlock()
|
||||||
|
|
||||||
|
// Verificar lista direta
|
||||||
|
if wl.allowed[ip] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar CIDRs
|
||||||
|
parsedIP := net.ParseIP(ip)
|
||||||
|
if parsedIP == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, network := range wl.cidrs {
|
||||||
|
if network.Contains(parsedIP) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user