🔐 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
|
||||
1. [Instalação do Servidor](#instalação-do-servidor)
|
||||
2. [Instalação do Agent](#instalação-do-agent)
|
||||
3. [Monitoramento de Docker](#monitoramento-de-docker)
|
||||
4. [Monitoramento de Aplicações](#monitoramento-de-aplicações)
|
||||
5. [Configuração de Alertas](#configuração-de-alertas)
|
||||
## Instalação Rápida (1 comando)
|
||||
|
||||
```bash
|
||||
curl -fsSL https://get.ophion.io | bash
|
||||
```
|
||||
|
||||
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
|
||||
- CPU: 2 cores
|
||||
- RAM: 4GB (8GB recomendado)
|
||||
- Disco: 50GB SSD
|
||||
- OS: Ubuntu 22.04+ / Debian 12+
|
||||
- Docker 24+
|
||||
|
||||
### Instalação Rápida (1 comando)
|
||||
### 1. Requisitos
|
||||
|
||||
```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
|
||||
# Clonar repositório
|
||||
git clone https://github.com/bigtux/ophion.git
|
||||
cd ophion/deploy/docker
|
||||
|
||||
# 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
|
||||
cd ophion
|
||||
```
|
||||
|
||||
### 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
|
||||
# Segurança (OBRIGATÓRIO - gere valores únicos!)
|
||||
JWT_SECRET=sua-chave-secreta-aqui-min-32-chars
|
||||
# Sua empresa
|
||||
ORG_NAME="Minha Empresa"
|
||||
ADMIN_EMAIL=admin@empresa.com
|
||||
ADMIN_PASSWORD=senha-segura-aqui
|
||||
|
||||
# Banco de Dados
|
||||
POSTGRES_USER=ophion
|
||||
POSTGRES_PASSWORD=senha-forte-aqui
|
||||
POSTGRES_DB=ophion
|
||||
# Domínio (ou localhost para testes)
|
||||
DOMAIN=ophion.empresa.com
|
||||
|
||||
# ClickHouse (métricas/logs)
|
||||
CLICKHOUSE_USER=default
|
||||
CLICKHOUSE_PASSWORD=senha-clickhouse
|
||||
# Portas
|
||||
SERVER_PORT=8080
|
||||
DASHBOARD_PORT=3000
|
||||
|
||||
# Redis
|
||||
REDIS_PASSWORD=senha-redis
|
||||
|
||||
# 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
|
||||
# Segurança (gere valores únicos!)
|
||||
JWT_SECRET=seu-jwt-secret-aqui
|
||||
```
|
||||
|
||||
### Verificar Instalação
|
||||
### 4. Iniciar
|
||||
|
||||
```bash
|
||||
# Health check
|
||||
curl http://localhost:8080/health
|
||||
|
||||
# Resposta esperada:
|
||||
# {"status":"healthy","service":"ophion","version":"0.1.0"}
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Acessar Dashboard
|
||||
### 5. Acessar
|
||||
|
||||
Abra no navegador: `http://seu-servidor:3000`
|
||||
|
||||
1. Crie sua conta de administrador
|
||||
2. Configure sua organização
|
||||
3. Gere API Keys para os agents
|
||||
- **Dashboard:** http://localhost:3000
|
||||
- **API:** http://localhost:8080
|
||||
|
||||
---
|
||||
|
||||
## 2. Instalação do Agent
|
||||
## Instalação do Agent (Servidores Monitorados)
|
||||
|
||||
O Agent coleta métricas do servidor e envia para o OPHION.
|
||||
|
||||
### Instalação Rápida
|
||||
Em cada servidor que você quer monitorar:
|
||||
|
||||
```bash
|
||||
# Substitua YOUR_API_KEY pela chave gerada no dashboard
|
||||
curl -fsSL https://ophion.com.br/agent.sh | OPHION_API_KEY=YOUR_API_KEY bash
|
||||
curl -fsSL http://SEU-SERVIDOR-OPHION:8080/install-agent.sh | sudo bash
|
||||
```
|
||||
|
||||
### Instalação Manual
|
||||
Ou manualmente:
|
||||
|
||||
```bash
|
||||
# Baixar binário
|
||||
wget https://github.com/bigtux/ophion/releases/latest/download/ophion-agent-linux-amd64
|
||||
chmod +x ophion-agent-linux-amd64
|
||||
sudo mv ophion-agent-linux-amd64 /usr/local/bin/ophion-agent
|
||||
# Baixar
|
||||
curl -o /usr/local/bin/ophion-agent \
|
||||
http://SEU-SERVIDOR-OPHION:8080/downloads/agent/linux/amd64/ophion-agent
|
||||
chmod +x /usr/local/bin/ophion-agent
|
||||
|
||||
# Criar arquivo de configuração
|
||||
sudo mkdir -p /etc/ophion
|
||||
sudo tee /etc/ophion/agent.yaml << EOF
|
||||
server: https://api.ophion.com.br
|
||||
api_key: YOUR_API_KEY
|
||||
hostname: $(hostname)
|
||||
interval: 30s
|
||||
# Configurar
|
||||
mkdir -p /etc/ophion
|
||||
cat > /etc/ophion/agent.yaml << EOF
|
||||
server:
|
||||
url: http://SEU-SERVIDOR-OPHION:8080
|
||||
api_key: SUA-API-KEY
|
||||
|
||||
collectors:
|
||||
cpu: true
|
||||
memory: true
|
||||
disk: true
|
||||
network: true
|
||||
processes: true
|
||||
collection:
|
||||
interval: 30s
|
||||
|
||||
metrics:
|
||||
enabled: true
|
||||
|
||||
logs:
|
||||
enabled: true
|
||||
paths:
|
||||
- /var/log/syslog
|
||||
EOF
|
||||
|
||||
# Criar serviço systemd
|
||||
sudo tee /etc/systemd/system/ophion-agent.service << EOF
|
||||
# Criar serviço
|
||||
cat > /etc/systemd/system/ophion-agent.service << EOF
|
||||
[Unit]
|
||||
Description=OPHION Monitoring Agent
|
||||
Description=OPHION Agent
|
||||
After=network.target
|
||||
|
||||
[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
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Iniciar serviço
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable 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"
|
||||
# Iniciar
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now ophion-agent
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Monitoramento de Docker
|
||||
|
||||
### Opção A: Agent com Acesso ao Docker Socket
|
||||
## Comandos Úteis
|
||||
|
||||
```bash
|
||||
# Adicionar ao agent.yaml
|
||||
collectors:
|
||||
docker:
|
||||
enabled: true
|
||||
socket: /var/run/docker.sock
|
||||
collect_container_stats: true
|
||||
collect_container_logs: true
|
||||
```
|
||||
# Status
|
||||
ophion status
|
||||
|
||||
### Opção B: Container Dedicado
|
||||
# Logs
|
||||
ophion logs # Logs do server
|
||||
ophion logs ophion-web # Logs do dashboard
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml do seu projeto
|
||||
services:
|
||||
ophion-agent:
|
||||
image: ophion/agent:latest
|
||||
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
|
||||
```
|
||||
# Gerenciamento
|
||||
ophion start
|
||||
ophion stop
|
||||
ophion restart
|
||||
ophion update
|
||||
|
||||
### Métricas Coletadas do Docker
|
||||
# Backup
|
||||
ophion backup
|
||||
|
||||
| Métrica | Descrição |
|
||||
|---------|-----------|
|
||||
| `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"
|
||||
# Gerar nova API Key
|
||||
ophion api-key
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Monitoramento de Aplicações (APM)
|
||||
## Estrutura de Diretórios
|
||||
|
||||
### Node.js
|
||||
|
||||
```bash
|
||||
npm install @ophion/apm
|
||||
```
|
||||
|
||||
```javascript
|
||||
// No início do seu app (antes de outros imports)
|
||||
const ophion = require('@ophion/apm');
|
||||
|
||||
ophion.init({
|
||||
serverUrl: 'https://api.ophion.com.br',
|
||||
apiKey: 'YOUR_API_KEY',
|
||||
serviceName: 'minha-api',
|
||||
environment: 'production'
|
||||
});
|
||||
|
||||
// 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"
|
||||
/opt/ophion/
|
||||
├── docker-compose.yml # Configuração dos containers
|
||||
├── .env # Variáveis de ambiente (SECRETO!)
|
||||
├── data/
|
||||
│ ├── postgres/ # Dados do PostgreSQL
|
||||
│ ├── clickhouse/ # Métricas e logs
|
||||
│ └── redis/ # Cache
|
||||
├── configs/ # Configurações customizadas
|
||||
├── logs/ # Logs da aplicação
|
||||
├── scripts/
|
||||
│ └── install-agent.sh # Instalador do agent
|
||||
└── backups/ # Backups automáticos
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Configuração de Alertas
|
||||
## Portas
|
||||
|
||||
### Via Dashboard
|
||||
|
||||
1. Acesse **Alertas** → **Novo Alerta**
|
||||
2. Defina a condição:
|
||||
- Métrica: `cpu.usage`
|
||||
- Operador: `>`
|
||||
- Valor: `80`
|
||||
- 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
|
||||
| Serviço | Porta | Descrição |
|
||||
|---------|-------|-----------|
|
||||
| Dashboard | 3000 | Interface web |
|
||||
| API | 8080 | REST API |
|
||||
| PostgreSQL | 5432 | Banco de dados (interno) |
|
||||
| ClickHouse | 9000 | Métricas/Logs (interno) |
|
||||
| Redis | 6379 | Cache (interno) |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Containers não iniciam
|
||||
|
||||
```bash
|
||||
# Ver logs
|
||||
docker compose logs
|
||||
|
||||
# Verificar recursos
|
||||
docker system df
|
||||
df -h
|
||||
```
|
||||
|
||||
### Agent não conecta
|
||||
|
||||
```bash
|
||||
# Verificar conectividade
|
||||
curl -v https://api.ophion.com.br/health
|
||||
# Testar conectividade
|
||||
curl http://SEU-SERVIDOR:8080/health
|
||||
|
||||
# Verificar logs do agent
|
||||
# Ver logs do agent
|
||||
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
|
||||
|
||||
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
|
||||
### Resetar senha admin
|
||||
|
||||
```bash
|
||||
# Verificar permissões do socket
|
||||
ls -la /var/run/docker.sock
|
||||
|
||||
# Agent precisa estar no grupo docker
|
||||
sudo usermod -aG docker ophion-agent
|
||||
docker compose exec postgres psql -U ophion -c \
|
||||
"UPDATE users SET password_hash = crypt('nova-senha', gen_salt('bf')) WHERE email = 'admin@email.com';"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Suporte
|
||||
|
||||
- 📧 Email: suporte@ophion.com.br
|
||||
- 💬 Telegram: [@ophion_suporte](https://t.me/ophion_suporte)
|
||||
- 📖 Docs: https://docs.ophion.com.br
|
||||
- 🐙 GitHub: https://github.com/bigtux/ophion
|
||||
|
||||
---
|
||||
|
||||
*Made with 🖤 in Brazil*
|
||||
- 📖 Docs: https://docs.ophion.io
|
||||
- 💬 Discord: https://discord.gg/ophion
|
||||
- 🐛 Issues: https://github.com/bigtux/ophion/issues
|
||||
- 📧 Email: support@ophion.io
|
||||
|
||||
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 (
|
||||
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/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
|
||||
#
|
||||
# 🐍 OPHION - Instalador Interativo
|
||||
# Plataforma de Observabilidade Open Source
|
||||
#
|
||||
# Uso: curl -fsSL https://get.ophion.io | bash
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
echo "🐍 OPHION - Observability Platform Installer"
|
||||
echo "============================================="
|
||||
# Cores
|
||||
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
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "❌ Docker not found. Installing..."
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
fi
|
||||
# ASCII Art
|
||||
show_banner() {
|
||||
echo -e "${PURPLE}"
|
||||
cat << "EOF"
|
||||
____ _____ _ _ _____ ____ _ _
|
||||
/ __ \| __ \| | | |_ _/ __ \| \ | |
|
||||
| | | | |__) | |__| | | || | | | \| |
|
||||
| | | | ___/| __ | | || | | | . ` |
|
||||
| |__| | | | | | |_| || |__| | |\ |
|
||||
\____/|_| |_| |_|_____\____/|_| \_|
|
||||
|
||||
# Check Docker Compose
|
||||
if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
|
||||
echo "❌ Docker Compose not found. Please install it."
|
||||
Open Source Observability Platform
|
||||
Made with 🖤 in Brazil
|
||||
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
|
||||
fi
|
||||
|
||||
# Create directory
|
||||
INSTALL_DIR="${OPHION_DIR:-/opt/ophion}"
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
# Baixar agent
|
||||
curl -fsSL -o /tmp/ophion-agent "\${SERVER_URL}/downloads/agent/linux/amd64/ophion-agent"
|
||||
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"
|
||||
|
||||
# Download docker-compose
|
||||
echo "📥 Downloading OPHION..."
|
||||
curl -fsSL https://raw.githubusercontent.com/bigtux/ophion/main/deploy/docker/docker-compose.yml -o docker-compose.yml
|
||||
case "$1" in
|
||||
start)
|
||||
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
|
||||
JWT_SECRET=$(openssl rand -hex 32)
|
||||
echo "JWT_SECRET=$JWT_SECRET" > .env
|
||||
chmod +x "$INSTALL_DIR/ophion"
|
||||
sudo ln -sf "$INSTALL_DIR/ophion" /usr/local/bin/ophion
|
||||
|
||||
# Start services
|
||||
echo "🚀 Starting OPHION..."
|
||||
docker compose up -d
|
||||
log_success "CLI instalado em /usr/local/bin/ophion"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "✅ OPHION installed successfully!"
|
||||
echo ""
|
||||
echo "📊 Dashboard: http://localhost:3000"
|
||||
echo "🔌 API: http://localhost:8080"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Open http://localhost:3000 in your browser"
|
||||
echo "2. Create your admin account"
|
||||
echo "3. Add your first server with the agent"
|
||||
echo ""
|
||||
echo "To install the agent on a server:"
|
||||
echo " curl -fsSL https://get.ophion.io/agent | bash"
|
||||
echo ""
|
||||
# Resumo final
|
||||
show_summary() {
|
||||
echo ""
|
||||
echo -e "${GREEN}═══════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} 🎉 OPHION INSTALADO COM SUCESSO! ${NC}"
|
||||
echo -e "${GREEN}═══════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
echo -e " ${CYAN}Organização:${NC} $ORG_NAME"
|
||||
echo -e " ${CYAN}Admin:${NC} $ADMIN_EMAIL"
|
||||
echo ""
|
||||
echo -e " ${CYAN}Dashboard:${NC} http://${DOMAIN}:${DASHBOARD_PORT}"
|
||||
echo -e " ${CYAN}API:${NC} http://${DOMAIN}:${SERVER_PORT}"
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/limiter"
|
||||
)
|
||||
|
||||
func RateLimitMiddleware() fiber.Handler {
|
||||
return limiter.New(limiter.Config{
|
||||
Max: 100, // 100 requests
|
||||
Expiration: 1 * time.Minute, // per minute
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🚦 RATE LIMITING MIDDLEWARE
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// 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 {
|
||||
// Use API key or IP for rate limiting
|
||||
if key := c.Locals("api_key"); key != nil {
|
||||
return key.(string)
|
||||
}
|
||||
return c.IP()
|
||||
},
|
||||
LimitReached: func(c *fiber.Ctx) error {
|
||||
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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bigtux/ophion/internal/security"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var jwtSecret []byte
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🔐 AUTH CONFIG
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
func Init(secret string) {
|
||||
jwtSecret = []byte(secret)
|
||||
type AuthConfig struct {
|
||||
JWTSecret []byte
|
||||
JWTExpiration time.Duration
|
||||
RefreshExpiration time.Duration
|
||||
Issuer string
|
||||
}
|
||||
|
||||
// GenerateAPIKey creates a new API key for agents
|
||||
func GenerateAPIKey() string {
|
||||
bytes := make([]byte, 32)
|
||||
rand.Read(bytes)
|
||||
return "ophion_" + hex.EncodeToString(bytes)
|
||||
type AuthService struct {
|
||||
config AuthConfig
|
||||
redis *redis.Client
|
||||
rateLimiter *security.RateLimiter
|
||||
loginTracker *security.LoginAttemptTracker
|
||||
apiKeyStore APIKeyStore
|
||||
}
|
||||
|
||||
// GenerateJWT creates a JWT token for users
|
||||
func GenerateJWT(userID string, email string) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"sub": userID,
|
||||
"email": email,
|
||||
"iat": time.Now().Unix(),
|
||||
"exp": time.Now().Add(24 * time.Hour).Unix(),
|
||||
// APIKeyStore interface para storage de API keys
|
||||
type APIKeyStore interface {
|
||||
ValidateKey(ctx context.Context, keyHash string) (*APIKeyInfo, error)
|
||||
UpdateLastUsed(ctx context.Context, keyID string) error
|
||||
}
|
||||
|
||||
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)
|
||||
return token.SignedString(jwtSecret)
|
||||
accessToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims).SignedString(s.config.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
|
||||
func ValidateJWT(tokenString string) (*jwt.MapClaims, error) {
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
return jwtSecret, nil
|
||||
// ValidateAccessToken valida access token
|
||||
func (s *AuthService) ValidateAccessToken(tokenString string) (*TokenClaims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
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 {
|
||||
return nil, err
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid token: %w", err)
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, jwt.ErrInvalidKey
|
||||
claims, ok := token.Claims.(*TokenClaims)
|
||||
if !ok || !token.Valid {
|
||||
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
|
||||
func AuthMiddleware() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
authHeader := c.Get("Authorization")
|
||||
// RevokeToken revoga um token
|
||||
func (s *AuthService) RevokeToken(tokenID string, expiration time.Duration) error {
|
||||
if s.redis == nil {
|
||||
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 == "" {
|
||||
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 ")
|
||||
|
||||
// Check if it's an API key
|
||||
if strings.HasPrefix(token, "ophion_") {
|
||||
// TODO: Validate API key against database
|
||||
c.Locals("auth_type", "api_key")
|
||||
c.Locals("api_key", token)
|
||||
return c.Next()
|
||||
// API Key authentication
|
||||
if strings.HasPrefix(token, security.APIKeyPrefix) {
|
||||
return s.authenticateAPIKey(c, token)
|
||||
}
|
||||
|
||||
// Validate JWT
|
||||
claims, err := ValidateJWT(token)
|
||||
if err != nil {
|
||||
// JWT authentication
|
||||
return s.authenticateJWT(c, token)
|
||||
}
|
||||
}
|
||||
|
||||
// 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{
|
||||
"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("user_id", (*claims)["sub"])
|
||||
c.Locals("email", (*claims)["email"])
|
||||
c.Locals("user_id", claims.UserID)
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// 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